Quick reference
1. ❌ FROM node:latest — The next rebuild can pull a very different version. ✅ FROM node:22.11.0-alpine — Pin the version. Update on purpose, not by accident.
2. Don't
no USER line (runs as root)
Any breakout starts with full root rights.
Do instead
USER appRun the process as an unprivileged user.
3. Don't
ENV DB_PASSWORD=secretBaked into the image. Anyone who pulls it can read it.
Do instead
docker run --env-file .env appPass secrets at runtime, or use a secrets manager.
4. Don't
-p 5432:5432Publishes the database on every interface, past ufw.
Do instead
-p 127.0.0.1:5432:5432Bind to localhost, or keep it on an internal network.
5. Don't
docker run -d appAfter a crash or a reboot, it simply stays down.
Do instead
docker run -d --restart unless-stopped appComes back after crashes and host reboots.
Explained
- FROM node:latest latest is not a version, it is whatever was newest when you built. Next month's rebuild can bring a new major version and break the app without a single code change. Instead: pin an exact tag like node:22.11.0-alpine, and update it deliberately.
- No USER line, so the app runs as root By default the process inside the container is root. If the app is compromised and the attacker breaks out of the container, they start with root rights. Instead: create a user in the Dockerfile and add
USER app.
ENV DB_PASSWORD=secretin the Dockerfile Everything in a Dockerfile ends up in the image layers. Anyone who can pull the image can read it with docker history or docker inspect. Instead: pass secrets at runtime with--env-fileor a secrets manager, and keep .env files out of git.
-p 5432:5432This publishes the port on every interface. Docker writes its own iptables rules, so the port can be reachable from the internet even when ufw says it is blocked. Instead:-p 127.0.0.1:5432:5432, or no published port at all and an internal Docker network between app and database.
docker run -d app, with no restart policy If the app crashes, or the server reboots for an update, the container stays down until someone notices. Instead:--restart unless-stopped, or restart: unless-stopped in compose.
None of these take more than a minute to fix, and each one has caused real outages or breaches.
In one line
Containers are easy. Production is not. Pin it, drop root, hide secrets, bind tight, restart.
Check what is published
docker ps --format '{{.Names}} {{.Ports}}'More notes: Docker & containers
- P277The Docker command cheat sheet
- P1995 Dockerfile mistakes to avoid
- P196Alpine vs Debian base images
- P1949 docker commands for building and shipping images
- P1649 docker compose commands for daily work
- P163Anatomy of a container image reference
- P152Docker Compose vs Kubernetes
- P147Quiz: 3 container questions
Open in the notes app: search, filters, copy This note on GitHub