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
- Create a Personal Access Token with write:packages.
- Export it:
export CR_PAT=ghp_xxx. - 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
maincommit.
With the container published, you have the perfect base for the Express article: clone, pull, run, done.
