Skip to content

Add a production image build and publish step #1

Description

@bmorrisey

The gap

make build-stack gets you a working stack on a laptop. There is no equivalent
for a server, and building on the server can impact hosted services.

Both Dockerfiles are built for the bind mounts in docker-compose.yml.
backend/Dockerfile copies requirements.txt and nothing else;
frontend/Dockerfile copies package.json and nothing else. The application
code arrives at runtime from ./backend:/app and ./frontend:/app. So the
images contain dependencies and no source. They cannot run anywhere the source
tree is not mounted.

The startup commands are also local-only. The backend runs
uvicorn --reload, which watches the filesystem. The frontend runs
npm run dev, which is the Vite dev server rather than the output of
npm run build.

What could be added

Two production Dockerfiles and one workflow, sitting beside the dev ones. The
dev files stay untouched, so make stack and hot reload keep working.

backend/Dockerfile.prod is the current file with COPY . . added and
--reload dropped. Migrations still run on start:

CMD ["sh", "-c", "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000"]

frontend/Dockerfile.prod is a two-stage build: npm run build in the node
stage, then copy dist/ into an nginx image that serves the static files and
proxies /api/ to the backend. That folds the proxy service into the
frontend image and drops a container from the production stack. The routing
rules already exist in proxy/nginx.conf; they need the dev-server proxying
and the HMR WebSocket headers removed.

Then a workflow that publishes both on a tag:

name: release
on:
  push:
    tags: ["v*"]
jobs:
  build:
    runs-on: ubuntu-latest
    permissions: { contents: read, packages: write }
    strategy:
      matrix:
        include:
          - { name: api, context: ./backend,  dockerfile: ./backend/Dockerfile.prod }
          - { name: web, context: ./frontend, dockerfile: ./frontend/Dockerfile.prod }
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: ${{ matrix.context }}
          file: ${{ matrix.dockerfile }}
          push: true
          tags: ghcr.io/${{ github.repository }}-${{ matrix.name }}:${{ github.ref_name }}

Tagging v1.2.3 then produces two images at the same tag. The shared tag is
worth keeping: deploy tooling usually takes one tag and applies it across every
service in a release, and separately versioned images make that impossible.

A docker-compose.prod.yml reading image: ${API_IMAGE} and
image: ${WEB_IMAGE} would round it out, but the images are the part that
cannot be worked around downstream.

Three defaults that block a deploy

Separate from the build, and cheap to fix:

The db healthcheck is hardcoded to the example credentials:

test: ["CMD-SHELL", "pg_isready -U user -d deltav_db"]

It ignores POSTGRES_USER and POSTGRES_DB. Change either one in .env, as
you must for a real deployment, and the check never passes. backend waits on
condition: service_healthy, so the stack hangs with no obvious cause.
pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB} fixes it and behaves the
same locally.

backend/auth.py:45 falls back to a literal when JWT_SECRET is unset:

SECRET_KEY = os.environ.get("JWT_SECRET", "dev-secret-change-me")

A deployment that forgets the variable signs tokens with a value published in
this repository, and nothing in the logs says so. Reading os.environ["JWT_SECRET"]
turns a silent auth bypass into a startup error. If the fallback is worth
keeping for the first make stack, gating it on an explicit dev flag keeps the
convenience without the failure mode.

The db service publishes 5432:5432. Useful for pgAdmin on a laptop, and it
exposes Postgres to the network on a server. A comment on that line saying to
remove it in production would be enough.

Context

I hit this wiring the boilerplate up to an ops toolkit that deploys by pulling
prebuilt images. The stack itself needed no changes, only the images. Happy to
open a PR with the two Dockerfiles, the nginx config, and the workflow if the
approach looks right.

Version this is against:
8cc51e3.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions