Docker Basics
10 examples to containerize a TypeScript Node.js API on Node 24 LTS - 7 basic and 3 intermediate.
Search across all documentation pages
10 examples to containerize a TypeScript Node.js API on Node 24 LTS - 7 basic and 3 intermediate.
mkdir node-api && cd node-api
npm init -y
npm pkg set type=module
npm install express@5
npm install -D typescript@5.6 tsx @types/express @types/nodeFor multi-stage patterns and image hardening, see Multi-Stage Builds and Non-Root Containers.
FROM node:24-bookworm-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist ./dist
ENV NODE_ENV=production
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]npm ci uses the lockfile for reproducible installspackage*.json before source so dependency layers cache wellUSER node runs the process without root privileges.dockerignorenode_modules
dist
.git
.env
.env.*
*.md
coverage
.vscode
Dockerfile*
docker-compose*.yml
docker build.env into an image layerFROM node:24-bookworm-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM node:24-bookworm-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/main.js"]docker builddist/ and production node_modulesPORT from the Environmentimport express from "express";
const app = express();
const port = Number(process.env.PORT ?? 3000);
app.get("/health", (_req, res) => {
res.json({ status: "ok" });
});
app.listen(port, "0.0.0.0", () => {
console.log(`listening on ${port}`);
});0.0.0.0 inside containers, not 127.0.0.1PORTdocker build and docker rundocker build -t my-api:local .
docker run --rm -p 3000:3000 -e PORT=3000 my-api:local
curl http://localhost:3000/health-p 3000:3000 maps host port to container port--rm removes the container after exit (good for local smoke tests)-e locally; use a secrets manager in productionpackage.json Scripts for Docker{
"scripts": {
"build": "tsc",
"start": "node dist/main.js",
"docker:build": "docker build -t my-api:local .",
"docker:run": "docker run --rm -p 3000:3000 -e PORT=3000 my-api:local"
}
}npm run docker:build after tests passstart as plain node, not tsx, in production imagesHEALTHCHECK in DockerfileHEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT||3000) + '/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"HEALTHCHECK/health fast: no database calls (use /ready for that)FROM node:24-bookworm-slim AS base
WORKDIR /app
COPY package*.json ./
FROM base AS dev
RUN npm ci
COPY . .
CMD ["npx", "tsx", "watch", "src/main.ts"]
FROM base AS prod
RUN npm ci --omit=dev
COPY dist ./dist
USER node
CMD ["node", "dist/main.js"]docker build --target dev -t my-api:dev .
docker build --target prod -t my-api:prod .tsx watch) and production--target selects the final stagedocker compose for Local Stack# compose.yml
services:
api:
build: .
ports:
- "3000:3000"
environment:
NODE_ENV: development
PORT: 3000
DATABASE_URL: postgres://postgres:postgres@db:5432/app
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5depends_on with condition: service_healthy avoids race on bootFROM node:24-bookworm-slim@sha256:abc123def456...Default to node:24-bookworm-slim (glibc). Alpine (musl) breaks some native npm modules. See Distroless & Alpine Trade-offs.
Yes for debugging image issues. CI builds images; developers still benefit from docker build and compose for integration tests.
Either in a Docker build stage or in CI before docker build. Do not ship tsx or typescript in production images unless you have a strong reason.
A slim Express API is often 150-250 MB. If yours is 800 MB+, audit layers with Image Slimming.
One process per container. Run API and background workers as separate Deployments so you can scale and deploy them independently.
Environment variables from orchestrator secrets (K8s Secrets, ECS task secrets, SSM). Never bake secrets into image layers. See ConfigMaps & Secrets.
USER node patterns/health vs /readyStack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 19, 2026