Local Dev Basics
This page is the one-stop intro to running a containerized service on your own machine: building images, wiring dependencies with Compose, and staying close to production so the same image ships everywhere.
Search across all documentation pages
This page is the one-stop intro to running a containerized service on your own machine: building images, wiring dependencies with Compose, and staying close to production so the same image ships everywhere.
docker compose (a subcommand, not the old docker-compose binary).Quick check that your toolchain is ready:
docker version
docker compose version
kubectl version --clientA multi-stage build keeps build tools out of the runtime image.
# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]# syntax= opts into the current Dockerfile frontend so newer features work under BuildKit.build compiles, runtime ships only the artifacts.USER node runs as a non-root user, matching production security defaults.node:22-bookworm-slim keeps the base predictable; pin by digest for stricter parity.Build once, then run it exactly as CI would.
docker build -t api:dev .
docker run --rm -p 3000:3000 -e LOG_LEVEL=debug api:dev-t api:dev names the image; the same recipe produces the image CI builds.--rm cleans up the container on exit so you do not accumulate stopped containers.-e LOG_LEVEL=debug injects config at run time - the app binary is unchanged.-p 3000:3000 publishes the container port to your host.You debug containers by looking inside them, not by guessing.
docker ps
docker logs -f <container-id>
docker exec -it <container-id> shdocker ps lists running containers and their published ports.docker logs -f streams stdout/stderr, which is where a twelve-factor app should log.docker exec -it ... sh opens a shell inside the running container to inspect files or env.Real apps have dependencies; Compose declares them as one graph.
services:
api:
build: .
ports:
- "3000:3000"
env_file: .env.dev
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: devsecret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 5build: . builds the API from your Dockerfile; image: pulls Postgres.env_file supplies dev config so no secrets are baked into the image.depends_on with condition: service_healthy waits for the healthcheck to pass.docker compose up.The everyday inner-loop commands.
docker compose up -d --build
docker compose logs -f api
docker compose down -vup -d --build rebuilds changed images and starts everything detached.logs -f api tails a single service.down -v stops the stack and removes named volumes, giving a clean slate.Databases need their data to survive container restarts.
services:
db:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:pgdata is managed by Docker and outlives the container.docker compose down (without -v) to keep the data, -v to wipe it.Config differs by environment, so it must not be baked in.
# .env.dev (git-ignored)
DATABASE_URL=postgres://postgres:devsecret@db:5432/app
LOG_LEVEL=debug.env.dev file feeds Compose via env_file, never the image.DATABASE_URL in prod from a Kubernetes Secret instead..gitignore so credentials never reach the repo.The strongest local check is running the real prod image, not a dev variant.
docker build -t api:prod --target runtime .
docker run --rm -p 3000:3000 --read-only --tmpfs /tmp \
-e DATABASE_URL=postgres://... api:prod--target runtime builds the final stage, the exact artifact you deploy.--read-only mirrors a hardened prod filesystem so you catch write-path bugs early.--tmpfs /tmp grants the one writable path most apps still need.Some behavior (probes, limits, policy) only appears in Kubernetes.
kind create cluster
kind load docker-image api:prod
kubectl run api --image=api:prod --port=3000 --image-pull-policy=Neverkind load docker-image injects your locally built image into the cluster's containerd.--image-pull-policy=Never tells Kubernetes to use the loaded image, not pull it.Give the pod the same guardrails production uses.
apiVersion: v1
kind: Pod
metadata:
name: api
spec:
containers:
- name: api
image: api:prod
readinessProbe:
httpGet: { path: /healthz, port: 3000 }
initialDelaySeconds: 3
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { memory: "256Mi" }requests inform scheduling; the memory limit triggers an OOM kill if exceeded.Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026