Skip to content

feat(ci): publish Docker images to GHCR for self-hosters#318

Merged
thewrz merged 4 commits into
mainfrom
feat/ghcr-publish
May 17, 2026
Merged

feat(ci): publish Docker images to GHCR for self-hosters#318
thewrz merged 4 commits into
mainfrom
feat/ghcr-publish

Conversation

@thewrz

@thewrz thewrz commented May 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #312.

Adds a CI/CD workflow that builds and publishes the API and frontend Docker images to GitHub Container Registry on every push to main and on every v* tag. Community users can now run WrzDJ without building the images manually.

Images published:

  • ghcr.io/wrzonance/wrzdj-api — FastAPI backend
  • ghcr.io/wrzonance/wrzdj-web — Next.js frontend

Tags produced: latest (main only), v2026.MM.DD (on tags), sha-<short> (every push), pr-N (PR builds, no push).

What's New

  • .github/workflows/docker-publish.yml — multi-arch publish workflow (linux/amd64 + linux/arm64)
  • deploy/docker-compose.ghcr.yml — pull-from-GHCR compose file for end users
  • deploy/deploy-ghcr.sh — helper script (pull + restart + healthcheck)
  • dashboard/docker-entrypoint.sh — runtime substitution of the build-time NEXT_PUBLIC_API_URL placeholder
  • deploy/DEPLOYMENT.md — Quick Start section for pre-built images

Why the Entrypoint Sed Pass

NEXT_PUBLIC_API_URL is baked into the Next.js bundle at build time — in JS chunks AND in routes-manifest.json (where the CSP connect-src header lives). A distributable image can't know the operator's API URL at build time. The workflow builds with placeholder __WRZDJ_API_URL__; the entrypoint substitutes the runtime NEXT_PUBLIC_API_URL env var across both .js and .json files in /app/.next/ before starting the server.

Operators set NEXT_PUBLIC_API_URL in their .env; the existing deploy/docker-compose.yml build-from-source path is unchanged.

Test Plan

Verified locally end-to-end:

  • Backend image builds clean
  • Frontend image builds clean with --build-arg NEXT_PUBLIC_API_URL=__WRZDJ_API_URL__
  • Pre-substitution: 14 files contain the placeholder, including routes-manifest.json (the CSP gatekeeper)
  • Post-substitution: 0 placeholder occurrences remain
  • Full stack via deploy/docker-compose.ghcr.yml: db + api + web all healthy
  • CSP header in actual HTTP response reflects the runtime NEXT_PUBLIC_API_URL (not the placeholder)
  • docker compose -f deploy/docker-compose.ghcr.yml config validates cleanly
  • No PII / hardcoded personal infrastructure references
  • CI smoke test (docker-build job) removed from ci.yml — superseded by pull_request builds in this workflow

CI will validate multi-arch (amd64 + arm64) on push.

One-Time Post-Merge Manual Step

GHCR packages default to private on first publish, even on a public repo:

  1. After first push to main lands, check https://github.com/wrzonance?tab=packages
  2. For each package (wrzdj-api, wrzdj-web) → Package settings → Change visibility → Public
  3. Optionally: Connect Repository → WrzDJ to link the package sidebar

Summary by CodeRabbit

  • New Features

    • Publish API and web Docker images to GitHub Container Registry.
    • Add deploy script and compose file to run pre-built images with automated health checks and port handling.
    • Container startup now supports runtime API URL substitution for pre-built web images.
  • Documentation

    • Quick-start guide for deploying pre-built images added to deployment docs; example env includes version setting.
  • Chores

    • CI workflow no longer runs the post-test Docker build smoke job.

Review Change Stack

claude and others added 2 commits May 16, 2026 17:27
Adds a parallel community deployment path via pre-built images at
ghcr.io/wrzonance/wrzdj-{api,web}. The owner VPS workflow (deploy.sh)
is unchanged; this adds docker-publish.yml, docker-compose.ghcr.yml,
and deploy-ghcr.sh alongside it.

NEXT_PUBLIC_API_URL is baked at build time as __WRZDJ_API_URL__ and
sed-replaced at container startup via docker-entrypoint.sh so a single
image works across any deployment target. SHA pins removed from base
images for multi-arch (linux/amd64, linux/arm64) compatibility.

Removes the plain docker-build smoke-test job from ci.yml; the new
publish workflow's pull_request trigger covers it with BuildKit.

