Docker seguro em produção na VPS

Docker com configuração padrão tem vários riscos de segurança: containers rodando como root, imagens com vulnerabilidades conhecidas, portas expostas desnecessariamente e secrets em variáveis de ambiente. Com poucos ajustes no Dockerfile e docker-compose.yml, você elimina a maioria dos vetores de ataque sem mudar o comportamento da aplicação.

Rodar containers como usuário não-root

Por padrão, processos dentro de containers Docker rodam como root, mesmo que o host tenha proteções, um escape de container com root tem impacto maior:

dockerfile
# Dockerfile — criar e usar usuário não-root
FROM node:22-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

# Criar usuário não-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Ou em Debian/Ubuntu:
# RUN useradd --create-home --no-log-init appuser

# Dar ownership dos arquivos ao novo usuário
RUN chown -R appuser:appgroup /app

USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]

# Verificar que o container não roda como root:
# docker compose exec app id
# uid=1000(appuser) gid=1000(appgroup)

Limitar recursos e filesystem read-only

Limite CPU, memória e torne o filesystem do container somente-leitura:

yaml
services:
  app:
    build: .
    restart: always
    user: "1000:1000"           # UID:GID do usuário não-root

    # Limitar recursos
    deploy:
      resources:
        limits:
          cpus: "1.0"           # máximo 1 vCPU
          memory: 512M          # máximo 512 MB RAM
        reservations:
          memory: 128M

    # Filesystem somente-leitura (exceto volumes explícitos)
    read_only: true
    tmpfs:
      - /tmp                    # /tmp ainda precisa ser gravável
      - /run

    # Segurança adicional
    security_opt:
      - no-new-privileges:true  # impedir escalonamento de privilégios
    cap_drop:
      - ALL                     # remover todas as capabilities Linux
    cap_add:
      - NET_BIND_SERVICE        # adicionar apenas o necessário

Redes Docker isoladas

Por padrão, todos os containers na mesma rede podem se comunicar. Isole por responsabilidade:

yaml
services:
  app:
    networks:
      - frontend_net   # acessa o Nginx
      - backend_net    # acessa o banco

  nginx:
    networks:
      - frontend_net   # apenas frontend

  postgres:
    networks:
      - backend_net    # apenas backend — sem acesso do Nginx direto

  redis:
    networks:
      - backend_net

networks:
  frontend_net:
    driver: bridge
  backend_net:
    driver: bridge
    internal: true     # sem acesso à internet (apenas comunicação interna)

Secrets e variáveis sensíveis

Variáveis de ambiente aparecem em docker inspect. Para secrets críticos, use Docker Secrets (modo Swarm) ou monte arquivos:

yaml
# Opção 1: variáveis de ambiente (aceitável, não ideal)
# Aparecem em: docker inspect, /proc/PID/environ
# Mitigação: não expor a porta Docker daemon; usar env_file

# Opção 2: arquivo montado como secret (sem Swarm)
services:
  app:
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - /opt/secrets/db_password:/run/secrets/db_password:ro

# Na aplicação Node.js — ler do arquivo:
const dbPassword = process.env.DB_PASSWORD_FILE
  ? require('fs').readFileSync(process.env.DB_PASSWORD_FILE, 'utf8').trim()
  : process.env.DB_PASSWORD

# Opção 3: Docker Secrets (modo Swarm)
# docker secret create db_password /path/to/file
# No compose: secrets: [db_password]

Escanear imagens por vulnerabilidades

Use Trivy para detectar CVEs em imagens Docker antes de subir para produção:

bash
# Instalar Trivy (scanner de vulnerabilidades)
apt install -y wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | tee /etc/apt/sources.list.d/trivy.list
apt update && apt install trivy

# Escanear imagem local
trivy image minha-api:latest

# Escanear e falhar se houver CVE crítica (bom para CI/CD)
trivy image --exit-code 1 --severity CRITICAL minha-api:latest

# Escanear Dockerfile por má configuração
trivy config ./Dockerfile

# Escanear diretório (IaC)
trivy config ./

Não quer configurar manualmente?

Implante o VPS em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

O daemon Docker roda como root no host por padrão. Um escape de container (vulnerabilidade no Docker ou kernel) com processo root dentro tem mais impacto. Mitigações: use Docker rootless mode (daemon sem root), Podman (rootless por padrão) ou user namespaces. Para VPS de produção simples: usuário não-root no container + no-new-privileges é suficiente.

Adicione tmpfs para diretórios que precisam de escrita: tmpfs: - /tmp - /var/run - /app/tmp. Identifique quais diretórios a aplicação precisa gravar com docker run --rm --read-only minha-imagem e inspecione os erros de permissão.

Nunca monte /var/run/docker.sock em containers de produção, quem controla o socket controla todos os containers (equivale a root no host). Se precisar de acesso ao Docker em containers (CI/CD): use Docker-in-Docker (dind) isolado ou soluções como Kaniko para build sem socket.

Nunca. A tag :latest muda sem aviso, um docker pull traz uma versão diferente da testada. Use sempre versões específicas: node:22.11.0-alpine3.20. Para segurança: rebuilde e redeploy regularmente com a versão pinada para incorporar patches de segurança das imagens base.

docker inspect CONTAINER | grep -E "Privileged|Capabilities|User" mostra o estado de segurança. docker bench security (projeto CIS Docker Benchmark) faz auditoria completa: docker run -it --net host --pid host --userns host --cap-add audit_control docker/docker-bench-security.