Docker for App Developers (Not Just DevOps)
Images, compose, and local parity habits that help fullstack teams ship without treating containers as someone else's problem.
Docker is not only for platform teams. Application developers who understand images, layers, and compose files debug production mismatches faster, write better CI pipelines, and stop treating “works on my machine” as a personality trait.
This post is for fullstack engineers who ship Next.js or Node APIs and need containers as a dev and deploy artifact, not a second career in Kubernetes. It connects to Postgres basics when your database runs in compose locally, background jobs when workers share an image with the API, and auth patterns when cookies and origins differ between host and container.
What a container actually gives you
A container packages your app plus its runtime assumptions into an image you can run anywhere the engine supports. That is portability with boundaries: same glibc, same Node version, same OS packages—not magic isolation from misconfiguration.
The Docker overview explains images, containers, and registries without drowning in orchestration jargon. Start there if your mental model is still “VM but smaller.”
Images: Dockerfile discipline
Treat your Dockerfile as code reviewed like application logic:
- Pin base image tags (
node:22-bookworm-slim), not floatinglatest. - Use multi-stage builds: compile in a builder stage, copy only production artifacts into the final image.
- Run as non-root when your platform allows; many Node images document an unprivileged user.
Follow Dockerfile best practices for layer ordering—dependency install before source copy so rebuilds stay fast when you change application code only.
.dockerignore matters as much as .gitignore. Do not copy node_modules, .next, or local .env into images. Secrets belong in runtime injection, not layers—see env and secrets without leaking production.
Compose for local product stacks
Docker Compose is the fastest way to run API + Postgres + Redis + mail catcher on one laptop. Name services clearly (api, db, worker). Use healthchecks so depends_on waits for Postgres readiness, not just container start.
Map ports explicitly; document which URL the Next.js app should use (http://api:3000 inside the network versus localhost:3001 from the host). Environment files for compose should list dev-only values—never production credentials, even “just for testing.”
When your marketing site uses static export, compose might only run the API and workers; the frontend builds to static files served by nginx in another service or skipped entirely in favor of next dev on the host. Pick one story per repo and document it in the README.
Parity without cloning production
Local compose does not need every managed service. Substitute:
- Local Postgres instead of Aurora for feature work.
- MinIO or Azurite for S3-shaped APIs when you test uploads.
- Stripe CLI for webhook forwarding instead of exposing ngrok in docs.
Parity targets behavior (SQL dialect, env var names, queue semantics), not scale. Load tests belong in staging with realistic data volumes.
Networking and URLs
Containers get DNS names on the compose network. Browser code cannot call http://api:3000—that hostname exists inside Docker, not on the host. Use localhost with published ports for client-side fetches, or proxy through Next.js rewrites during dev.
Misconfigured NEXT_PUBLIC_* URLs are a top source of “CORS works in prod but not locally.” Align public origins with what auth cookies expect; auth session patterns apply unchanged in containerized dev.
Volumes and data persistence
Bind-mount source for hot reload in dev; use named volumes for database data so docker compose down does not wipe your seed data unless you intend to. Document docker compose down -v as destructive in team onboarding.
Migration workflows should run in a one-off container or documented make target: docker compose run --rm api npm run migrate. Same image as production reduces “migration worked locally in psql only” surprises.
CI: build once, promote the digest
CI should docker build and push to a registry with immutable tags (commit SHA). Deploy by digest or SHA tag, not main floating tags. Scan images with your provider’s scanner or Docker Scout where available—treat CVE noise as triage, not automatic panic.
Integration tests can docker compose -f compose.ci.yml up against ephemeral services. Keep CI compose minimal: faster feedback beats reproducing every sidecar.
When not to dockerize the app
Static sites and edge-hosted frontends may never need a runtime image—artifact upload to CDN is enough. Serverless functions package differently. Do not dockerize reflexively; dockerize when repeatable runtime or multi-service local dev wins.
Production paths beyond compose
Compose is not production orchestration for most teams. You will land on managed container services (ECS, Cloud Run, App Service), Kubernetes, or PaaS that builds from your Dockerfile. The image you built for CI should be the same artifact promoted—only env and scale change.
Read your host’s docs on graceful shutdown: Node apps should handle SIGTERM, drain HTTP, then exit. Workers should finish or release job leases—pair with background jobs you can trust.
Debugging containerized apps
docker logs and structured JSON logging beat console.log without requestId. Exec into containers sparingly in prod; prefer observability hooks from the observability starter kit.
For “file not found” in prod, compare working directory (WORKDIR), copied paths, and whether Next.js output: 'standalone' is configured when you expect a self-contained Node server per Next.js output configuration.
Resource limits in dev and CI
Without CPU and memory limits, one hungry container can starve your laptop or CI runner. Compose supports deploy.resources.limits on supported engines; at minimum document recommended Docker Desktop memory for new hires. OOM kills during next build inside Docker usually mean the Node heap needs NODE_OPTIONS=--max-old-space-size=4096 or a larger build machine—not “delete .next and pray.”
Keep dev images smaller than prod if you add debug tooling locally; do not ship debug shells in production tags.
Security basics developers own
- No secrets in images or build args logged by CI.
- Minimal packages in final stage; fewer CVEs to chase.
- Read-only root filesystem where the platform supports it.
- Pin dependencies in
package-lock.jsonand rebuild on security advisories.
Working with platform teams
If a platform group owns Kubernetes, your job is still to produce a runnable image with documented ports, health check path, and required env vars. Provide a minimal HEALTHCHECK in the Dockerfile or defer to orchestrator probes—agree on one pattern. Changelog breaking changes to base images in team channels; silent Node minor bumps have broken native addons before.
Closing
Docker for app developers means owning the Dockerfile, compose for local stacks, and the contract between build and deploy. You do not need to operate a cluster to benefit—faster onboarding, fewer environment arguments, and images that CI and production both trust. Start with one service and Postgres in compose, multi-stage production Dockerfile, and documented env files; expand when the product actually needs more moving parts.
When onboarding asks “why is Docker slow on Windows,” point them to WSL2 backend docs and virtiofs settings before disabling containers entirely—half the team on native Linux and half on mismatched Docker setups recreates the environment debate every sprint.
Treat docker system prune like git clean: useful when you understand what disappears, embarrassing when someone deletes the only local copy of a seeded database volume the night before a demo.