Voltar ao blog
Agosto 19, 2026Guias

Como implantar aplicações com Docker Compose em um VPS

Guia prático de produção do Docker Compose v2 no Ubuntu: instalar o engine, escrever um arquivo Compose com redes e volumes, cuidar de segredos, colocar um proxy reverso na frente, atualizar sem pânico e fazer backup dos volumes nomeados.

Como implantar aplicações com Docker Compose em um VPS

Docker Compose é o jeito usual de rodar uma pilha pequena num VPS: um app web, um banco, um cache Redis e um proxy reverso, descritos num arquivo YAML. Não é Kubernetes. Você não vai ter failover multi-nó. Vai ter um setup repetível e revisável, que recria em minutos num VPS Hiddence novo — exatamente o que a maioria dos side projects e produtos pequenos precisa.

Este artigo assume que você quer algo mais perto de produção do que um docker run numa sexta à noite. Vamos instalar o Docker Engine e o plugin Compose, criar um usuário de deploy sem root, escrever um arquivo Compose com healthchecks e políticas de restart, manter os segredos fora do Git, publicar na internet só as portas do proxy reverso e definir uma rotina de atualização e backup. A pilha de exemplo é um app web típico mais PostgreSQL mais Caddy, mas o mesmo padrão vale para Node, PHP, Python ou um bando de workers.

Por que Compose num VPS só ainda faz sentido

O pessoal pula para Kubernetes porque está na moda e depois passa o fim de semana em YAML para um site. O Compose continua legível. Você versiona o arquivo, documenta variáveis de ambiente e consegue fazer diff da mudança antes de aplicar. O isolamento basta: um container do app comprometido não deveria segurar o socket UNIX do banco se você usou redes e usuários com o mínimo de privilégio. Limites de recurso impedem que um vazamento de memória congele o VPS inteiro.

  • Um arquivo descreve a pilha inteira
  • Volumes nomeados sobrevivem à recriação de containers
  • Redes internas do Docker mantêm o Postgres fora da internet pública
  • restart: unless-stopped cobre a maioria dos reboots
  • Fácil de copiar para um segundo VPS quando o primeiro fica apertado

Requisitos

Use um VPS com RAM suficiente para o app mais o Postgres. Um plano de 2 GB é um mínimo realista para app + banco + proxy. Assume-se Ubuntu 24.04. Não instale o Docker de um Snap qualquer sem ler o que ele faz com cgroups; os passos abaixo usam o repositório apt oficial da Docker.

  • Ubuntu 22.04/24.04, root ou sudo
  • 2 GB de RAM recomendados (1 GB só para pilhas minúsculas sem Postgres)
  • Um domínio apontando para o VPS se você quiser HTTPS automático
  • Git ou outro jeito de copiar o projeto para o servidor

Passo 1: Instalar o Docker Engine e o Compose v2

O Compose v2 é um plugin invocado como docker compose (com um espaço), não o binário Python antigo docker-compose. Instale os dois a partir do repositório da Docker para receber correções de segurança atuais.

bash
ssh root@YOUR_VPS_IP
apt update && apt -y install ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

apt update
apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

docker version
docker compose version

Passo 2: Usuário de deploy e layout de diretórios

Rodar Compose como root funciona, mas um usuário dedicado no grupo docker é mais limpo. Coloque o projeto em /opt ou /srv, não em /root, para backups e permissões ficarem óbvios. Nunca deixe com escrita mundial o diretório que contém .env.

bash
adduser --disabled-password --gecos '' deploy
usermod -aG docker deploy
mkdir -p /srv/app
chown deploy:deploy /srv/app
chmod 750 /srv/app

# Log in as deploy for the rest of the file editing:
# su - deploy
# cd /srv/app

Passo 3: Escrever um arquivo Compose pensado para produção

O arquivo abaixo é um modelo. Troque a imagem do app pela sua. O Postgres não é publicado em 0.0.0.0:5432 — só o Caddy sai nas 80 e 443. Healthchecks impedem que um proxy reverso mande tráfego para um app que ainda está migrando o banco. Fixe as tags de imagem; latest é como surgem as quebras de surpresa.

bash
# /srv/app/compose.yaml
services:
  caddy:
    image: caddy:2.8-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      app:
        condition: service_healthy

  app:
    image: ghcr.io/example/webapp:1.4.2
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
      interval: 10s
      timeout: 3s
      retries: 10
    networks:
      - frontend
      - backend

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    env_file: .env
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - backend

networks:
  frontend: {}
  backend: {}

volumes:
  pgdata:
  caddy_data:
  caddy_config:

Passo 4: Segredos, .env e Caddyfile

Coloque senhas em .env no servidor, chmod 600, e nunca faça commit desse arquivo. Gere senhas com openssl rand -base64 32. O Caddyfile só precisa fazer proxy reverso para o nome de serviço do app na rede Docker.

