Publish only the distroless Flood runtime as latest

This commit is contained in:
Ketan Patel committed 2026-10-03 00:06:02 -04:00
1 parent 66e87558ce
commit b1657c3b61
7 files changed
+59 -253

No files matched your search

-71
View File
@@ -1,71 +0,0 @@
name: Build Distroless Flood
on:
workflow_dispatch:
inputs:
flood_version:
description: 'jesec/flood release tag'
required: true
default: 'v4.16.2'
type: string
jobs:
build:
runs-on: kube
if: github.ref == 'refs/heads/main'
env:
DOCKER_BUILD_RECORD_UPLOAD: 'false'
DOCKER_BUILD_SUMMARY: 'false'
steps:
- name: Checkout code
uses: https://github.com/actions/checkout@v4
with:
persist-credentials: false
- name: Fetch Flood release
env:
FLOOD_VERSION: ${{ github.event.inputs.flood_version }}
run: |
test -n "$FLOOD_VERSION" || { echo "flood_version is required" >&2; exit 1; }
git check-ref-format "refs/tags/$FLOOD_VERSION"
git init flood-repo
git -C flood-repo fetch --depth=1 https://github.com/jesec/flood.git "refs/tags/$FLOOD_VERSION"
git -C flood-repo checkout --detach FETCH_HEAD
cp Dockerfile.distroless flood-repo/Dockerfile.distroless
cp build/runtime-root.py flood-repo/.ci-runtime-root.py
cp tests/startup-distroless.cjs flood-repo/.ci-startup-distroless.cjs
printf '\n.git\n.github\n.env\n' >> flood-repo/.dockerignore
- name: Install Docker CLI
run: |
apt-get update
apt-get install -y --no-install-recommends docker.io
rm -rf /var/lib/apt/lists/*
- name: Log in to Docker Hub
uses: https://github.com/docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Set up temporary builder
uses: https://github.com/docker/setup-buildx-action@v3
with:
driver: docker-container
cache-binary: false
keep-state: false
cleanup: true
- name: Build and push to Docker Hub
uses: https://github.com/docker/build-push-action@v6
with:
context: ./flood-repo
file: ./flood-repo/Dockerfile.distroless
platforms: linux/amd64
pull: true
push: true
load: false
tags: docker.io/k2patel/floodui:distroless
labels: |
org.opencontainers.image.source=https://git.k2patel.in/k2patel/docker-flood-client
org.opencontainers.image.revision=${{ github.sha }}
+2 -1
View File
@@ -1,4 +1,4 @@
name: Build and Push Custom Flood Image
name: Build and Push Distroless Flood
on:
workflow_dispatch:
@@ -32,6 +32,7 @@ jobs:
git -C flood-repo fetch --depth=1 https://github.com/jesec/flood.git "refs/tags/$FLOOD_VERSION"
git -C flood-repo checkout --detach FETCH_HEAD
cp Dockerfile.client flood-repo/Dockerfile.client
cp build/runtime-root.py flood-repo/.ci-runtime-root.py
cp tests/startup.cjs flood-repo/.ci-startup.cjs
printf '\n.git\n.github\n.env\n' >> flood-repo/.dockerignore
+21 -49
View File
@@ -1,55 +1,27 @@
# Flood UI only: connects to an external torrent service. No bundled rTorrent.
# Build from a clean upstream release checkout using the Gitea workflow.
ARG BUILDPLATFORM=amd64
# Alpine-built binaries and musl stay together; no shell/package manager in final image.
ARG NODE_IMAGE=docker.io/node:24.18.1-alpine3.24
FROM --platform=$BUILDPLATFORM ${NODE_IMAGE} as nodebuild
WORKDIR /usr/src/app/
# Copy project files
FROM ${NODE_IMAGE} AS nodebuild
WORKDIR /usr/src/app
COPY . ./
# Use the package manager and lockfile declared by the upstream release.
RUN npm install -g corepack && corepack enable && corepack install
RUN pnpm install --frozen-lockfile
# Build Flood server and client assets.
RUN npm install -g corepack && corepack enable && corepack install \
&& pnpm install --frozen-lockfile
RUN npm run build
# Now get the clean Node.js image
FROM ${NODE_IMAGE} as flood
FROM ${NODE_IMAGE} AS runtime
RUN apk add --no-cache mediainfo coreutils ca-certificates tzdata \
lddtreepax python3
COPY .ci-runtime-root.py /build/runtime-root.py
RUN python3 /build/runtime-root.py /runtime /usr/local/bin/node \
/usr/bin/mediainfo "$(command -v df)"
COPY --from=nodebuild /usr/src/app/dist /runtime/usr/src/app/dist
COPY --from=nodebuild /usr/src/app/package.json /runtime/usr/src/app/package.json
COPY --from=nodebuild /usr/src/app/LICENSE /runtime/usr/src/app/LICENSE
WORKDIR /usr/src/app/
# Copy sources
COPY --from=nodebuild /usr/src/app ./
# Install runtime dependencies
RUN apk --no-cache add \
mediainfo
# Create "download" user
# RUN adduser -h /home/download -s /sbin/nologin --disabled-password download
# Run as "download" user
# USER download
# Expose port 3000 and 4200
FROM scratch
COPY --from=runtime /runtime/ /
ENV NODE_ENV=production HOME=/root PATH=/usr/local/bin:/usr/bin:/bin
WORKDIR /usr/src/app
RUN --mount=type=bind,from=nodebuild,source=/usr/src/app/.ci-startup.cjs,target=/ci-startup.cjs ["/usr/local/bin/node", "/ci-startup.cjs"]
VOLUME ["/server/db"]
EXPOSE 3000
EXPOSE 4200
# Creating mountpoint for config
RUN mkdir -p /server/db/
RUN apk update
RUN apk add coreutils
# System local configuration
VOLUME /server/db/
# Fail the build before publication if the actual server cannot start.
RUN node .ci-startup.cjs && rm .ci-startup.cjs
# Start Flood only, using the upstream v4.16.2 production entrypoint.
ENTRYPOINT ["npm", "--prefix=/usr/src/app/", "run", "start", "--", "--host=0.0.0.0", "--rundir=/server/"]
ENTRYPOINT ["/usr/local/bin/node", "--enable-source-maps", "--use_strict", "dist/index.js", "--host=0.0.0.0", "--rundir=/server/"]
-27
View File
@@ -1,27 +0,0 @@
# Alpine-built binaries and musl stay together; no shell/package manager in final image.
ARG NODE_IMAGE=docker.io/node:24.18.1-alpine3.24
FROM ${NODE_IMAGE} AS nodebuild
WORKDIR /usr/src/app
COPY . ./
RUN npm install -g corepack && corepack enable && corepack install \
&& pnpm install --frozen-lockfile
RUN npm run build
FROM ${NODE_IMAGE} AS runtime
RUN apk add --no-cache mediainfo coreutils ca-certificates tzdata \
lddtreepax python3
COPY .ci-runtime-root.py /build/runtime-root.py
RUN python3 /build/runtime-root.py /runtime /usr/local/bin/node \
/usr/bin/mediainfo "$(command -v df)"
COPY --from=nodebuild /usr/src/app/dist /runtime/usr/src/app/dist
COPY --from=nodebuild /usr/src/app/package.json /runtime/usr/src/app/package.json
COPY --from=nodebuild /usr/src/app/LICENSE /runtime/usr/src/app/LICENSE
FROM scratch
COPY --from=runtime /runtime/ /
ENV NODE_ENV=production HOME=/root PATH=/usr/local/bin:/usr/bin:/bin
WORKDIR /usr/src/app
RUN --mount=type=bind,from=nodebuild,source=/usr/src/app/.ci-startup-distroless.cjs,target=/ci-startup.cjs ["/usr/local/bin/node", "/ci-startup.cjs"]
VOLUME ["/server/db"]
EXPOSE 3000
ENTRYPOINT ["/usr/local/bin/node", "--enable-source-maps", "--use_strict", "dist/index.js", "--host=0.0.0.0", "--rundir=/server/"]
+28 -45
View File
@@ -1,55 +1,38 @@
# docker-flood-client
Flood client image. Source: https://git.k2patel.in/k2patel/docker-flood-client
Source: https://git.k2patel.in/k2patel/docker-flood-client
Public access is read-only; account registration on this server is disabled.
Report issues at https://github.com/k2patel/docker-flood-client/issues.
For contributions, open an issue with a link to your branch or patch on another
Git provider. Changes are reviewed and applied by the maintainer on Gitea.
`docker.io/k2patel/floodui:latest` is the only maintained image variant and is
an Alpine/musl distroless runtime. The separate `:distroless` tag is retired.
It runs the Flood web UI/API and connects to an external torrent service;
it does not bundle or start rTorrent.
Gitea Actions publishes `docker.io/k2patel/floodui:latest` only on manual runs.
Select `main` in Actions → Build and Push Custom Flood Image → Run workflow
and set the required `flood_version` release tag. The configured release is
`v4.16.2`. Repository pushes do not trigger builds.
## Build and publication
This is a Flood-only image using the custom `Dockerfile.client`: it runs the
Flood web UI/API and connects to an external torrent service. It does not bundle
or start rTorrent. The Dockerfile uses the upstream pnpm lockfile and builds the server and client
assets; changing release families may require updating the build tooling.
Gitea Actions publishes only on manual runs. Select `main` in Actions →
**Build and Push Distroless Flood** → Run workflow and set the required
`flood_version` release tag (default `v4.16.2`). Pushes do not trigger builds.
The upstream pnpm lockfile builds the server and UI; switching release families
may require changes to the build tooling.
Set Actions secrets `DOCKER_USERNAME` and `DOCKER_TOKEN` with Docker Hub write
access to `k2patel/floodui`. Builds run remotely on the Kubernetes runner for
`linux/amd64`, pushing directly to Docker Hub. Images are not loaded into the
runner image store, and the temporary builder and cache are removed afterward.
No image archives or build records are uploaded to Gitea, and no local container
builds are required.
The workflow uses `Dockerfile.client`, Node 24.18.1 on Alpine 3.24, and a scratch
final image. It includes Node, the bundled server/UI, mediainfo, GNU df, their
musl libraries, CA certificates and timezone data. There is no shell, npm/pnpm,
package manager, source tree, node_modules or compiler in the runtime.
Node starts directly with `--host=0.0.0.0 --rundir=/server/`.
Base image: Node.js 24.18.1 on Alpine Linux 3.24 stable (`node:24.18.1-alpine3.24`),
with the Alpine stable series explicitly selected.
CI starts Flood in the stripped filesystem and requires HTTP 200, working
mediainfo/df, and absence of shell/package-manager binaries before publication.
Builds use the Kubernetes runner for `linux/amd64`, pushing directly to Docker
Hub; temporary builders and caches are removed afterward. Configure Actions
secrets `DOCKER_USERNAME` and `DOCKER_TOKEN` with write access to `k2patel/floodui`.
The image build starts the actual Flood server and requires HTTP 200 before
publishing; it does not start a torrent daemon.
## Updating an existing deployment
The entrypoint uses `npm run start`, matching upstream v4.16.2, while retaining
`--host=0.0.0.0` and `--rundir=/server/`. The upstream combined rTorrent stage
is deliberately excluded.
Use `k2patel/floodui:latest`, pull, and recreate the service. Keep existing
connection settings and mounts. Root UID, port 3000 and `/server/db` are unchanged.
The default Compose file in docker-rtorrent uses distroless `:latest` for both
services and supplies rTorrent's native health check; no overlay is needed.
## Distroless variant
The manual **Build Distroless Flood** workflow publishes `k2patel/floodui:distroless`
for the selected Flood release (default `v4.16.2`). It does not change `latest`.
The Alpine 3.24-derived runtime contains Node 24.18.1, the bundled Flood server/UI,
mediainfo, GNU df, their musl libraries, CA certificates and timezone data. The
final image is built from scratch, with no shell, npm/pnpm, package manager,
node_modules, source tree or build tools. It starts Node directly and remains
separate from rTorrent. Root UID, port 3000, and `/server/db` stay compatible.
Before publication, CI starts Flood inside the exact stripped filesystem and
requires an HTTP 200 response; it also checks mediainfo and df can execute and
that shell/package-manager binaries are absent. All builds run remotely, and
the temporary builder/cache is removed afterward.
Change only the Compose image tag to `k2patel/floodui:distroless`; keep existing
mounts and connection settings. Revert to `latest` to roll back. A matching
rTorrent Compose overlay is provided in the docker-rtorrent repository.
Public access is read-only; account registration is disabled. Maintainer-applied
contributions can be based on a branch or patch against the current Gitea source.
-57
View File
@@ -1,57 +0,0 @@
const {spawn, execFileSync} = require('node:child_process');
const http = require('node:http');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
for (const file of ['/bin/sh', '/bin/bash', '/bin/busybox', '/sbin/apk', '/usr/local/bin/npm']) {
if (fs.existsSync(file)) throw new Error(`Unexpected runtime tool: ${file}`);
}
execFileSync('mediainfo', ['--Version'], {stdio: 'inherit'});
execFileSync('df', ['--version'], {stdio: 'inherit'});
const runDir = fs.mkdtempSync(path.join(os.tmpdir(), 'flood-smoke-'));
const child = spawn(process.execPath, ['--enable-source-maps', '--use_strict', 'dist/index.js',
'--host=127.0.0.1', '--port=3000', `--rundir=${runDir}`],
{cwd: '/usr/src/app', stdio: 'inherit', detached: true});
let startupError;
child.on('error', error => { startupError = error; });
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const check = () => new Promise(resolve => {
const req = http.get('http://127.0.0.1:3000/', response => {
let bytes = 0;
response.on('data', chunk => { bytes += chunk.length; });
response.on('end', () => resolve(response.statusCode === 200 && bytes > 0));
});
req.setTimeout(1000, () => req.destroy());
req.on('error', () => resolve(false));
});
(async () => {
try {
const deadline = Date.now() + 60000;
while (Date.now() < deadline) {
if (startupError) throw startupError;
if (child.exitCode !== null || child.signalCode !== null) {
throw new Error(`Flood exited before serving HTTP: ${child.exitCode ?? child.signalCode}`);
}
if (await check()) {
console.log('Flood startup smoke test passed: HTTP 200 with response body');
return;
}
await sleep(500);
}
throw new Error('Flood did not serve HTTP within 60 seconds');
} catch (error) {
console.error(error);
process.exitCode = 1;
} finally {
if (child.pid) {
try { process.kill(-child.pid, 'SIGTERM'); } catch (error) {
if (error.code !== 'ESRCH') throw error;
}
await sleep(500);
try { process.kill(-child.pid, 'SIGKILL'); } catch (error) {
if (error.code !== 'ESRCH') throw error;
}
}
fs.rmSync(runDir, {recursive: true, force: true});
}
})();
+8 -3
View File
@@ -1,12 +1,17 @@
const {spawn} = require('node:child_process');
const {spawn, execFileSync} = require('node:child_process');
const http = require('node:http');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
for (const file of ['/bin/sh', '/bin/bash', '/bin/busybox', '/sbin/apk', '/usr/local/bin/npm']) {
if (fs.existsSync(file)) throw new Error(`Unexpected runtime tool: ${file}`);
}
execFileSync('mediainfo', ['--Version'], {stdio: 'inherit'});
execFileSync('df', ['--version'], {stdio: 'inherit'});
const runDir = fs.mkdtempSync(path.join(os.tmpdir(), 'flood-smoke-'));
const child = spawn('npm', ['run', 'start', '--',
const child = spawn(process.execPath, ['--enable-source-maps', '--use_strict', 'dist/index.js',
'--host=127.0.0.1', '--port=3000', `--rundir=${runDir}`],
{stdio: 'inherit', detached: true});
{cwd: '/usr/src/app', stdio: 'inherit', detached: true});
let startupError;
child.on('error', error => { startupError = error; });
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));