Build and publish the container image on push to main - #4
Merged
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 tomain/workflow_dispatch: builds the image, pushesghcr.io/c4g/va-stats:latestand:<sha>, then triggers a Coolify deploy of va-stats-test.docker-compose.yml— references the published image, nobuild: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 thebuild: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 inNEXT_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_URLdiffers per environment (https://va-stats.c4g.devvshttps://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.js→getNextPublicEnvironmentVariables), so with it unset the expression survives as a real runtime lookup and each environment supplies its own value through Coolify — whichdocker-compose.ymlalready passed at runtime.Verified empirically: built the app with the variable unset and inspected the compiled server output.
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 only —
utils/auditLogger.js:3, imported solely bypages/api/*, and guarded bytypeof window === "undefined". The Dockerfile carries a comment recording that constraint, since reading it from client code would returnundefinedin the browser.Environments
va-stats-test.c4g.devva-stats-test(v1084st0j09gwuxnapqkr9xk)latest, deployed automatically on push to mainva-stats.c4g.devva-stats(gwggx3pjadr1unl07bo471dk)IMAGE_TAGpinned to a commit SHA, deployed manuallyProduction promotion is explicit: set
IMAGE_TAGto 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
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_UUIDrepository variable set tov1084st0j09gwuxnapqkr9xk(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.jsonon the server), soghcr.io/c4g/va-statsmust be set to public — matching the existingghcr.io/c4g/metro-atlanta-saves-*andghcr.io/c4g/va-datpackages — or the first deploy will fail to pull.🤖 Generated with Claude Code