bash
# /srv/app/.env (permissions 600)
POSTGRES_USER=app
POSTGRES_PASSWORD=change-me-long-random
POSTGRES_DB=app
# plus any APP_SECRET / NEXTAUTH_SECRET your image needs

# /srv/app/Caddyfile
app.example.com {
    encode gzip
    reverse_proxy app:8000
}

chmod 600 /srv/app/.env
cd /srv/app
docker compose up -d
docker compose ps
docker compose logs -f --tail=100

Passo 5: Firewall, logs e limites de recurso

O UFW deve liberar só 22, 80 e 443. O Docker às vezes contorna o UFW nas portas publicadas; se você precisa de um firewall de host rígido, consulte a interação atual Docker + UFW da sua versão do Ubuntu ou publique portas só em 127.0.0.1 e coloque um Caddy de host na frente. Defina limites de memória no Compose para o Postgres não comer o VPS.

bash
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

# Optional in compose.yaml under app or db:
#    deploy:
#      resources:
#        limits:
#          memory: 512M

# Logs (do not let json-file grow forever):
# { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
# in /etc/docker/daemon.json then: systemctl restart docker

Passo 6: Atualizações e rollbacks

A atualização chata que funciona: puxar imagens novas, up -d, olhar os healthchecks, guardar a tag anterior no git para poder voltar a fixá-la. Não rode docker compose down num banco de produção a menos que você queira parar de aceitar tráfego. down -v apaga volumes — isso é wipe, não restart.

bash
cd /srv/app
git pull   # if the Compose file lives in git

# Change the image tag in compose.yaml, then:
docker compose pull
docker compose up -d
docker compose ps

# Rollback: set the old tag, pull, up -d again
# NEVER: docker compose down -v   # this deletes named volumes

Fazer backup dos volumes nomeados

Um snapshot do VPS é bom. Um dump lógico do Postgres é melhor se você precisa restaurar em outra máquina. Agende isso com cron. Teste um restore uma vez; senão você não tem backups — tem arquivos que espera que sejam backups.

bash
# Postgres dump while the db container is running:
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gz

# Copy off-box
# scp /var/backups/app-*.sql.gz backup-host:~

# Restore sketch (maintenance window):
# gunzip -c app-2026-08-19.sql.gz | docker compose exec -T db psql -U app app

Solução de problemas

Leia docker compose ps e docker compose logs service. Falhas no pull de imagem costumam ser limite de taxa do ghcr/Docker Hub ou uma imagem privada sem login. Um 502 do Caddy significa que o app não está healthy ou que o hostname do proxy está errado (use o nome de serviço do Compose, não localhost, de dentro do container Caddy). Erros de permissão em volumes muitas vezes significam que a imagem roda como uid 1000 enquanto o diretório do host é do root.

  • compose: command not found — você instalou o nome antigo do binário; use docker compose
  • port is already allocated — outra coisa é dona de 80/443 (Apache, Nginx, outro Caddy)
  • database connection refused — o app subiu antes de o Postgres ficar pronto; use healthcheck + condição depends_on
  • disk full — docker system df, depois prune cuidadoso de imagens não usadas
  • permission denied on docker.sock — usuário fora do grupo docker, ou você precisa de uma nova sessão de login

Segurança

Não publique portas de banco. Não use privileged: true. Não monte /var/run/docker.sock num container de app a menos que você esteja escrevendo de propósito um gerenciador de containers. Mantenha o engine atualizado. Escaneie as suas próprias imagens se você as constrói. .env no disco continua sendo um arquivo secreto — restrinja quem pode entrar por SSH na máquina.

  • Sem 5432 / 3306 / 6379 públicos
  • Fixe digests de imagem ou pelo menos tags imutáveis
  • chmod 600 .env
  • Unattended-upgrades do SO host ainda importam
  • Um projeto Compose por app reduz o raio de explosão

Dicas

  • Coloque compose.yaml no git; mantenha .env.example sem segredos reais
  • Use profiles para workers opcionais (docker compose --profile workers up -d)
  • Olhe docker stats quando for dimensionar o VPS
  • Bind mounts de código-fonte não são o padrão de produção — faça o bake de uma imagem
  • Se a pilha crescer para vários VPS, aí sim olhe orquestração — não antes

Um setup Compose quase de produção num VPS é: pacotes oficiais Docker, um .env travado, um YAML que não publica o banco, healthchecks, um proxy reverso nas 80/443, UFW, tags de imagem para as quais você pode voltar e dumps Postgres que você realmente restaurou uma vez. Comece pelo modelo deste artigo, troque a imagem do app e trate docker compose down -v como um comando destrutivo, com o mesmo respeito de rm -rf.