Skip to content

Build and publish the container image on push to main - #4

Merged
theNEXlevel merged 4 commits into
mainfrom
ci/publish-container
Aug 8, 2026
Merged

Build and publish the container image on push to main#4
theNEXlevel merged 4 commits into
mainfrom
ci/publish-container

Conversation

@theNEXlevel

Copy link
Copy Markdown
Contributor

Summary

Same setup as C4G/template#8: move the production image build off the shared Coolify host and into GitHub Actions.

  • .github/workflows/publish.yaml (new) — on push to main / workflow_dispatch: builds the image, pushes ghcr.io/c4g/va-stats:latest and :<sha>, then triggers a Coolify deploy of va-stats-test.
  • docker-compose.yml — references the published image, no build: key. That absence is what stops Coolify from compiling on the server; a deploy becomes a pull-and-restart.
  • docker-compose.build.yml (new) — restores the build: key for local builds.
  • .github/workflows/ci.yaml — PR runs overlay the build file so they still verify an image built from the branch.
  • Dockerfile — stops baking in NEXT_PUBLIC_BASE_URL (see below).
  • README.md — documents the deploy flow, environments, and promotion. Replaces the stale "Deploy on Vercel" section.

One image, two origins

NEXT_PUBLIC_BASE_URL differs per environment (https://va-stats.c4g.dev vs https://va-stats-test.c4g.dev) and was passed as a build arg, so a single published image could not have served both.

The Dockerfile now leaves it unset at build time. Next.js only substitutes NEXT_PUBLIC_* variables that are present in the environment during the build (next/dist/build/webpack/plugins/define-env-plugin.jsgetNextPublicEnvironmentVariables), so with it unset the expression survives as a real runtime lookup and each environment supplies its own value through Coolify — which docker-compose.yml already passed at runtime.

Verified empirically: built the app with the variable unset and inspected the compiled server output.

$ grep -o "process\.env\.NEXT_PUBLIC_BASE_URL" .next/server/pages/api/updateusers.js
process.env.NEXT_PUBLIC_BASE_URL

The expression is intact rather than replaced with a literal, so no Docker entrypoint or code change is needed. This holds because the value is read server-side onlyutils/auditLogger.js:3, imported solely by pages/api/*, and guarded by typeof window === "undefined". The Dockerfile carries a comment recording that constraint, since reading it from client code would return undefined in the browser.

Environments

Environment Coolify app Image tag
va-stats-test.c4g.dev va-stats-test (v1084st0j09gwuxnapqkr9xk) latest, deployed automatically on push to main
va-stats.c4g.dev va-stats (gwggx3pjadr1unl07bo471dk) IMAGE_TAG pinned to a commit SHA, deployed manually

Production promotion is explicit: set IMAGE_TAG to a SHA already verified on va-stats-test, then redeploy. Production runs the exact image that was tested rather than a rebuild.

Already applied outside this PR

  • Coolify git auto-deploy disabled on va-stats-test (it was on). Otherwise Coolify's webhook would race the workflow and pull the previous :latest. The workflow's API call is now the single trigger. Production already had auto-deploy off.
  • COOLIFY_APP_UUID repository variable set to v1084st0j09gwuxnapqkr9xk (va-stats-test).

Follow-up required after the first run

The new GHCR package is created private. The Coolify host pulls anonymously (no ~/.docker/config.json on the server), so ghcr.io/c4g/va-stats must be set to public — matching the existing ghcr.io/c4g/metro-atlanta-saves-* and ghcr.io/c4g/va-dat packages — or the first deploy will fail to pull.

🤖 Generated with Claude Code

Justin McLellan and others added 2 commits August 7, 2026 22:37
Coolify compiles the application on the shared host on every deploy, which ties
up CPU and disk for the other applications on the box. Move the build to GitHub
Actions: publish.yaml builds the image, pushes it to GHCR, and then calls the
Coolify deploy API for va-stats-test.

docker-compose.yml now references the published image and has no `build:` key —
that absence is what keeps the build off the server. The build key moves to
docker-compose.build.yml, which CI overlays so PR runs still verify an image
built from the branch rather than the published :latest.

One image has to serve both va-stats-test and production, which have different
origins, so the Dockerfile stops baking NEXT_PUBLIC_BASE_URL in. Next.js only
substitutes NEXT_PUBLIC_* variables that are present at build time; leaving it
unset keeps process.env.NEXT_PUBLIC_BASE_URL in the compiled server output as a
real runtime lookup, and compose already passes the value at runtime. This is
safe only because the variable is read server-side (utils/auditLogger.js, used
solely by pages/api/*), which the Dockerfile comment records.

Production is promoted by pinning IMAGE_TAG to a tested commit SHA rather than
tracking :latest, so it runs the exact image verified on va-stats-test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cut the explanatory blocks down to the load-bearing line or two. The reasoning
they carried lives in README.md and the PR discussion.
Justin McLellan and others added 2 commits August 8, 2026 00:17
CI builds the PR head; with squash merges the commit that lands on main is
never exercised before its image is deployed. Run the production compose
against the just-pushed tag with no build overlay, so the step also proves the
image pulls and runs without anything being built.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The repo's format check covers .github; lint-staged does not, so the workflow
file slipped through the commit hook.
@theNEXlevel
theNEXlevel merged commit f5be1b6 into main Aug 8, 2026
1 check passed
@theNEXlevel
theNEXlevel deleted the ci/publish-container branch August 8, 2026 05:37
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.

1 participant