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.
Prerequisites
- Docker Engine 29.x with BuildKit (the default build backend).
- Compose v2 - invoked as
docker compose(a subcommand, not the olddocker-composebinary). - A local Kubernetes for the K8s-specific checks: kind, minikube, or k3d (optional but recommended).
- kubectl matched to your cluster minor (1.34-1.36 for a 1.36 control plane).
Quick check that your toolchain is ready:
docker version
docker compose version
kubectl version --clientBasic Examples
1. A minimal, production-shaped Dockerfile
A 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.- Two stages:
buildcompiles,runtimeships only the artifacts. USER noderuns as a non-root user, matching production security defaults.- Pinning
node:22-bookworm-slimkeeps the base predictable; pin by digest for stricter parity.
2. Build and run the image
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:devnames the image; the same recipe produces the image CI builds.--rmcleans up the container on exit so you do not accumulate stopped containers.-e LOG_LEVEL=debuginjects config at run time - the app binary is unchanged.-p 3000:3000publishes the container port to your host.
3. Inspect a running container
You debug containers by looking inside them, not by guessing.
docker ps
docker logs -f <container-id>
docker exec -it <container-id> shdocker pslists running containers and their published ports.docker logs -fstreams stdout/stderr, which is where a twelve-factor app should log.docker exec -it ... shopens a shell inside the running container to inspect files or env.
4. Compose for multi-service wiring
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_filesupplies dev config so no secrets are baked into the image.depends_onwithcondition: service_healthywaits for the healthcheck to pass.- Compose v2 is the current spec; run it with
docker compose up.
5. Start, view, and tear down a stack
The everyday inner-loop commands.
docker compose up -d --build
docker compose logs -f api
docker compose down -vup -d --buildrebuilds changed images and starts everything detached.logs -f apitails a single service.down -vstops the stack and removes named volumes, giving a clean slate.
6. Persist data with a named volume
Databases need their data to survive container restarts.
services:
db:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:- A named volume
pgdatais managed by Docker and outlives the container. - Mounting it at Postgres's data directory keeps your local schema between runs.
- Use
docker compose down(without-v) to keep the data,-vto wipe it.
7. Keep secrets and config out of the image
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- The
.env.devfile feeds Compose viaenv_file, never the image. - The same image reads
DATABASE_URLin prod from a Kubernetes Secret instead. - Add these files to
.gitignoreso credentials never reach the repo.
Intermediate Examples
8. Run the production image locally
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 runtimebuilds the final stage, the exact artifact you deploy.--read-onlymirrors a hardened prod filesystem so you catch write-path bugs early.--tmpfs /tmpgrants the one writable path most apps still need.
9. Load your image into a local Kubernetes
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-imageinjects your locally built image into the cluster's containerd.--image-pull-policy=Nevertells Kubernetes to use the loaded image, not pull it.- Remember the runtime here is containerd via the CRI, not Docker Engine.
10. Add a health probe and resource limits
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" }- The readiness probe keeps traffic away until the app reports healthy.
requestsinform scheduling; the memorylimittriggers an OOM kill if exceeded.- Testing these locally catches crash-loops that Compose can never reproduce.
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).