Closes #312

https://claude.ai/code/session_01Nh9hSosLdUYBAGUsKkwBjC
- entrypoint: broaden find pattern to .json so routes-manifest.json gets
  patched; without this the baked CSP connect-src header keeps the
  __WRZDJ_API_URL__ placeholder and browsers block every API call.
  Switch sed delimiter from | to @ for URL-safety.
- Dockerfile: replace COPY --chmod=755 (BuildKit-only) with COPY +
  RUN chmod +x so the image builds on legacy Docker as well as Buildx.
- deploy-ghcr.sh: mirror deploy.sh port-kill step between `down` and
  `up` — docker compose down alone does not always release ports.

Verified locally:
- both images build
- placeholder lives in routes-manifest.json + 13 .js chunks pre-substitution
- entrypoint reduces remaining placeholder occurrences to zero
- full stack via deploy/docker-compose.ghcr.yml: API health 200, web 200,
  CSP header reflects runtime NEXT_PUBLIC_API_URL value
@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d080f59b-4777-44dd-a4ea-fd0b6e3d69f2

📥 Commits

Reviewing files that changed from the base of the PR and between e41d0f6 and ae8ac0a.

📒 Files selected for processing (1)
  • dashboard/docker-entrypoint.sh
✅ Files skipped from review due to trivial changes (1)
  • dashboard/docker-entrypoint.sh

📝 Walkthrough

Walkthrough

Adds GHCR publishing CI, dashboard runtime API URL substitution, GHCR-based docker-compose and deploy script, and quick-start deployment docs and .env example for running pre-built images.

Changes

Pre-built Image Publishing and Deployment

Layer / File(s) Summary
Dockerfiles and dashboard entrypoint
server/Dockerfile, dashboard/Dockerfile, dashboard/docker-entrypoint.sh
Server and dashboard Dockerfiles use floating base tags. Dashboard adds an entrypoint that conditionally performs in-place __WRZDJ_API_URL__NEXT_PUBLIC_API_URL substitution in compiled Next.js artifacts before exec'ing the container command.
Docker image publishing CI/CD
.github/workflows/docker-publish.yml, .github/workflows/ci.yml, .github/workflows/release.yml
Removes CI docker-build smoke job. Adds docker-publish workflow that builds multi-arch API (./server) and web (./dashboard) images and conditionally pushes to GHCR for non-PR events. Release workflow bundles GHCR deploy assets into the release archive.
Pre-built image deployment compose & script
deploy/docker-compose.ghcr.yml, deploy/deploy-ghcr.sh
Adds docker-compose.ghcr.yml to run db, api, and web from GHCR images with healthchecks and container hardening. Adds deploy-ghcr.sh to pull images, stop/replace running stack (force-kill port holders), start the stack, and wait up to 60s for API /health.
Deployment docs and env
deploy/DEPLOYMENT.md, deploy/.env.example
Adds a Quick Start section to DEPLOYMENT.md describing pre-built image deployment and GHCR image locations. Adds WRZDJ_VERSION (default latest) to .env.example with guidance to pin to releases or tags.

Sequence Diagram(s)

sequenceDiagram
  participant GitHub as GitHub Actions
  participant Buildx as Docker Buildx
  participant GHCR as GHCR Registry
  GitHub->>Buildx: Build API image from ./server
  Buildx->>GHCR: Push multi-platform API image (when not PR)
  GitHub->>Buildx: Build web image from ./dashboard
  Buildx->>GHCR: Push multi-platform web image (when not PR)
Loading
sequenceDiagram
  participant User
  participant DeployScript as deploy-ghcr.sh
  participant Compose as docker-compose
  participant API as API Service
  User->>DeployScript: Run with VERSION
  DeployScript->>Compose: Pull images
  DeployScript->>Compose: Down old stack
  DeployScript->>DeployScript: Force-kill port holders
  DeployScript->>Compose: Up new stack
  DeployScript->>API: Poll /health (60s timeout)
  API->>DeployScript: 200 OK (healthy)
  DeployScript->>User: docker compose ps and completion
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 From source builds gone, a tidy route,
GHCR stocks each image en route,
Compose pulls, health checks chime,
Ports freed, services start on time,
Rabbit hops—deploys in one fine loop.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding CI/CD workflow to publish Docker images to GHCR, which is the primary feature introduced in this changeset.
Linked Issues check ✅ Passed All coding requirements from #312 are met: multi-arch Docker images built and published to GHCR, runtime API URL configurability via entrypoint substitution, pre-built compose file and deployment script provided, and documentation added.
Out of Scope Changes check ✅ Passed All changes are directly related to the GHCR publishing objective. Updates to Dockerfiles, workflow configs, deployment scripts, and documentation are all necessary to enable the feature requested in #312.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ghcr-publish

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
deploy/DEPLOYMENT.md (1)

