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.
| Aspect | Container | Virtual machine |
|---|---|---|
| Startup | Seconds | Minutes |
| Isolation | Process | Full system |
| Size | Megabytes | Gigabytes |
| Kernel | Shared with the host | Its own |
| Typical use | Services, CI, development | Strong 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
| Symptom | Likely cause | Action |
|---|---|---|
| Port already in use | Another process owns the port | Change the mapping or stop the process |
| Container restarts | The process or healthcheck fails | Check docker logs and the command |
| Data does not persist | Missing volume | Declare the volume in Compose |
| Permission denied | UID differs from the host | Adjust the user or permissions |
| Huge image | Build dependencies and layers | Multi-stage, Alpine and .dockerignore |
| Wrong architecture | ARM image on an x86 server | Build 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.