Containerising with Docker

Build a Minimal Koa Image with a Multi-Stage Dockerfile and a Healthcheck

Containerising with Docker

Write a multi-stage Dockerfile for a Koa ESM app that produces a small, secure image — production dependencies only, non-root user, and a built-in HEALTHCHECK instruction.

4 min read Level 3/5 #koa#production#docker
What you'll learn
  • Write a two-stage Dockerfile that separates build from runtime
  • Run the Koa process as a non-root user inside the container
  • Add a HEALTHCHECK that polls the /health/live endpoint

Docker lets you ship your Koa app as a self-contained image that runs identically in development, staging, and production. A multi-stage build keeps the final image small by discarding everything not needed at runtime.

Project Structure Assumptions

my-koa-app/
├── src/
│   ├── app.js
│   └── server.js
├── package.json
├── package-lock.json
└── Dockerfile

The app uses ES modules ("type": "module" in package.json) and has no compile step — if you use TypeScript, add a tsc build stage.

The Dockerfile

# ── Stage 1: install dependencies ────────────────────────────────────────────
FROM node:20-alpine AS deps
WORKDIR /app

COPY package.json package-lock.json ./
# Install only production dependencies
RUN npm ci --omit=dev

# ── Stage 2: runtime image ────────────────────────────────────────────────────
FROM node:20-alpine AS runtime
WORKDIR /app

# Create a non-root user and group
RUN addgroup -S koa && adduser -S koa -G koa

# Copy production node_modules from deps stage
COPY --from=deps /app/node_modules ./node_modules

# Copy application source
COPY src/ ./src/
COPY package.json ./

# Own files as non-root user
RUN chown -R koa:koa /app
USER koa

ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:3000/health/live || exit 1

CMD ["node", "src/server.js"]

Why Multi-Stage?

The first stage (deps) installs everything including devDependencies. The second stage (runtime) copies only the pruned node_modules from deps, leaving test runners, build tools, and type definitions out of the final image.

Image typeTypical size
node:20 (full Debian)~1 GB
node:20-alpine single-stage~200 MB
Multi-stage alpine (prod-only)~80–120 MB

Building and Running

# Build
docker build -t my-koa-app:latest .

# Run locally
docker run -p 3000:3000 --env DATABASE_URL=postgres://... my-koa-app:latest

# Check the healthcheck status
docker inspect --format='{{.State.Health.Status}}' <container-id>

.dockerignore

Exclude files that should never enter the build context:

node_modules
.env
.env.*
*.test.js
coverage/
.git

This keeps the build context small and prevents accidental secret leakage into the image layer.

Up Next

Explore the wider Koa ecosystem and decide when Koa is (and is not) the right tool.

Going Further with Koa →