feat(ci): publish Docker images to GHCR for self-hosters#318
Conversation
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughAdds 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. ChangesPre-built Image Publishing and Deployment
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)
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
deploy/DEPLOYMENT.md (1)
19-22: ⚡ Quick winConsider 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
📒 Files selected for processing (10)
.github/workflows/ci.yml.github/workflows/docker-publish.yml.github/workflows/release.ymldashboard/Dockerfiledashboard/docker-entrypoint.shdeploy/.env.exampledeploy/DEPLOYMENT.mddeploy/deploy-ghcr.shdeploy/docker-compose.ghcr.ymlserver/Dockerfile
💤 Files with no reviewable changes (1)
- .github/workflows/ci.yml
…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.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
dashboard/docker-entrypoint.sh (1)
13-13:⚠️ Potential issue | 🟠 Major | ⚡ Quick winComplete 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 fixsed 's/[&\]/\\&/g', but line 13 is still missing the backslash. IfNEXT_PUBLIC_API_URLcontains\, 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
📒 Files selected for processing (4)
.github/workflows/docker-publish.ymldashboard/docker-entrypoint.shdeploy/DEPLOYMENT.mddeploy/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 &.
* 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.
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
mainand on everyv*tag. Community users can now run WrzDJ without building the images manually.Images published:
ghcr.io/wrzonance/wrzdj-api— FastAPI backendghcr.io/wrzonance/wrzdj-web— Next.js frontendTags 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 usersdeploy/deploy-ghcr.sh— helper script (pull + restart + healthcheck)dashboard/docker-entrypoint.sh— runtime substitution of the build-timeNEXT_PUBLIC_API_URLplaceholderdeploy/DEPLOYMENT.md— Quick Start section for pre-built imagesWhy the Entrypoint Sed Pass
NEXT_PUBLIC_API_URLis baked into the Next.js bundle at build time — in JS chunks AND inroutes-manifest.json(where the CSPconnect-srcheader 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 runtimeNEXT_PUBLIC_API_URLenv var across both.jsand.jsonfiles in/app/.next/before starting the server.Operators set
NEXT_PUBLIC_API_URLin their.env; the existingdeploy/docker-compose.ymlbuild-from-source path is unchanged.Test Plan
Verified locally end-to-end:
--build-arg NEXT_PUBLIC_API_URL=__WRZDJ_API_URL__routes-manifest.json(the CSP gatekeeper)deploy/docker-compose.ghcr.yml: db + api + web all healthyNEXT_PUBLIC_API_URL(not the placeholder)docker compose -f deploy/docker-compose.ghcr.yml configvalidates cleanlydocker-buildjob) removed fromci.yml— superseded bypull_requestbuilds in this workflowCI 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:
mainlands, checkhttps://github.com/wrzonance?tab=packageswrzdj-api,wrzdj-web) → Package settings → Change visibility → PublicSummary by CodeRabbit
New Features
Documentation
Chores