From 8e05c4eb53c0b5a14ab7d5dca07b1c23f8991754 Mon Sep 17 00:00:00 2001 From: Till Heidrich Date: Sat, 10 Oct 2026 06:30:57 +0000 Subject: [PATCH] A prebuilt image for amd64 and arm64, and the container no longer runs as root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until now `docker compose up` built the image on the user's machine: a compiler toolchain, every dev dependency, a few minutes of CPU — on a Raspberry Pi considerably more. The compose files now pull ghcr.io/tillheidrich/klarbild:1, built by .github/workflows/image.yml for linux/amd64 and linux/arm64. `:1` follows every 1.x release, so an update is `docker compose pull && docker compose up -d`. `build: .` is one commented line away for anyone who prefers to build. The Dockerfile has two stages now. The runtime stage carries the built server, production dependencies and the migrations — no compiler, no dev dependencies, no source tree. argon2 and sharp ship prebuilt binaries for both architectures, so python/make/g++ are gone entirely. It runs as the `node` user (uid 1000); a fresh named volume at /data inherits that ownership. Verified locally: image builds, boots against Postgres, applies all 17 migrations, prints the first-run password, passes its healthcheck, accepts an upload as the unprivileged user and returns an A4 sheet that measures 210.00 x 297.00 mm. --- .dockerignore | 5 +++ .github/workflows/image.yml | 62 +++++++++++++++++++++++++++++++++++++ Dockerfile | 32 +++++++++++++------ docker-compose.full.yaml | 8 +++-- docker-compose.yaml | 10 +++--- 5 files changed, 100 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/image.yml diff --git a/.dockerignore b/.dockerignore index b5e923f..cdc556b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,8 @@ dist *.log .env # Do NOT ignore migrations/ — they are read at runtime, on every boot. +docs +tests +.github +*.md +!README.md diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml new file mode 100644 index 0000000..271fd16 --- /dev/null +++ b/.github/workflows/image.yml @@ -0,0 +1,62 @@ +name: Container image + +# Builds the image that docker-compose.yaml pulls, for amd64 and arm64 +# (a Raspberry Pi 4/5 or an ARM VPS runs the same image). +# +# push to main -> ghcr.io/tillheidrich/klarbild:edge +# tag v1.2.3 -> :1.2.3, :1.2, :1 and :latest +# +# Nothing to configure: the workflow pushes with its own GITHUB_TOKEN. + +on: + push: + branches: [main] + tags: ['v*.*.*'] + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: image-${{ github.ref }} + cancel-in-progress: true + +jobs: + image: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/klarbild + tags: | + type=edge,branch=main + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + labels: | + org.opencontainers.image.title=Klarbild + org.opencontainers.image.description=Self-hosted print studio: pictures on paper at exactly the size you asked for. + org.opencontainers.image.licenses=AGPL-3.0-only + + - uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 1451756..e0e1cb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,34 @@ -FROM node:22-bookworm-slim +# Klarbild — two stages. The first builds, the second only runs: no compiler, +# no dev dependencies, no source tree in the image that ends up on your server. +FROM node:22-bookworm-slim AS build WORKDIR /app -ENV NODE_ENV=production PORT=4321 HOST=0.0.0.0 - -# Build tools for the native modules (argon2). sharp ships a prebuilt libvips. -RUN apt-get update && apt-get install -y --no-install-recommends \ - python3 make g++ ca-certificates \ - && rm -rf /var/lib/apt/lists/* - # The lockfile comes first and `npm ci` honours it, so the image is built from # the exact versions CI tested — not from whatever `^` resolved to today. +# Native modules (argon2, sharp) ship prebuilt binaries for amd64 and arm64, +# so no compiler is needed in either stage. COPY package.json package-lock.json ./ RUN npm ci --no-audit --no-fund - COPY . . -RUN npm run build +RUN npm run build && npm prune --omit=dev --no-audit --no-fund + +FROM node:22-bookworm-slim +WORKDIR /app +ENV NODE_ENV=production PORT=4321 HOST=0.0.0.0 STORAGE_DIR=/data + +COPY --from=build --chown=node:node /app/package.json ./ +COPY --from=build --chown=node:node /app/node_modules ./node_modules +COPY --from=build --chown=node:node /app/dist ./dist +# Read at runtime, on every boot. +COPY --from=build --chown=node:node /app/migrations ./migrations + +# Not root. A fresh named volume mounted at /data takes over this ownership; +# for a bind mount, give the directory to uid 1000 (`chown 1000:1000 ./data`). +RUN mkdir -p /data && chown node:node /data +USER node EXPOSE 4321 +VOLUME ["/data"] HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||4321)+'/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" diff --git a/docker-compose.full.yaml b/docker-compose.full.yaml index f5c36ea..2bd9081 100644 --- a/docker-compose.full.yaml +++ b/docker-compose.full.yaml @@ -9,9 +9,11 @@ services: app: - build: - context: . - dockerfile: Dockerfile + # The prebuilt image (amd64 and arm64). `:1` follows every 1.x release, so + # `docker compose pull && docker compose up -d` is the whole update. + image: ghcr.io/tillheidrich/klarbild:${KLARBILD_VERSION:-1} + # Rather build it yourself from this checkout? Swap the line above for: + # build: . environment: PORT: "4321" DATABASE_URL: "postgres://klarbild:${DB_PASSWORD:?set DB_PASSWORD in .env}@db:5432/klarbild" diff --git a/docker-compose.yaml b/docker-compose.yaml index 0b21f64..1c46d19 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,6 +1,6 @@ # Klarbild — the default stack: the print studio and a database. That is all. # -# cp .env.example .env # fill in the three secrets at the top +# cp .env.example .env # fill in the values at the top; the file says which # docker compose up -d # docker compose logs app # the first-start password is printed once, here # @@ -9,9 +9,11 @@ services: app: - build: - context: . - dockerfile: Dockerfile + # The prebuilt image (amd64 and arm64). `:1` follows every 1.x release, so + # `docker compose pull && docker compose up -d` is the whole update. + image: ghcr.io/tillheidrich/klarbild:${KLARBILD_VERSION:-1} + # Rather build it yourself from this checkout? Swap the line above for: + # build: . environment: PORT: "4321" DATABASE_URL: "postgres://klarbild:${DB_PASSWORD:?set DB_PASSWORD in .env}@db:5432/klarbild"