Docker Compose and Dockerfile Configuration

A container that works is not the same as a container that is safe to expose. The defaults, a root user, a writable filesystem, every Linux capability and a port open on all interfaces, are the opposite of what you want in production.

Pin the image and drop root

A tag like latest changes under you on the next build, so pin a major version. Run the process as an unprivileged user: if the app is compromised, the attacker then cannot rewrite system files or install tools. The official Node image already ships a node user; on slim Python images you create one with useradd.

FROM node:22-alpine
COPY --chown=node:node . .
USER node

Harden the running container

A read-only root filesystem with a tmpfs for /tmp stops an attacker persisting files. Dropping all capabilities removes powers such as changing file ownership or binding low ports, and no-new-privileges stops a process gaining more through a setuid binary. Apps that need to write somewhere should get a named volume, not a writable root.

read_only: true
tmpfs:
  - /tmp
cap_drop:
  - ALL
security_opt:
  - no-new-privileges:true

Publish on localhost behind a reverse proxy

A published port such as 3000:3000 listens on every interface, and Docker adds its own firewall rules that bypass ufw. Publish on 127.0.0.1 and let Nginx or Caddy on the same host face the internet. Databases and caches should not be published at all: other containers reach them by service name.

ports:
  - "127.0.0.1:3000:3000"

Healthchecks and start order

A HEALTHCHECK lets Docker mark a container unhealthy, and depends_on with condition: service_healthy makes the app wait until the database actually accepts connections instead of just having started. Slim Python images have neither curl nor wget, so the check calls the interpreter.

Open the Docker & Compose generator

Frequently asked questions

Why does Docker bypass my ufw rules?

Docker inserts its own iptables rules for published ports, ahead of the chains ufw manages. A port published without a 127.0.0.1 prefix is reachable even if ufw denies it. Bind to localhost or manage the DOCKER-USER chain.

Where should I keep database passwords?

In a .env file next to the compose file, kept out of git and out of the image by .dockerignore. The ${POSTGRES_PASSWORD:?message} form makes Compose refuse to start when the value is missing.

Is a multi-stage build worth it?

Yes for Node: the first stage installs production dependencies, and the final image holds only what runs, without build caches or dev tooling.

Guides