2026-07-28 · Cybersecurity

Docker Container Security: 9 Settings I Learned the Hard Way

Three weeks. That's how long it took me to fully clean up after a compromised Docker container back in 2023. The attacker got in through a Redis container I'd spun up for testing, forgot about, and left running with default settings for six months.

They mined crypto. They probed my internal network. They used my server to scan other people's infrastructure. AWS sent me a nicely-worded abuse report that started my Saturday off great.

I've since turned container security into a bit of a personal mission. Here are the 9 settings I check on every single container deployment now — whether it's a side project or something handling customer data.

1. Never Run as Root

This is the big one. By default, processes inside a container run as root. If someone breaks out of that container, they have root access on your host (or close to it).

I've seen way too many Dockerfiles that start with FROM node:18 and never set USER. Here's what you should do instead:

FROM node:18-alpine

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

COPY --chown=appuser:appgroup . /app
WORKDIR /app

USER appuser

CMD ["node", "server.js"]

Alpine images make this easy. If you're using Ubuntu-based images, use useradd -m appuser instead.

One gotcha: if your app needs to bind to a port below 1024 (like 80 or 443), you won't be able to as a non-root user. Use a reverse proxy like Nginx or Caddy that forwards to your app on a high port like 3000.

2. Read-Only Root Filesystem

This one literally saved me when a container got compromised last year. The attacker downloaded a script to /tmp, but my filesystem was read-only so it failed.

docker run --read-only --tmpfs /tmp --tmpfs /var/run my-app:latest

The --read-only flag makes the container's filesystem immutable. You need --tmpfs for directories that require writes, but everything else is locked down.

In docker-compose:

services:
  app:
    image: my-app:latest
    read_only: true
    tmpfs:
      - /tmp
      - /var/run

3. Drop All Capabilities, Add Only What You Need

By default, Docker gives containers a bunch of Linux capabilities — CHOWN, DAC_OVERRIDE, FSETID, FOWNER, MKNOD, NET_RAW, SETGID, SETUID, SETFCAP, SETPCAP, NET_BIND_SERVICE, SYS_CHROOT, KILL, AUDIT_WRITE.

Most applications need almost none of these:

docker run --cap-drop ALL --cap-add NET_BIND_SERVICE my-app:latest

For a Node.js API, this is usually sufficient. If you're unsure, run with --cap-drop ALL and watch what breaks in the logs.

4. Set Memory and CPU Limits

I learned this one when a container with a memory leak consumed 12GB of RAM and brought down every other container on the same host.

docker run -m 512m --cpus 0.5 my-app:latest

In docker-compose:

services:
  app:
    image: my-app:latest
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

Don't skip this. A runaway container can cost you real money on cloud infra — I've seen crypto miners consume $200/day in compute.

5. Mount Secrets Read-Only

docker run -v /host/secrets/db_password:/run/secrets/db_password:ro my-app:latest

The :ro at the end is crucial. If the container gets compromised, the attacker can read secrets but can't overwrite them.

6. Don't Expose the Docker Socket

Mounting /var/run/docker.sock into a container gives it root access to your host. Someone who compromises that container can run any Docker command.

# NEVER do this in production
services:
  portainer:
    image: portainer/portainer
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock  # 🚩

If you must use it, at minimum keep it behind authentication. Better: use Docker's remote API with TLS certificates.

7. Scan Images for Vulnerabilities

I scanned my production images with Trivy and found 47 vulnerabilities — including 3 critical ones.

# Install and scan
trivy image my-app:latest

# High+critical only
trivy image --severity HIGH,CRITICAL my-app:latest

Trivy is free and open source. I run it in my CI pipeline — if critical vulnerabilities are found, the build fails.

8. Drop Privileges in Entrypoint

If your entrypoint script needs to run setup tasks, drop privileges before starting the main process:

#!/bin/sh
/app/run-migrations.sh
exec su-exec appuser node server.js

Use su-exec (Alpine) or gosu (Debian) for the privilege drop.

9. Configure Health Checks

A silently crashed container can leave ports open and processes in inconsistent states.

services:
  app:
    image: my-app:latest
    healthcheck:
      test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

The One-Command Security Checklist

Here's the command I now run on every new container deployment:

docker run -d \
  --name my-app \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /var/run \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  -m 512m \
  --cpus 0.5 \
  --restart unless-stopped \
  -v /host/secrets:/run/secrets:ro \
  --health-cmd "wget --spider http://localhost:3000/health" \
  --health-interval 30s \
  --health-timeout 10s \
  --health-retries 3 \
  my-app:latest

Copy it. Modify it. Use it.

These 9 settings have saved me at least three times since I started taking container security seriously. The crypto miner incident was the wake-up call I needed — hopefully this checklist saves you the same headache.

Got a security setting I missed? Drop it in the comments.

← Back to Home