conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

commit 824a5e90651d6d841539f90b24aa6b0ba0c2328e
parent abab20c7e28927bb75b489d139188b82cf578ea9
Author: finwo <finwo@pm.me>
Date:   Sat, 19 Sep 2026 16:41:03 +0200

Alpine based images, reaching architectures node does not publish

Diffstat:
Mdeploy/Dockerfile | 44++++++++++++++++++++++++++++++++------------
Mdeploy/Dockerfile.worker | 52++++++++++++++++++++++++++--------------------------
Adeploy/docker-compose.build.yml | 25+++++++++++++++++++++++++
Mdeploy/docker-compose.yml | 20++++++++++----------
Mdeploy/worker/docker-compose.yml | 2+-
5 files changed, 94 insertions(+), 49 deletions(-)

diff --git a/deploy/Dockerfile b/deploy/Dockerfile @@ -1,29 +1,49 @@ # deploy/Dockerfile - the conductor # # Build from the repository root, since the context is the whole project: -# docker build -f deploy/Dockerfile -t conductor . +# docker build -f deploy/Dockerfile -t finwo/conductor . +# +# Alpine rather than the official node image, because that one is only +# published for amd64, arm64 and ppc64le, and this is meant to run on +# riscv64 as well. Alpine 3.23 carries node 24 on every architecture we +# publish for, so the base is the same everywhere and nothing has to be +# fetched from a second source at build time. # # State lives in /data: the sqlite database, the git mirrors, the log spool # and, unless object storage is configured, artifacts. Mount a volume there # or none of it survives a restart. -FROM node:24-bookworm-slim +ARG ALPINE_VERSION=3.23 -# git is needed for the mirrors the conductor reads pipelines and source -# archives out of. Everything else it does is in node. -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates git \ - && rm -rf /var/lib/apt/lists/* +# npm is only needed to resolve dependencies, so it stays in this stage and +# out of the image that ships. +FROM alpine:${ALPINE_VERSION} AS deps + +RUN apk add --no-cache nodejs npm WORKDIR /app -# Dependencies first, so editing source does not reinstall them. mysql2 and -# pg are optional and only pulled when the database calls for them; both are -# installed here so one image covers every backend. +# Dependencies before source, so editing code does not reinstall them. +# mysql2 and pg are optional and pure javascript; both are installed so a +# single image covers every database backend. COPY package.json ./ RUN npm install --omit=dev --no-audit --no-fund \ && npm cache clean --force +FROM alpine:${ALPINE_VERSION} + +# git is needed for the mirrors the conductor reads pipelines and source +# archives out of. Everything else it does is node, and node's sqlite is +# built in, so there is no database client to install. +RUN apk add --no-cache ca-certificates git nodejs + +# Alpine has no unprivileged user to borrow, unlike the node image. +RUN addgroup -S conductor && adduser -S -G conductor -h /app conductor + +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY package.json ./ COPY src/ ./src/ COPY migrations/ ./migrations/ COPY assets/ ./assets/ @@ -38,9 +58,9 @@ ENV NODE_ENV=production \ CONDUCTOR_MIRROR_PATH=/data/mirrors \ CONDUCTOR_LOG_PATH=/data/logs -RUN mkdir -p /data && chown -R node:node /data +RUN mkdir -p /data && chown -R conductor:conductor /data -USER node +USER conductor VOLUME ["/data"] EXPOSE 8080 diff --git a/deploy/Dockerfile.worker b/deploy/Dockerfile.worker @@ -1,7 +1,13 @@ # deploy/Dockerfile.worker - the worker # # Build from the repository root: -# docker build -f deploy/Dockerfile.worker -t conductor-worker . +# docker build -f deploy/Dockerfile.worker -t finwo/conductor-worker . +# +# Alpine rather than the official node image, for the same reason as the +# conductor: node is not published for riscv64, and Alpine 3.23 has both +# node 24 and a docker client on every architecture we publish for. That +# also means the docker client comes from the package manager rather than +# a tarball off download.docker.com, which has no riscv64 build at all. # # The worker runs each job in its own container, so it needs a docker # socket. It does not run a daemon of its own: the containers it starts are @@ -13,33 +19,13 @@ # compose file in deploy/worker does that; if you run this by hand, mount # the workspace at the path you configure rather than somewhere convenient. -FROM node:24-bookworm-slim - -# Static docker CLI, rather than docker.io, which would drag in a daemon -# this image has no use for. -ARG DOCKER_CLI_VERSION=27.3.1 - -# git is only needed for projects configured to clone rather than download -# a source archive; tar always is. -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates curl git tar \ - && arch="$(uname -m)" \ - && case "${arch}" in \ - x86_64) docker_arch=x86_64 ;; \ - aarch64) docker_arch=aarch64 ;; \ - *) echo "unsupported architecture: ${arch}" >&2; exit 1 ;; \ - esac \ - && curl -fsSL "https://download.docker.com/linux/static/stable/${docker_arch}/docker-${DOCKER_CLI_VERSION}.tgz" \ - | tar -xz -C /usr/local/bin --strip-components=1 docker/docker \ - && docker --version \ - && apt-get purge -y --auto-remove curl \ - && rm -rf /var/lib/apt/lists/* +ARG ALPINE_VERSION=3.23 -WORKDIR /app +FROM alpine:${ALPINE_VERSION} AS deps -# The source is ES modules, and node reparses every file with a warning -# unless the package says so. -RUN printf '{\n "name": "conductor-worker",\n "private": true,\n "type": "module"\n}\n' > package.json +RUN apk add --no-cache nodejs npm + +WORKDIR /app # The worker source needs no npm packages at all. yaml is installed only so # the configuration file may be YAML as well as JSON; nothing breaks @@ -47,6 +33,20 @@ RUN printf '{\n "name": "conductor-worker",\n "private": true,\n "type": "mod RUN npm install --omit=dev --no-audit --no-fund yaml \ && npm cache clean --force +FROM alpine:${ALPINE_VERSION} + +# docker-cli to start job containers on the host daemon, git for projects +# configured to clone rather than download an archive, and GNU tar because +# the busybox one is not a full substitute for reading source archives. +RUN apk add --no-cache ca-certificates docker-cli git nodejs tar + +WORKDIR /app + +# The source is ES modules, and node reparses every file with a warning +# unless the package says so. +RUN printf '{\n "name": "conductor-worker",\n "private": true,\n "type": "module"\n}\n' > package.json + +COPY --from=deps /app/node_modules ./node_modules COPY src/worker/ ./src/worker/ # Deliberately no CONDUCTOR_WORKER_CONFIG: the worker is configurable by diff --git a/deploy/docker-compose.build.yml b/deploy/docker-compose.build.yml @@ -0,0 +1,25 @@ +# deploy/docker-compose.build.yml - build the images instead of pulling +# +# The main compose file runs the published images, which is what a +# deployment wants. Layer this over it to build from the working tree: +# +# docker compose -f deploy/docker-compose.yml \ +# -f deploy/docker-compose.build.yml up -d --build +# +# The image names stay the same, so nothing else has to change and the +# locally built image simply shadows the published one. + +services: + conductor: + build: + context: .. + dockerfile: deploy/Dockerfile + image: "${CONDUCTOR_IMAGE:-finwo/conductor:latest}" + pull_policy: build + + worker: + build: + context: .. + dockerfile: deploy/Dockerfile.worker + image: "${CONDUCTOR_WORKER_IMAGE:-finwo/conductor-worker:latest}" + pull_policy: build diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml @@ -1,6 +1,12 @@ # deploy/docker-compose.yml - a conductor and one worker # -# docker compose -f deploy/docker-compose.yml up -d --build +# docker compose -f deploy/docker-compose.yml up -d +# +# That pulls the published images. To run your own build of them instead, +# add the override next to it: +# +# docker compose -f deploy/docker-compose.yml \ +# -f deploy/docker-compose.build.yml up -d --build # # Out of the box this is sqlite on a volume with artifacts on disk, which # is a perfectly reasonable way to run it. Postgres and MinIO are behind @@ -15,10 +21,7 @@ name: conductor services: conductor: - build: - context: .. - dockerfile: deploy/Dockerfile - image: conductor:latest + image: "${CONDUCTOR_IMAGE:-finwo/conductor:latest}" restart: unless-stopped ports: - "${CONDUCTOR_PORT:-8080}:8080" @@ -40,10 +43,7 @@ services: - conductor-data:/data worker: - build: - context: .. - dockerfile: deploy/Dockerfile.worker - image: conductor-worker:latest + image: "${CONDUCTOR_WORKER_IMAGE:-finwo/conductor-worker:latest}" restart: unless-stopped depends_on: - conductor @@ -51,7 +51,7 @@ services: CONDUCTOR_URL: http://conductor:8080 CONDUCTOR_WORKER_NAME: "${CONDUCTOR_WORKER_NAME:-compose-worker}" # Mint one with: docker compose exec conductor node src/admin-cli.js token:add compose-worker - CONDUCTOR_WORKER_TOKEN: "${CONDUCTOR_WORKER_TOKEN:?set CONDUCTOR_WORKER_TOKEN, see deploy/README}" + CONDUCTOR_WORKER_TOKEN: "${CONDUCTOR_WORKER_TOKEN:?set CONDUCTOR_WORKER_TOKEN, see docs/deployment.md}" CONDUCTOR_WORKER_ARCHES: "${CONDUCTOR_WORKER_ARCHES:-}" CONDUCTOR_WORKER_CONCURRENCY: "${CONDUCTOR_WORKER_CONCURRENCY:-2}" # The path has to be identical inside and outside this container. diff --git a/deploy/worker/docker-compose.yml b/deploy/worker/docker-compose.yml @@ -22,7 +22,7 @@ name: conductor-worker services: worker: - image: "${CONDUCTOR_WORKER_IMAGE:-conductor-worker:latest}" + image: "${CONDUCTOR_WORKER_IMAGE:-finwo/conductor-worker:latest}" restart: unless-stopped environment: CONDUCTOR_URL: "${CONDUCTOR_URL:?set CONDUCTOR_URL in .env}"