Containers isolate applications, but they aren’t a security boundary by default the way a VM is. They share the host kernel, and a few common mistakes (an exposed Docker API, privileged containers, the Docker socket mounted into a web app) hand attackers root on the host. This checklist covers the practices that matter most on a server running Docker or Docker Compose.

1. Protect the Docker daemon

  • Never expose the Docker API on TCP (-H tcp://0.0.0.0:2375). Anyone who can reach it gets root on the host. Bots scan for it constantly to deploy crypto-miners. If remote access is needed, use SSH (DOCKER_HOST=ssh://user@server) or TLS with client certificates.
  • The docker group is root-equivalent. Only add trusted admins. See Docker socket permission.
  • Don’t mount /var/run/docker.sock into containers unless the container is a trusted management tool (Traefik, Portainer, Watchtower), and understand that it then controls the host. Use a socket proxy with limited permissions where possible.
  • Consider rootless Docker or Podman for extra isolation. See rootless Docker and Podman vs Docker.

2. Mind the firewall bypass

Ports published with -p 8080:80 are opened in iptables by Docker directly, bypassing UFW rules. A database published as -p 5432:5432 is reachable from the internet even if UFW says otherwise.

  • Publish internal services only on localhost: -p 127.0.0.1:5432:5432, or not at all. Containers on the same Compose network can talk without publishing ports.
  • Use the DOCKER-USER iptables chain for rules that must apply to containers.
  • Verify from outside with nmap.

See Docker networking and UFW.

3. Run containers as non-root

Most images run as root inside the container by default. If the app is compromised, root inside a container makes escaping easier, and files it writes to bind mounts are owned by root.

FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]

Or at runtime: docker run --user 1000:1000 … / user: "1000:1000" in Compose.

4. Drop privileges and capabilities

services:
  app:
    image: myapp:1.4.2
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
  • Never use --privileged for normal applications. It disables nearly all isolation.
  • cap_drop: ALL, then add back only what’s needed (NET_BIND_SERVICE for ports below 1024).
  • read_only: true with tmpfs for scratch space blocks attackers from writing tools into the container.
  • Keep the default seccomp and AppArmor or SELinux profiles enabled.

5. Use small, trusted, pinned images

  • Prefer official or verified images, and minimal bases (alpine, distroless, -slim variants). Fewer packages means fewer vulnerabilities. See reduce Docker image size.
  • Pin versions (postgres:16.4, or digests) instead of latest, so updates are deliberate and reproducible.
  • Use multi-stage builds, so compilers and build tools don’t ship in the final image. See multi-stage builds.
  • Scan images for known CVEs:
trivy image myapp:1.4.2
docker scout cves myapp:1.4.2

6. Handle secrets properly

  • Never bake secrets into images (COPY .env, ENV PASSWORD=…, ARG values all end up in image layers).
  • Pass them at runtime with env files outside the image, or better, Compose secrets mounted as files. See secrets management and Docker environment variables.
  • Keep .env out of Git and out of the build context (.dockerignore).

7. Limit resources

A runaway or attacked container shouldn’t take down the host:

    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
    pids_limit: 200

See Docker CPU and memory limits.

8. Isolate networks

Put each stack on its own network. The database joins only the backend network, and only the reverse proxy joins the public-facing one:

networks:
  frontend:
  backend:
    internal: true      # no outbound internet access

9. Keep everything updated

  • Update the host OS and Docker Engine. See patch management.
  • Rebuild images regularly to pick up base image fixes, and redeploy. Watchtower or CI pipelines can automate pulls. Pin major versions so updates don’t break things unexpectedly. See Watchtower.
  • Remove unused images and containers: docker system prune. See docker prune.

10. Log and monitor

  • Container logs to the journal or a log stack, with size limits set (max-size). See docker logs.
  • Watch for unexpected containers (docker ps -a), images you didn’t pull, and CPU spikes. A miner in a container is common after a daemon exposure.
  • Health checks and restart policies for reliability. See Docker healthcheck.

Quick checklist

Check Command / setting
Daemon not on TCP ss -tlnp | grep -E '2375|2376' returns nothing
No privileged containers docker ps -q | xargs docker inspect -f '{{.Name}} {{.HostConfig.Privileged}}'
Ports bound as intended docker ps --format '{{.Names}} {{.Ports}}'
Non-root users USER in the Dockerfile, user: in Compose
Images scanned trivy image …
Limits set mem_limit / deploy.resources

Related: Docker vs VM and Kubernetes security basics.

  • Docker
  • Security
  • DevOps