C.G.

Docker for Developers — The Essentials

Docker does not remove bugs, but it removes an entire class of differences between environments. This guide covers what I use every day: images and containers, Dockerfile, Compose, data, networking, security and operations, plus the mistakes that cost the most.

What Docker is and what problem it solves

Docker packages an application with its dependencies into an image that runs the same locally, in CI and on the server. It solves the classic "it works on my machine" problem without forcing you to maintain a virtual machine per service.

AspectContainerVirtual machine
StartupSecondsMinutes
IsolationProcessFull system
SizeMegabytesGigabytes
KernelShared with the hostIts own
Typical useServices, CI, developmentStrong isolation or another OS

Image, container, volume and network

An image is the immutable package: base system, dependencies and code. A container is a running instance. If the image is the recipe, the container is the dish. A volume is storage that survives the container, and a network lets services find each other by name without exposing unnecessary ports.

That distinction explains why rebuilding an image changes the container and why data written inside a container disappears when it is recreated unless it lives in a volume.

First commands

# Build an image
docker build -t my-app .

# Run a container
docker run -d -p 3000:3000 --name app my-app

# Follow logs
docker logs -f app

# Open a shell inside the container
docker exec -it app sh

# List containers and images
docker ps
docker images

# Stop and remove a container
docker stop app
docker rm app

Dockerfile: layers, multi-stage and .dockerignore

A Dockerfile builds in layers. Order matters: copying package.json and the lockfile first lets Docker reuse the dependency layer when only the code changes. Copying everything before npm ci invalidates the cache on every commit.

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main.js"]

Multi-stage separates build from runtime, leaves a smaller final image and keeps build dependencies out of production. A .dockerignore with node_modules, .git and local files speeds up the build and shrinks the context.

Docker Compose: multiple services without pain

services:
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
  api:
    build: ./api
    ports:
      - "3000:3000"
    depends_on:
      - db

volumes:
  pgdata:

With a single docker compose up -d I deploy PostgreSQL, Redis, n8n, a NestJS API and a React dashboard. A docker compose down stops the services, but the pgdata volume keeps the data.

Data and secrets

Persistent data belongs in volumes; secrets belong in environment variables or a secret manager. Never in the image or the repository. A .env file is fine for development but must be in .gitignore. If the PostgreSQL container has no volume, a docker compose down can wipe the database.

Networking and reverse proxy

In Compose, services find each other by name: the API connects to db:5432, not localhost. That internal network keeps the database off the public internet; only the service that needs a public port publishes one. In production, a reverse proxy or the platform terminates TLS and forwards to the container; the container should not care whether it sits behind Nginx, Caddy or a tunnel.

Build cache and CI/CD

In CI I build the image once, tag it with the commit SHA and promote it across environments. That way what gets tested is exactly what gets deployed. Layer cache speeds up builds only if the Dockerfile order respects it. For heavy dependencies, a BuildKit cache or an intermediate registry avoids rebuilding everything on each pipeline.

Basic security

Do not run containers as root, do not publish the database and keep base images updated. A Trivy scan in CI catches known vulnerabilities before they reach production. Secrets do not belong in the Dockerfile: if you must pass them at build time, use BuildKit secrets instead of ARG, because arguments stay in the image history.

Operations and troubleshooting

In production I watch logs, use healthchecks and set a restart policy. Compose lets me declare restart: unless-stopped and a healthcheck that hits an endpoint. If the container crashes, it restarts; if it stops responding, it is marked unhealthy. docker stats shows CPU and memory, docker system df reveals space usage and docker system prune cleans up unused resources.

Common errors and how to diagnose them

SymptomLikely causeAction
Port already in useAnother process owns the portChange the mapping or stop the process
Container restartsThe process or healthcheck failsCheck docker logs and the command
Data does not persistMissing volumeDeclare the volume in Compose
Permission deniedUID differs from the hostAdjust the user or permissions
Huge imageBuild dependencies and layersMulti-stage, Alpine and .dockerignore
Wrong architectureARM image on an x86 serverBuild with --platform

Docker in development

Locally I use bind mounts to mount the code inside the container and benefit from hot reload. The container keeps the dependencies while the editor keeps working on the host. For one-off tasks, docker compose run --rm runs a command without leaving dangling containers.

I do not rebuild the image on every code change; only when dependencies or the Dockerfile change. That separation saves time and avoids unnecessary builds.

Compose, Swarm or Kubernetes

Compose is enough for a VPS with a few services on a single host. If you need high availability, autoscaling or multiple nodes, the next step is an orchestrator. Kubernetes solves that, but adds operational complexity; I would not adopt it just because it sounds more professional.

For many projects, a VPS with Compose, healthchecks and well-tested backups is more reliable than a poorly managed cluster.

Registries and images

I tag every image with the commit SHA and publish it to a registry. The server pulls that exact tag, not latest. That lets me return to a previous version without guessing what the image contained.

Resource limits and logs

In Compose I set memory and CPU limits so one service cannot consume the whole host. I also configure the log driver with rotation; without a limit, a noisy container can fill the disk. They are small settings that prevent large incidents.

Docker, Podman and other alternatives

Podman is a daemonless alternative with rootless support; for most workflows, the commands are compatible with Docker. Docker remains the standard and has the largest ecosystem. The choice depends on the environment and security policies, not on personal preference.

Lighter runtimes exist for embedded cases, but they do not replace Docker when you need portable images, Compose and a registry. Before switching tools, measure the specific problem you are solving.

Zero-downtime deployment

To update a service without dropping connections, I start the new container, wait for its healthcheck and only then remove the old one. With a reverse proxy in front, traffic moves when the new container is healthy. If the healthcheck fails, the proxy keeps pointing to the previous version.

It is not orchestration magic: it is a small, testable procedure. On a VPS with Compose, a deployment script with a healthcheck covers most cases.

When not to use Docker

For a one-off script or a small serverless function, a container can be more infrastructure than you need. Docker shines when there are several services, system dependencies or a real difference between local and production.

Production checklist

  • Image pinned to a version, not latest.
  • Non-root user and .dockerignore.
  • Data in volumes and secrets outside the image.
  • Healthcheck and restart policy.
  • Only the necessary ports published.
  • Memory and CPU limits defined.
  • Accessible logs and rotation configured.
  • Vulnerability scan in CI.
  • Tested database backup.
  • Documented rollback process.

Frequently asked questions

Is Docker a virtual machine?

No. A container shares the host kernel and isolates the process; a virtual machine emulates a full system. Containers start faster and weigh less, although the isolation model is different.

Can I use Docker in production?

Yes, with healthchecks, restarts, resource limits and a deployment strategy. For small projects, a managed platform can be simpler than running an orchestrator.

How do I persist data?

With volumes. Data written to the container layer is lost when it is recreated. Declare the volume in Compose and test restoring a backup.

How do I reduce image size?

Use Alpine, multi-stage builds, .dockerignore and production-only dependencies in the final stage. Inspect size by layer before optimizing blindly.

Further reading

The official reference is the Docker and Docker Compose documentation. The Convenios case study, with PostgreSQL, n8n and a NestJS API in containers, is here. If you want a second opinion on your infrastructure, let's talk. You can also read how I take an AI model to a service in From Time Series to Product.

← Back