Create artifacts easily on the GitHub Container Registry
    DevOps

    Create artifacts easily on the GitHub Container Registry

    Setting a backend up in Docker and publishing the image to GitHub Container Registry lets you run the exact same code on your laptop, CI, or production server.

    Reverse PolaritySeptember 13, 20245 min read

    Setting a backend up in Docker (and publishing the image to GitHub Container Registry) lets you run the exact same code on your laptop, CI, or production server. Here’s a simple guide to get you started, by making a simple harness that you can reuse down the road, in your projects.


    1 Structure of the backend

    packages/
    └─ backend/
       ├─ src/
       │   ├─ server.ts
       │   ├─ app.ts
       │   └─ …
       ├─ tsconfig.json
       ├─ package.json
       └─ .env.example
    

    server.ts listens on 8081 and expects env variables to connect to other services. In this example, we connect to Mongo, Stripe, and Clerk later on.


    2 Dockerfile

    # ---------- Build stage ----------
    FROM node:22-bookworm AS builder
    
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --omit=dev
    COPY tsconfig*.json ./
    COPY src ./src
    RUN npx tsc --build
    
    # ---------- Runtime stage ----------
    FROM node:22-slim
    
    ENV NODE_ENV=production
    WORKDIR /app
    COPY --from=builder /app/package*.json ./
    RUN npm ci --omit=dev
    COPY --from=builder /app/dist ./dist
    
    EXPOSE 8081
    CMD ["node", "dist/server.js"]
    

    Add a .dockerignore:

    .git
    node_modules
    **/*.ts
    .env*
    

    3 Build and run locally

    docker build -t my-backend:dev -f Dockerfile .
    
    docker run --rm -p 8081:8081 \
      -e MONGO_URL=mongodb://host.docker.internal:27017 \
      -e MONGO_DB_NAME=diagrams \
      -e STRIPE_SECRET_KEY=sk_test_xxx \
      my-backend:dev
    

    Open http://localhost:8081/api/diagrams to hit the routes from diagrams.routes.ts.


    4 Tag for GitHub Container Registry

    ORG=my-github-username
    IMAGE=diagram-backend
    VERSION=0.1.0
    
    docker tag my-backend:dev ghcr.io/$ORG/$IMAGE:$VERSION
    

    5 Login to GHCR

    1. Create a Personal Access Token with write:packages.
    2. Export it: export CR_PAT=ghp_xxx.
    3. Login:
    echo $CR_PAT | docker login ghcr.io -u $ORG --password-stdin
    docker push ghcr.io/$ORG/$IMAGE:$VERSION
    

    The package now shows under Packages on GitHub.


    6 Automate with GitHub Actions

    .github/workflows/docker.yml

    name: Build & publish backend image
    on:
      push:
        paths:
          - 'packages/backend/**'
          - 'Dockerfile'
          - '.github/workflows/docker.yml'
        branches: [main]
    
    env:
      IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/diagram-backend
    
    jobs:
      build:
        runs-on: ubuntu-latest
        permissions:
          contents: read
          packages: write
        steps:
          - uses: actions/checkout@v4
    
          - uses: docker/setup-buildx-action@v3
    
          - uses: docker/login-action@v3
            with:
              registry: ghcr.io
              username: ${{ github.repository_owner }}
              password: ${{ secrets.GITHUB_TOKEN }}
    
          - uses: docker/build-push-action@v5
            with:
              context: .
              file: Dockerfile
              push: true
              tags: |
                ${{ env.IMAGE_NAME }}:latest
                ${{ env.IMAGE_NAME }}:${{ github.sha }}
    

    GITHUB_TOKEN is provided by GitHub—no extra secrets needed.


    7 Run in production

    docker run -d --name diagrams-backend \
      -p 80:8081 \
      -e MONGO_URL=$MONGO_URL \
      -e MONGO_DB_NAME=diagrams \
      ghcr.io/$ORG/diagram-backend:latest
    

    Any host (or platform like Fly.io, Render, DigitalOcean) can pull the image; supply a read-only token if required.


    8 Clean up old images

    Delete unused digests manually or schedule a job:

    gh api --method DELETE \
      /user/packages/container/diagram-backend/versions/<digest_id>
    

    Wrap-up

    • Single Dockerfile packages the compiled backend with only production deps.
    • Local parity—run the same container everywhere.
    • GitHub Action builds and pushes on every main commit.

    With the container published, you have the perfect base for the Express article: clone, pull, run, done.

    More Articles