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: on:
workflow_dispatch: 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 fetch --depth=1 https://github.com/jesec/flood.git "refs/tags/$FLOOD_VERSION"
git -C flood-repo checkout --detach FETCH_HEAD git -C flood-repo checkout --detach FETCH_HEAD
cp Dockerfile.client flood-repo/Dockerfile.client 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 cp tests/startup.cjs flood-repo/.ci-startup.cjs
printf '\n.git\n.github\n.env\n' >> flood-repo/.dockerignore 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. # Alpine-built binaries and musl stay together; no shell/package manager in final image.
# Build from a clean upstream release checkout using the Gitea workflow.
ARG BUILDPLATFORM=amd64
ARG NODE_IMAGE=docker.io/node:24.18.1-alpine3.24 ARG NODE_IMAGE=docker.io/node:24.18.1-alpine3.24
FROM ${NODE_IMAGE} AS nodebuild
FROM --platform=$BUILDPLATFORM ${NODE_IMAGE} as nodebuild WORKDIR /usr/src/app
WORKDIR /usr/src/app/
# Copy project files
COPY . ./ COPY . ./
RUN npm install -g corepack && corepack enable && corepack install \
# Use the package manager and lockfile declared by the upstream release. && pnpm install --frozen-lockfile
RUN npm install -g corepack && corepack enable && corepack install
RUN pnpm install --frozen-lockfile
# Build Flood server and client assets.
RUN npm run build RUN npm run build
# Now get the clean Node.js image FROM ${NODE_IMAGE} AS runtime
FROM ${NODE_IMAGE} as flood 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/ FROM scratch
COPY --from=runtime /runtime/ /
# Copy sources ENV NODE_ENV=production HOME=/root PATH=/usr/local/bin:/usr/bin:/bin
COPY --from=nodebuild /usr/src/app ./ 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"]
# Install runtime dependencies VOLUME ["/server/db"]
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
EXPOSE 3000 EXPOSE 3000
EXPOSE 4200 ENTRYPOINT ["/usr/local/bin/node", "--enable-source-maps", "--use_strict", "dist/index.js", "--host=0.0.0.0", "--rundir=/server/"]
# 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/"]
-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 # 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. `docker.io/k2patel/floodui:latest` is the only maintained image variant and is
Report issues at https://github.com/k2patel/docker-flood-client/issues. an Alpine/musl distroless runtime. The separate `:distroless` tag is retired.
For contributions, open an issue with a link to your branch or patch on another It runs the Flood web UI/API and connects to an external torrent service;
Git provider. Changes are reviewed and applied by the maintainer on Gitea. it does not bundle or start rTorrent.
Gitea Actions publishes `docker.io/k2patel/floodui:latest` only on manual runs. ## Build and publication
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.
This is a Flood-only image using the custom `Dockerfile.client`: it runs the Gitea Actions publishes only on manual runs. Select `main` in Actions →
Flood web UI/API and connects to an external torrent service. It does not bundle **Build and Push Distroless Flood** → Run workflow and set the required
or start rTorrent. The Dockerfile uses the upstream pnpm lockfile and builds the server and client `flood_version` release tag (default `v4.16.2`). Pushes do not trigger builds.
assets; changing release families may require updating the build tooling. 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 The workflow uses `Dockerfile.client`, Node 24.18.1 on Alpine 3.24, and a scratch
access to `k2patel/floodui`. Builds run remotely on the Kubernetes runner for final image. It includes Node, the bundled server/UI, mediainfo, GNU df, their
`linux/amd64`, pushing directly to Docker Hub. Images are not loaded into the musl libraries, CA certificates and timezone data. There is no shell, npm/pnpm,
runner image store, and the temporary builder and cache are removed afterward. package manager, source tree, node_modules or compiler in the runtime.
No image archives or build records are uploaded to Gitea, and no local container Node starts directly with `--host=0.0.0.0 --rundir=/server/`.
builds are required.
Base image: Node.js 24.18.1 on Alpine Linux 3.24 stable (`node:24.18.1-alpine3.24`), CI starts Flood in the stripped filesystem and requires HTTP 200, working
with the Alpine stable series explicitly selected. 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 ## Updating an existing deployment
publishing; it does not start a torrent daemon.
The entrypoint uses `npm run start`, matching upstream v4.16.2, while retaining Use `k2patel/floodui:latest`, pull, and recreate the service. Keep existing
`--host=0.0.0.0` and `--rundir=/server/`. The upstream combined rTorrent stage connection settings and mounts. Root UID, port 3000 and `/server/db` are unchanged.
is deliberately excluded. 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 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.
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.
-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 http = require('node:http');
const fs = require('node:fs'); const fs = require('node:fs');
const os = require('node:os'); const os = require('node:os');
const path = require('node:path'); 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 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}`], '--host=127.0.0.1', '--port=3000', `--rundir=${runDir}`],
{stdio: 'inherit', detached: true}); {cwd: '/usr/src/app', stdio: 'inherit', detached: true});
let startupError; let startupError;
child.on('error', error => { startupError = error; }); child.on('error', error => { startupError = error; });
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));