19-22: ⚡ Quick win

Consider adding a troubleshooting note for GHCR authentication errors.

The PR objectives mention that GHCR packages need to be manually made public post-merge. If users attempt to pull images before this is done, they'll encounter authentication errors. Consider adding a brief troubleshooting note, such as:

> **Note:** If you encounter authentication errors pulling images, verify that the packages are public at the links above. Reach out via issues if access is blocked.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/DEPLOYMENT.md` around lines 19 - 22, Add a short troubleshooting note
to DEPLOYMENT.md explaining GHCR authentication errors: update the section
listing the published images (the ghcr.io/wrzonance/wrzdj-api and
ghcr.io/wrzonance/wrzdj-web links) to include a brief note that images may be
private immediately after publish and that users seeing authentication errors
should verify the package visibility at those links (and open an issue if access
is blocked).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/docker-publish.yml:
- Around line 44-46: The tag-generation metadata blocks that currently contain
the rules "type=semver,pattern={{version}}",
"type=semver,pattern={{major}}.{{minor}}", and
"type=sha,prefix=sha-,format=short" must also include an explicit ref-based rule
so calendar-style tags (e.g., v2026.05.20) are accepted; add a rule such as a
"type=ref,event=tag,match=refs/tags/v*" (or equivalent ref/tag rule) into both
metadata blocks that include those three existing entries so tag pushes on v*
will emit tags for GHCR.

In `@dashboard/docker-entrypoint.sh`:
- Line 10: The sed replacement is inserting NEXT_PUBLIC_API_URL raw which breaks
when the value contains & or \; before running sed, create an escaped variable
(e.g., ESCAPED_NEXT_PUBLIC_API_URL) that replaces every backslash "\" with "\\"
and every ampersand "&" with "\&", then use that escaped variable in the sed
invocation targeting the "__WRZDJ_API_URL__" placeholder in docker-entrypoint.sh
so special characters are treated literally.

In `@dashboard/Dockerfile`:
- Line 1: The Dockerfile currently uses an unpinned base image reference in the
FROM instruction ("FROM node:26-alpine AS base"); update that FROM line to use
an immutable manifest digest (e.g., "node:26-alpine@sha256:<digest>") so builds
are deterministic. Lookup the correct SHA256 digest for the desired
node:26-alpine manifest from the official Node image registry (or docker
manifest inspect) and replace the tag-only reference in the Dockerfile's FROM
line (the "base" stage) with the tag@sha256 digest.

In `@deploy/deploy-ghcr.sh`:
- Around line 31-53: The current port-cleanup loop in deploy-ghcr.sh enumerates
PIDS and kills them unconditionally (see variables PORT_API/PORT_FRONTEND, PIDS,
PID, PROC), which can terminate unrelated host services; change the logic to
instead detect whether each PID is a known container-related process (e.g.,
check PROC name or cgroup for docker, containerd, docker-proxy, kube-proxy, or a
container runtime) and only kill those; if any PID is not in the allowed list,
print a clear error listing the offending PID(s)/PROC(s) and exit non‑zero
unless an explicit opt-in flag or env var (e.g., FORCE_KILL_PORTS=true or
--force-kill-ports) is set, in which case proceed with the current kill/sigkill
behavior. Ensure the messages reference the PID and PROC so operators can decide
whether to pass the force flag.
- Around line 77-82: The script currently only prints a warning when the API
never becomes healthy (ELAPSED >= 60) but still exits success; update the
health-wait block that checks ELAPSED and prints logs (referencing ELAPSED,
COMPOSE_FILE and the docker compose logs line) to exit with a non-zero status
(e.g. exit 1) after printing the warning and logs so CI/automation detects the
failure; ensure the exit happens inside the same if branch that prints "WARNING:
API did not become healthy within 60s".

In `@deploy/DEPLOYMENT.md`:
- Around line 8-17: Add a short "Prerequisites" section (Docker, docker-compose
versions, min RAM/disk, required open ports) above step 1; expand Step 1 ("Get
the deploy files") to show both options for obtaining files (release tarball or
git clone) and mention changing into the repo and copying deploy/.env.example to
deploy/.env; clarify which environment variables are required vs optional by
listing required vars (POSTGRES_PASSWORD, JWT_SECRET, TOKEN_ENCRYPTION_KEY,
HUMAN_COOKIE_SECRET, CORS_ORIGINS, PUBLIC_URL, NEXT_PUBLIC_API_URL) and noting
that third-party credentials (Spotify/Tidal/Beatport/Turnstile/Resend) are
optional for initial run with a pointer to the "Configure environment" section
for full list; add post-deployment verification steps after running
deploy/deploy-ghcr.sh: ensure execute permission (chmod +x deploy/deploy-ghcr.sh
if needed), curl the /health endpoint to confirm services are up, instructions
to create the first admin user, and note the default access URL (PUBLIC_URL) to
visit; keep references to deploy/.env.example and deploy/deploy-ghcr.sh and the
/health endpoint so reviewers can find the changes.

In `@server/Dockerfile`:
- Line 1: Replace the floating base image reference "python:3.11-slim" in the
Dockerfile with an immutable digest-pinned image; find the correct digest for
your deployment architecture (e.g., via docker inspect or Docker Hub) and update
the FROM line to use the full repo@sha256:... digest instead of the tag so
builds are reproducible and rollbacks are reliable.

---

Nitpick comments:
In `@deploy/DEPLOYMENT.md`:
- Around line 19-22: Add a short troubleshooting note to DEPLOYMENT.md
explaining GHCR authentication errors: update the section listing the published
images (the ghcr.io/wrzonance/wrzdj-api and ghcr.io/wrzonance/wrzdj-web links)
to include a brief note that images may be private immediately after publish and
that users seeing authentication errors should verify the package visibility at
those links (and open an issue if access is blocked).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f428f60-b6a7-472c-b2ce-07dee86299b0

📥 Commits

Reviewing files that changed from the base of the PR and between 4f6f133 and f25daa0.

📒 Files selected for processing (10)
  • .github/workflows/ci.yml
  • .github/workflows/docker-publish.yml
  • .github/workflows/release.yml
  • dashboard/Dockerfile
  • dashboard/docker-entrypoint.sh
  • deploy/.env.example
  • deploy/DEPLOYMENT.md
  • deploy/deploy-ghcr.sh
  • deploy/docker-compose.ghcr.yml
  • server/Dockerfile
💤 Files with no reviewable changes (1)
  • .github/workflows/ci.yml

Comment thread .github/workflows/docker-publish.yml
Comment thread dashboard/docker-entrypoint.sh Outdated
Comment thread dashboard/Dockerfile
Comment thread deploy/deploy-ghcr.sh
Comment thread deploy/deploy-ghcr.sh
Comment thread deploy/DEPLOYMENT.md Outdated
Comment thread server/Dockerfile
…ck exit, docs

- docker-publish.yml: add `type=ref,event=tag` so calendar tags like
  v2026.05.20.2 (non-semver same-day) still emit a version tag, not just sha-*.
- docker-entrypoint.sh: escape sed replacement metacharacters (\ and &) in
  NEXT_PUBLIC_API_URL before substitution. & means "matched text" in sed
  replacement and would otherwise corrupt URLs containing query separators.
- deploy-ghcr.sh: exit 1 when the API healthcheck never becomes ready instead
  of printing a warning and returning 0 — automation can otherwise mark
  failed rollouts as successful.
- DEPLOYMENT.md: expand Quick Start with prerequisites (Docker version, RAM,
  ports), concrete file-acquisition steps (clone vs release tarball),
  required-vs-optional env var split, post-deploy verification, and a
  troubleshooting note for the GHCR private-by-default first-pull 401.

Declined (resolved with rationale in PR threads):
- Pinning base images to single-arch SHA digests breaks multi-arch builds
  by design (the digest of a multi-arch manifest differs from per-arch
  digests). Mitigation: Dependabot tracks the tags.
- Port-kill behavior in deploy-ghcr.sh mirrors deploy.sh (project
  convention); scope is limited to PORT_API/PORT_FRONTEND.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
dashboard/docker-entrypoint.sh (1)

13-13: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Complete the sed escaping by adding backslash to the character class.

The character class [\&] only escapes ampersand. Line 10's comment states "backslash and ampersand" must be escaped, and the past review comment proposed the fix sed 's/[&\]/\\&/g', but line 13 is still missing the backslash. If NEXT_PUBLIC_API_URL contains \, sed will misinterpret it as an escape character, corrupting the replacement or causing sed to fail.

🔧 Proposed fix
-  ESCAPED_URL=$(printf '%s' "${NEXT_PUBLIC_API_URL}" | sed -e 's/[\&]/\\&/g')
+  ESCAPED_URL=$(printf '%s' "${NEXT_PUBLIC_API_URL}" | sed -e 's/[&\\]/\\&/g')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/docker-entrypoint.sh` at line 13, The sed character class in the
ESCAPED_URL assignment only escapes ampersand and must also escape backslash;
update the sed expression used when building ESCAPED_URL from
NEXT_PUBLIC_API_URL so the character class includes both backslash and ampersand
(i.e., escape the backslash inside the bracket expression) so sed treats literal
"\" correctly when replacing; locate the ESCAPED_URL assignment and modify the
sed pattern accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@dashboard/docker-entrypoint.sh`:
- Line 13: The sed character class in the ESCAPED_URL assignment only escapes
ampersand and must also escape backslash; update the sed expression used when
building ESCAPED_URL from NEXT_PUBLIC_API_URL so the character class includes
both backslash and ampersand (i.e., escape the backslash inside the bracket
expression) so sed treats literal "\" correctly when replacing; locate the
ESCAPED_URL assignment and modify the sed pattern accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b5ba7ad3-79eb-4774-abe3-0e9c6ffc1e5f

📥 Commits

Reviewing files that changed from the base of the PR and between f25daa0 and e41d0f6.

📒 Files selected for processing (4)
  • .github/workflows/docker-publish.yml
  • dashboard/docker-entrypoint.sh
  • deploy/DEPLOYMENT.md
  • deploy/deploy-ghcr.sh
✅ Files skipped from review due to trivial changes (1)
  • deploy/DEPLOYMENT.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/docker-publish.yml
  • deploy/deploy-ghcr.sh

CodeRabbit pointed out the previous escape pattern [\&] was incomplete:
inside a BRE character class, the bracketed \ is literal but some sed
implementations don't include backslash in [\&] reliably. Switch to the
canonical [&\\] form so both ampersand and backslash are guaranteed
to be escaped before passing the URL to the outer sed substitution.

Verified locally with URLs containing both \ and &.
@thewrz thewrz merged commit 705c92d into main May 17, 2026
14 checks passed
@thewrz thewrz deleted the feat/ghcr-publish branch May 17, 2026 02:16
thewrz added a commit that referenced this pull request Jun 3, 2026
* docs: refresh README + CONTRIB for features landed on main since #304

Document main-branch features merged after the last README content
update (#304, 2026-05-11):

- Frictionless join (#369/#380): nickname gate is now conditional;
  auto-named guests with rename affordance; per-event toggle + per-DJ
  account default
- Pre-built GHCR images + deploy-ghcr.sh no-build deploy path (#318)
- Split collection/live event codes; public endpoints never expose the
  internal event id (#324/#382)
- Broaden supply-chain note: GitHub Actions pinned to commit SHAs,
  committed uv.lock at CVE-floor versions (#322/#323)
- Add LISTENBRAINZ_USER_TOKEN to the env block

CONTRIB drift fixes: Node 20 -> 22, coverage 70% -> 85%, add 10 missing
live env vars, complete the conftest fixture list, fix the BRIDGE_API_KEY
description, add the dual-code resolver pitfall.

Deliberately excludes the provider-agnostic LLM gateway epic, which lives
on epic/ai-engine and is not yet merged to main.

* docs: correct supply-chain claim — only bridge image pins base SHA

CodeRabbit flagged the 'pinned base-image SHAs' claim as overbroad.
Verified: server/Dockerfile (python:3.11-slim) and dashboard/Dockerfile
(node:26-alpine) use floating tags for multi-arch; only bridge/Dockerfile
pins a SHA256 digest. Reword to the actually-true mitigations: Actions
SHA-pinning, committed lockfiles, CI scans (bandit/pip-audit/npm audit),
and the bridge base-image digest pin.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

For convenience; build ghcr images

2 participants