Skip to main content
Navigation
HomeTechnical ReferenceJournalGitHubGitHub
Sidebar — toggle document categories via the logo
Categories

Docker Project Examples

Overview

Real-world Docker setups go beyond single-service containers. This reference provides production-ready Dockerfile examples for common tech stacks, multi-service Docker Compose patterns, and project layout conventions.

Project structure conventions

Monorepo with multiple services

The most common layout is a monorepo where each service lives in its own directory with its own Dockerfile, plus Compose files at the root that wire everything together. Keeping per-service files colocated with their source makes builds self-contained and easy to reason about.

project/
├── docker-compose.yml
├── docker-compose.prod.yml
├── .env
├── api/ # Backend API
│ ├── Dockerfile
│ ├── Dockerfile.dev
│ └── src/
├── www/ # Frontend web app
│ ├── Dockerfile
│ ├── Dockerfile.dev
│ └── src/
├── scripts/ # Utility scripts
│ └── entrypoint.sh
└── docs/

Per-service Dockerfiles

Use separate Dockerfile and Dockerfile.dev when development requires different tools (hot-reload, debuggers).

Language-specific Dockerfiles

Node.js / Express (TypeScript)

This two-stage Dockerfile builds a TypeScript Express app and ships a minimal production image: it compiles to dist in the builder stage, then copies only the compiled output, node_modules, and a non-root runtime user. tini is used as a lightweight init process so signals like SIGTERM reach the Node process correctly.

# ---- Build Stage ----
FROM node:22.12.0-alpine AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build

# ---- Production Stage ----
FROM node:22.12.0-alpine
WORKDIR /app

# Create a non-root user; tini reaps signals and zombie processes
RUN addgroup -S app && adduser -S app -G app
RUN apk add --no-cache tini curl

# Ship only the compiled output and production dependencies
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -sf http://localhost:3000/health || exit 1

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/server.js"]

Dev Dockerfile:

FROM node:22.12.0-alpine
WORKDIR /app
RUN apk add --no-cache tini

COPY package*.json ./
RUN npm ci
COPY . .

EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["npx", "tsx", "watch", "src/server.ts"]

Next.js

This Dockerfile builds Next.js with the standalone output — the smallest deployable target Next.js can emit — so the production stage copies only public, the standalone server, and static assets. NEXT_PUBLIC_* values are baked in at build time via ARG, which means they are fixed per build and must be set when building.

# ---- Build Stage ----
FROM node:22.12.0-alpine AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

RUN npm run build

# ---- Production Stage ----
FROM node:22.12.0-alpine
WORKDIR /app

# Create a non-root user; tini reaps signals and zombie processes
RUN addgroup -S app && adduser -S app -G app
RUN apk add --no-cache tini curl

# Copy only the standalone server, public assets, and static files
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s CMD curl -sf http://localhost:3000 || exit 1

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

Python / FastAPI

This Dockerfile installs the FastAPI dependencies in a builder stage, then copies the installed site-packages and binaries into a clean runtime image — no source code or build tools in the final layer. It runs as a non-root user and launches uvicorn with the app entrypoint from src/main.py.

# ---- Build Stage ----
FROM python:3.12-slim AS builder
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ---- Production Stage ----
FROM python:3.12-slim
WORKDIR /app

