Skip to content

Docker — Practical

1.7
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY package.json ./
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/healthz || exit 1
CMD ["node", "--enable-source-maps", "dist/server.js"]
FROM golang:1.22 AS build
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian12
COPY --from=build /out/app /app
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/app"]
FROM python:3.12-slim AS deps
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN pip install --no-cache-dir uv && uv sync --frozen --no-dev
FROM python:3.12-slim
WORKDIR /app
COPY --from=deps /app/.venv /app/.venv
COPY . .
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
USER 1000:1000
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
node_modules
.git
.github
.env*
dist
build
*.log
coverage
.vscode
*.test.*
README.md
services:
api:
build: { context: ., target: dev }
ports: [ "3000:3000" ]
volumes: [ ".:/app", "/app/node_modules" ]
command: npm run dev
env_file: .env.local
depends_on:
pg: { condition: service_healthy }
redis: { condition: service_started }
pg:
image: postgres:16-alpine
environment: { POSTGRES_PASSWORD: x }
ports: [ "5432:5432" ]
volumes: [ pgdata:/var/lib/postgresql/data ]
healthcheck:
test: ["CMD","pg_isready","-U","postgres"]
interval: 5s
redis:
image: redis:7-alpine
ports: [ "6379:6379" ]
volumes:
pgdata:
Terminal window
docker buildx create --name multi --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/org/app:1.2.3 \
--push .
Terminal window
docker run --rm \
--memory=512m --memory-swap=512m \
--cpus=1.5 \
--pids-limit=200 \
--read-only --tmpfs /tmp \
--user 1000:1000 \
--cap-drop=ALL --cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
myimg
Terminal window
docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}'
docker history myimg
docker save myimg | tar -t | head -50
dive myimg # interactive layer browser
Terminal window
trivy image myimg:1.2.3
trivy fs . # scan project deps too
grype myimg
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
// Node graceful shutdown
process.on('SIGTERM', async () => {
await server.close();
await db.end();
process.exit(0);
});
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -fsS http://localhost:3000/healthz || exit 1
# option 1: dumb-init
RUN apk add --no-cache dumb-init
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]
# option 2: --init at runtime
docker run --init myimg
Terminal window
# clean dangling images, stopped containers, unused networks/volumes
docker system prune -af --volumes
# full disk audit
docker system df
  • dive — image layer explorer.
  • hadolint — Dockerfile linter.
  • trivy / grype / snyk — vuln scan.
  • cosign — image signing.
  • dockle — best-practice linter.
  • kaniko / buildah — daemonless builds (CI).
  • podman — daemonless drop-in for docker CLI.
  • slim / docker-slim — image diet automation.

1.7
FROM node:20-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
FROM deps AS build
COPY tsconfig.json ./
COPY src ./src
RUN npm run build # produces /app/dist
RUN npm prune --omit=dev # strip devDependencies
FROM gcr.io/distroless/nodejs20-debian12 AS runtime
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER 1000
EXPOSE 3000
CMD ["dist/server.js"]

Deep — alternative: distroless from builder

Section titled “Deep — alternative: distroless from builder”
FROM node:20-bookworm-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
# Distroless runtime — no shell, no apt, no curl
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=build /app /app
USER nonroot # distroless ships UID 65532 'nonroot'
CMD ["server.js"] # NOTE: no shell form allowed

Deep — K8s “restricted” pod baseline

Section titled “Deep — K8s “restricted” pod baseline”
apiVersion: v1
kind: Pod
metadata: { name: api }
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile: { type: RuntimeDefault }
containers:
- name: api
image: registry/api:1.4.2
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }
volumeMounts:
- { name: tmp, mountPath: /tmp }
- { name: cache, mountPath: /app/.cache }
volumes:
- { name: tmp, emptyDir: {} }
- { name: cache, emptyDir: {} }

Deep — cache-optimised Dockerfile + .dockerignore + multi-arch

Section titled “Deep — cache-optimised Dockerfile + .dockerignore + multi-arch”
1.7
FROM node:20-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./ # ← changes rarely
RUN --mount=type=cache,target=/root/.npm npm ci
COPY tsconfig.json ./
COPY src ./src # ← changes often, AFTER deps
RUN npm run build && npm prune --omit=dev
Terminal window
# multi-arch build & push (single command)
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from type=gha --cache-to type=gha,mode=max \
-t $REGISTRY/api:$SHA --push .
.dockerignore
node_modules
.git
.env*
coverage
**/*.test.ts
Dockerfile*

Deep — production-grade Deployment skeleton

Section titled “Deep — production-grade Deployment skeleton”
apiVersion: apps/v1
kind: Deployment
metadata: { name: api }
spec:
replicas: 3
strategy: { type: RollingUpdate, rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } }
template:
spec:
terminationGracePeriodSeconds: 30
securityContext: { runAsNonRoot: true, runAsUser: 1000, seccompProfile: { type: RuntimeDefault } }
containers:
- name: api
image: registry/api@sha256:abc123...
ports: [{ containerPort: 8080 }]
resources:
requests: { cpu: 200m, memory: 256Mi }
limits: { cpu: 1, memory: 512Mi }
livenessProbe: { httpGet: { path: /healthz, port: 8080 }, periodSeconds: 10, failureThreshold: 3 }
readinessProbe: { httpGet: { path: /ready, port: 8080 }, periodSeconds: 5, failureThreshold: 2 }
startupProbe: { httpGet: { path: /healthz, port: 8080 }, periodSeconds: 5, failureThreshold: 30 }
lifecycle:
# Distroless-safe: no /bin/sh. This endpoint should mark readiness false and start draining.
preStop: { httpGet: { path: /drain, port: 8080 } }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }
volumeMounts:
- { name: tmp, mountPath: /tmp }
volumes:
- { name: tmp, emptyDir: {} }