Como implementar aplicações com Docker Compose num VPS
Guia prático de produção do Docker Compose v2 no Ubuntu: instalar o motor, escrever um ficheiro Compose com redes e volumes, tratar de segredos, colocar um proxy inverso à frente, atualizar sem pânico e fazer cópias dos volumes com nome.

O Docker Compose é a forma habitual de correr uma pilha pequena num VPS: uma aplicação web, uma base de dados, uma cache Redis e um proxy inverso, descritos num ficheiro YAML. Não é Kubernetes. Não terá comutação multi-nó. Terá um arranjo repetível e revisto, que pode recriar em minutos num VPS Hiddence novo — exatamente o que a maioria dos projetos paralelos e produtos pequenos precisa.
Este artigo assume que quer algo mais próximo da produção do que um docker run numa sexta à noite. Vamos instalar o Docker Engine e o plugin Compose, criar um utilizador de implementação sem root, escrever um ficheiro Compose com healthchecks e políticas de reinício, manter os segredos fora do Git, publicar na internet só as portas do proxy inverso e definir uma rotina de atualização e cópia de segurança. A pilha de exemplo é uma aplicação web típica mais PostgreSQL mais Caddy, mas o mesmo padrão serve para Node, PHP, Python ou um conjunto de workers.
Porque o Compose num único VPS ainda faz sentido
As pessoas saltam para o Kubernetes porque está na moda e depois passam o fim de semana em YAML para um sítio. O Compose continua legível. Versiona o ficheiro, documenta variáveis de ambiente e pode fazer um diff da alteração antes de a aplicar. O isolamento chega: um contentor da aplicação comprometido não deveria deter o socket UNIX da base se usou redes e utilizadores com o mínimo de privilégio. Os limites de recursos impedem que uma fuga de memória congele todo o VPS.
- Um ficheiro descreve toda a pilha
- Os volumes com nome sobrevivem à recriação de contentores
- As redes internas do Docker mantêm o Postgres fora da internet pública
- restart: unless-stopped cobre a maioria dos reinícios
- Fácil de copiar para um segundo VPS quando o primeiro fica pequeno
Requisitos
Use um VPS com RAM suficiente para a aplicação mais o Postgres. Um plano de 2 GB é um mínimo realista para aplicação + base + proxy. Assume-se Ubuntu 24.04. Não instale o Docker a partir de um Snap qualquer sem ler o que faz aos cgroups; os passos abaixo usam o repositório apt oficial da Docker.
- Ubuntu 22.04/24.04, root ou sudo
- Recomendam-se 2 GB de RAM (1 GB só para pilhas minúsculas sem Postgres)
- Um domínio a apontar para o VPS se quiser HTTPS automático
- Git ou outra forma 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 antigo binário Python docker-compose. Instale ambos a partir do repositório da Docker para receber correções de segurança atuais.
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 versionPasso 2: Utilizador de implementação e estrutura de diretórios
Correr o Compose como root funciona, mas um utilizador dedicado no grupo docker é mais limpo. Coloque o projeto em /opt ou /srv, não em /root, para as cópias e permissões ficarem óbvias. Nunca deixe com escrita mundial o diretório que contém .env.
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/appPasso 3: Escrever um ficheiro Compose pensado para produção
O ficheiro abaixo é um modelo. Substitua a imagem da aplicação pela sua. O Postgres não é publicado em 0.0.0.0:5432 — só o Caddy é publicado nas 80 e 443. Os healthchecks impedem que um proxy inverso envie tráfego a uma aplicação que ainda está a migrar a base. Fixe as etiquetas de imagem; latest é como acontecem as quebras de surpresa.
# /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 as palavras-passe em .env no servidor, chmod 600, e nunca faça commit desse ficheiro. Gere palavras-passe com openssl rand -base64 32. O Caddyfile só precisa de fazer proxy inverso para o nome de serviço da aplicação na rede Docker.
# /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=100Passo 5: Firewall, registos e limites de recursos
O UFW só deve permitir 22, 80 e 443. O Docker por vezes contorna o UFW nas portas publicadas; se precisar de uma firewall de anfitrião rigorosa, 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 anfitrião à frente. Defina limites de memória no Compose para o Postgres não comer o VPS.
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 dockerPasso 6: Atualizações e reversões
A atualização aborrecida que funciona: obter imagens novas, up -d, vigiar healthchecks, manter a etiqueta anterior no git para poder voltar a fixá-la. Não execute docker compose down numa base de produção salvo se pretender deixar de aceitar tráfego. down -v apaga volumes — isso é uma limpeza, não um reinício.
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 volumesCópias de segurança dos volumes com nome
Uma instantânea do VPS é boa. Um despejo lógico do Postgres é melhor se precisar de restaurar noutra máquina. Agende isto com cron. Teste um restauro uma vez; caso contrário não tem cópias de segurança — tem ficheiros que espera que o sejam.
# 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 appResolução de problemas
Leia docker compose ps e docker compose logs service. Falhas ao obter imagens são normalmente limites de taxa do ghcr/Docker Hub ou uma imagem privada sem início de sessão. Um 502 do Caddy significa que a aplicação não está saudável ou que o nome de anfitrião do proxy está errado (use o nome de serviço Compose, não localhost, a partir do contentor Caddy). Erros de permissão em volumes significam muitas vezes que a imagem corre como uid 1000 enquanto o diretório do anfitrião é de root.
- compose: command not found — instalou o nome antigo do binário; use docker compose
- port is already allocated — outra coisa possui 80/443 (Apache, Nginx, outro Caddy)
- database connection refused — a aplicação arrancou antes de o Postgres estar 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 — utilizador fora do grupo docker, ou precisa de uma nova sessão de acesso
Segurança
Não publique portas de base de dados. Não use privileged: true. Não monte /var/run/docker.sock num contentor da aplicação salvo se estiver a escrever de propósito um gestor de contentores. Mantenha o motor atualizado. Analise as suas próprias imagens se as construir. .env no disco continua a ser um ficheiro secreto — restrinja quem pode entrar por SSH na máquina.
- Sem 5432 / 3306 / 6379 públicos
- Fixe digestos de imagem ou pelo menos etiquetas imutáveis
- chmod 600 .env
- Unattended-upgrades do sistema anfitrião ainda importam
- Um projeto Compose por aplicação reduz o raio de impacto
Dicas
- Coloque compose.yaml no git; mantenha .env.example sem segredos reais
- Use perfis para workers opcionais (docker compose --profile workers up -d)
- Observe docker stats quando dimensionar o VPS
- Montagens bind do código-fonte não são o padrão de produção — construa uma imagem
- Se a pilha crescer para vários VPS, então olhe para orquestração — não antes
Um arranjo Compose quase de produção num VPS é: pacotes oficiais Docker, um .env protegido, um YAML que não publica a base, healthchecks, um proxy inverso nas 80/443, UFW, etiquetas de imagem às quais pode voltar e despejos Postgres que restaurou de verdade uma vez. Parta do modelo deste artigo, substitua a imagem da aplicação e trate docker compose down -v como um comando destrutivo, com o mesmo respeito que rm -rf.