Publish only the distroless Flood runtime as latest
This commit is contained in:
1 parent
66e87558ce
commit
b1657c3b61
7 files changed
+59
-253
No files matched your search
@@ -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 }}
|
||||
@@ -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
@@ -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/"]
|
||||
@@ -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/"]
|
||||
@@ -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.
|
||||
@@ -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
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user