RUN groupadd -r app && useradd -r -g app app
RUN apt-get update && apt-get install -y --no-install-recommends \
curl && rm -rf /var/lib/apt/lists/*

# Copy installed packages and binaries, skipping the heavy build image
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY src/ ./src/

USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -sf http://localhost:8000/health || exit 1

CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

Rust / Axum

This Dockerfile compiles an Axum server in a Rust builder stage and runs the resulting binary on plain Debian slim — the smallest image that still ships a working glibc binary. The dummy src/main.rs trick warms the dependency cache so the final build only recompiles your code, and ca-certificates are installed for HTTPS connections.

# ---- Build Stage ----
FROM rust:1.80-slim-bookworm AS builder
WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*

# Warm the dependency cache before copying real source
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src

# Copy the real source; only our code recompiles now
COPY src/ ./src/
RUN cargo build --release

# ---- Production Stage ----
FROM debian:bookworm-slim
WORKDIR /app

RUN groupadd -r app && useradd -r -g app app
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/target/release/server /app/server

USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -sf http://localhost:3000/health || exit 1

CMD ["/app/server"]

Multi-service Compose examples

Full-stack web application

This Compose stack ties together everything from the language examples: a Next.js frontend behind an nginx reverse proxy, a FastAPI backend, a background worker, plus PostgreSQL and Redis. The frontend only talks to the API over the internal frontend network, the API and worker share the backend network with the data stores, and every service waits for its dependencies via depends_on health conditions.

services:
www:
build:
context: ./www
dockerfile: Dockerfile
args:
NEXT_PUBLIC_API_URL: https://api.example.com
ports:
- "3000:3000"
restart: unless-stopped
depends_on:
api:
condition: service_healthy
networks:
- frontend

api:
build:
context: ./api
dockerfile: Dockerfile
expose:
- "4000"
environment:
DATABASE_URL: postgresql://app:${DB_PASSWORD}@db:5432/appdb
REDIS_URL: redis://cache:6379
restart: unless-stopped
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
networks:
- frontend
- backend
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:4000/health"]
interval: 15s
timeout: 5s
retries: 3

worker:
build:
context: ./api
dockerfile: Dockerfile
command: ["python", "-m", "src.worker"]
environment:
DATABASE_URL: postgresql://app:${DB_PASSWORD}@db:5432/appdb
REDIS_URL: redis://cache:6379
restart: unless-stopped
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
networks:
- backend

db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: appdb
restart: unless-stopped
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 5s
retries: 5

cache:
image: redis:7-alpine
volumes:
- redisdata:/data
restart: unless-stopped
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5

nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
restart: unless-stopped
depends_on:
- www
- api
networks:
- frontend

networks:
frontend:
backend:

volumes:
pgdata:
redisdata:

Development environment with hot-reload

For development, bind-mount the source directories into the containers so edits on the host trigger hot-reload inside them, and use the Dockerfile.dev images that run watch tooling. Both services are gated behind the dev profile so a plain up stays production-shaped while --profile dev enables the live-reload stack.

services:
www:
build:
context: ./www
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- ./www/src:/app/src:ro # hot-reload source
environment:
API_URL: http://api:4000
depends_on:
- api
profiles: ["dev"]

api:
build:
context: ./api
dockerfile: Dockerfile.dev
ports:
- "4000:4000"
volumes:
- ./api/src:/app/src:ro # hot-reload source
environment:
DATABASE_URL: postgresql://app:devpass@db:5432/appdb_dev
depends_on:
db:
condition: service_healthy
profiles: ["dev"]

db:
image: postgres:16-alpine
ports:
- "5432:5432"
volumes:
- dev_pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: devpass
POSTGRES_DB: appdb_dev
networks:
- default

volumes:
dev_pgdata:
docker compose --profile dev up -d

Docker entrypoint scripts

An entrypoint script is the standard place to perform runtime setup that should not be baked into the image: waiting for a database to accept connections, running migrations, or generating configuration. The script ends by forwarding the container's arguments to the application with exec, which replaces the shell process so signals reach the app directly.

#!/bin/sh
# entrypoint.sh — wait for dependencies, then exec the application

set -e

# Wait for database
if [ -n "$DATABASE_URL" ]; then
echo "Waiting for database..."
until nc -z db 5432; do
sleep 0.5
done
echo "Database is ready."
fi

# Run migrations
if [ "${RUN_MIGRATIONS}" = "true" ]; then
echo "Running database migrations..."
npm run migrate
fi

# Execute the command
exec "$@"

Wire the script into the image and make it the container's entrypoint:

COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["node", "server.js"]

.dockerignore

A .dockerignore file lists the paths excluded from the build context — the set of files sent to the Docker daemon when building. Excluding node_modules, build output, .git, and especially .env keeps builds fast, image layers small, and secrets out of the image entirely.

# Dependencies
node_modules/
__pycache__/
*.pyc

# Build outputs
dist/
build/
.next/

# Version control
.git/
.gitignore

# Environment files
.env
.env.local
.env.*.local

# Documentation
*.md
docs/

# IDE
.idea/
.vscode/
*.swp

# OS
.DS_Store
Thumbs.db

# Tests
**/*.test.*
**/*.spec.*
tests/
__tests__/

# Logs
*.log
npm-debug.log*

See also