Docker Compose no Ubuntu 24.04: guia para VPS
Instale Docker Engine e Compose no Ubuntu 24.04, crie uma stack Miniflux com PostgreSQL, evite a armadilha do ufw e faça backup dos volumes.
O que você vai criar
O Docker Compose é a base de praticamente tudo o que existe neste site. Nextcloud, Vaultwarden, n8n, Immich, Rocket.Chat: todos esses guias começam com “escreva este ficheiro compose”, e esta é a página que explica o que esse ficheiro realmente significa. Você vai instalar o Docker Engine e o plugin Compose a partir do repositório apt oficial do Docker no Ubuntu 24.04. Em seguida, vai criar uma stack real com dois serviços: o Miniflux, um leitor de RSS pequeno, e o PostgreSQL. Essa combinação exercita todos os padrões usados pelas aplicações maiores: imagens fixadas, uma base de dados com healthcheck, um volume nomeado, segredos num ficheiro .env e uma porta publicada apenas para localhost.
A instalação demora cinco minutos. O restante deste guia aborda os problemas que causam dificuldades mais tarde: o grupo docker ser root com outro nome, as portas publicadas ignorarem diretamente as regras do ufw e a única flag em docker compose down que elimina a sua base de dados sem pedir confirmação.
Pré-requisitos: uma VPS KVM Ubuntu 24.04 nova, um utilizador com sudo e pelo menos um gigabyte de RAM. Uma instalação existente do Docker também é aceitável; a primeira secção explica o que deve ser removido.
Instale a partir do repositório do Docker, não do Ubuntu
Há dois caminhos errados que deve rejeitar antes do primeiro comando. O pacote docker.io do próprio Ubuntu funciona, mas fica atrás das versões lançadas pelo Docker e não inclui a estrutura de plugins que todo o restante pressupõe. O comando com hífen, docker-compose, é o caminho indicado por tutoriais antigos. Num servidor antigo, esse comando normalmente corresponde ao Compose v1, escrito em Python, cujo suporte terminou oficialmente no Docker e que faz esses tutoriais falharem. O Docker ainda publica um binário autónomo docker-compose compilado a partir do código Go atual, mas a própria página de instalação classifica esse método como não recomendado e suportado apenas por compatibilidade retroativa. Atualmente, o Compose é docker compose com um espaço, um plugin da CLI instalado a partir do mesmo repositório que o engine.
Se algum desses componentes já estiver instalado no servidor, remova-o primeiro, incluindo docker-compose-v2 e docker-buildx, os pacotes dos plugins fornecidos pelo Ubuntu, para que tudo venha do mesmo repositório:
sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runcPackage 'docker.io' is not installed, so not removed é o resultado normal numa VPS nova. Em seguida, adicione o repositório do Docker e instale:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo 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" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginVerifique as três camadas:
docker --version
docker compose version
sudo docker run --rm hello-worldAs duas primeiras linhas mostram as versões. Docker Compose version v5.x.x confirma que tem o plugin, e não o binário v1 sem suporte. O plugin saltou diretamente da v2 para a v5 em dezembro de 2025. As versões 3 e 4 foram omitidas para evitar confusão com as versões antigas do formato dos ficheiros Compose. Por isso, os guias que mostram v2.x.x referem-se à mesma ferramenta. O comando hello-world deve terminar com Hello from Docker!. O pacote ativa o serviço no arranque do sistema. systemctl is-enabled docker mostra enabled.
O grupo docker equivale a root; decida de forma consciente
Neste momento, todos os comandos docker precisam de sudo, porque o socket do daemon em /var/run/docker.sock pertence a root e ao grupo docker. Sem ser membro desse grupo, verá o erro do Docker mais pesquisado:
permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sockA correção padrão:
sudo usermod -aG docker $USERA associação ao grupo só é aplicada no início da sessão, por isso o erro persiste na shell atual. Execute newgrp docker nesta sessão ou termine a sessão e volte a iniciar; depois, id deverá listar docker entre os seus grupos.
Agora, a parte importante, de forma direta: ser membro do grupo docker equivale a ter root no host. Não é "quase root", nem acesso "elevado": é root. Qualquer pessoa nesse grupo pode executar docker run --rm -it -v /:/host alpine chroot /host e assumir o controlo de todo o sistema de ficheiros, sem ser solicitada uma palavra-passe. O grupo existe por conveniência, não para contenção.
O modo rootless do Docker é a alternativa real: o próprio daemon é executado pelo seu utilizador sem privilégios. Há custos: as portas abaixo de 1024 precisam de configuração adicional, a rede passa por um shim em userspace com sobrecarga mensurável e algumas imagens comportam-se de forma incorreta sem root real. Num VPS com um único administrador, em que o único login já tem sudo, a alteração do grupo não muda nada na prática. É essa a configuração assumida por todos os guias aqui, mas nunca atribua este acesso como se fosse menos privilegiado do que sudo.
Anatomia de um ficheiro Compose
Dê a cada stack o seu próprio diretório. O nome do diretório torna-se o nome do projeto e é usado como prefixo dos contentores, redes e volumes:
sudo mkdir -p /opt/miniflux && sudo chown $USER /opt/miniflux && cd /opt/minifluxCrie compose.yml (o nome moderno; docker-compose.yml continua a funcionar). Não use a chave antiga version:. Está obsoleta e o Compose emite um aviso quando a encontra.
services:
miniflux:
image: miniflux/miniflux:2.2.9
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
- DATABASE_URL=postgres://miniflux:${POSTGRES_PASSWORD}@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=miniflux
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "miniflux", "-d", "miniflux"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:Cada linha acima representa uma decisão. Analise-as uma de cada vez.
Fixe as versões das imagens: :latest com um pull é uma atualização não supervisionada
postgres:16-alpine, não postgres:latest. Uma tag não é fixa: :latest é resolvido novamente para o que o maintainer publicou mais recentemente, sempre que faz um pull. Combine isso com o procedimento de atualização que vai aprender, docker compose pull && docker compose up -d, e :latest significa que saltos de versão principal serão aplicados quando o upstream os publicar, não quando escolher. Com PostgreSQL, isto não é uma hipótese: um salto inesperado de 16 para 17 deixa o contentor num ciclo contínuo de falhas, com um diretório de dados incompatível, porque as atualizações de versão principal do Postgres exigem dump e restore, não um restart.
Fixe pelo menos a versão principal (postgres:16-alpine acompanha as versões de correção 16.x) e fixe as aplicações numa release exata, como miniflux/miniflux:2.2.9. Consulte a página de releases do projeto e use a versão atual no momento em que escrever o ficheiro. Uma atualização passa então a ser uma edição de uma linha, feita deliberadamente e visível em git diff.
Publique em 127.0.0.1, porque o Docker contorna o ufw
"127.0.0.1:8080:8080": endereço do host, porta do host, porta do contentor. A maioria dos tutoriais escreve "8080:8080", que é uma abreviatura de 0.0.0.0:8080:8080: escutar em todas as interfaces, incluindo a interface pública.
Este é o problema, e acontece a quase toda a gente pelo menos uma vez. O Docker publica uma porta através de uma regra DNAT que reescreve o destino do pacote para o IP interno do contentor antes da filtragem. Assim, o pacote segue o caminho FORWARD e nunca passa por INPUT, onde estão as regras do ufw. sudo ufw deny 8080 indica sucesso, ufw status mostra a porta como bloqueada e o serviço continua a responder à Internet inteira. A firewall não está avariada; está a ser contornada por desenho. Porque o Docker contorna o ufw e como filtrar realmente o tráfego dos contentores explica o mecanismo e a correção DOCKER-USER para as portas que têm de permanecer públicas.
O procedimento que elimina o problema: associe as portas publicadas a 127.0.0.1, salvo se tiver uma razão específica para não o fazer, e coloque um reverse proxy à frente de tudo o que deva estar exposto ao exterior. É exatamente isso que o guia do reverse proxy Traefik configura como passo seguinte a esta página: um contentor que controla as portas 80 e 443 e encaminha o tráfego para os restantes serviços pelo nome do host, com TLS. (Se vem de uma configuração Traefik v2 antiga, o guia de migração do Traefik v2 para o v3 explica as renomeações e alterações das regras.)
Confirme a associação depois de iniciar a stack: sudo ss -tlnp | grep 8080 deve mostrar 127.0.0.1:8080, e não 0.0.0.0:8080 nem *:8080.
Volumes nomeados e bind mounts
db-data:/var/lib/postgresql/data é um volume nomeado: o Docker cria e gere um diretório em /var/lib/docker/volumes/ e monta-o no contentor. A alternativa é um bind mount, ./data:/var/lib/postgresql/data, que associa um caminho escolhido no host.
A divisão que funciona na prática é esta: volumes nomeados para dados acedidos apenas pelos contentores, sobretudo bases de dados, porque o Docker inicializa o volume com o proprietário esperado pela imagem e as permissões funcionam sem ajustes. Bind mounts para ficheiros que edita a partir do host, como ficheiros de configuração editados num editor de texto, uma biblioteca multimédia para a qual faz rsync, ou qualquer conteúdo cujo caminho queira tornar explícito. A falha clássica dos bind mounts está nas permissões: a imagem executa o processo com um UID fixo (a imagem Miniflux usa o UID 65534, postgres:16-alpine muda para o UID 70), o diretório do host pertence ao UID 1000 e a aplicação falha no arranque com permission denied nos logs. Os volumes nomeados eliminam na maioria dos casos este tipo de problema, ao custo de os dados ficarem num caminho gerido pelo Docker, explicado abaixo.
environment e .env: mantenha os secrets fora do git
O Compose preenche ${POSTGRES_PASSWORD} a partir de um ficheiro chamado .env, localizado junto de compose.yml, mas apenas quando a shell não define já essa variável: um valor exportado no ambiente da shell tem precedência sobre o ficheiro .env. Um export POSTGRES_PASSWORD=... esquecido de uma experiência anterior pode, por isso, substituir silenciosamente o valor do ficheiro, e docker compose config (abaixo) mostra qual dos valores foi usado. Crie o ficheiro:
cat > .env <<'EOF'
POSTGRES_PASSWORD=change-me-to-something-long
ADMIN_PASSWORD=change-me-too
EOF
chmod 600 .env
echo ".env" >> .gitignoreGere valores reais com openssl rand -hex 24. Use hexadecimal, não base64, de propósito: esta password entra na string de ligação DATABASE_URL, e os caracteres /, + e = produzidos pelo base64 quebram a análise do URL. A falha aparece como um erro de autenticação, não como um erro de sintaxe, e pode consumir uma noite inteira. A linha .gitignore deve ser adicionada antes do primeiro commit: o ficheiro Compose pode ser publicado e versionado com segurança, mas o ficheiro .env nunca deve ser publicado. Um secret que entrou no histórico do git é um secret que deve ser substituído. Se iniciar a stack com uma variável em falta, o Compose emite um aviso explícito e continua com uma string vazia. Para uma password do Postgres, isso resulta numa implementação avariada:
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.docker compose config apresenta o ficheiro totalmente interpolado. É a forma mais rápida de confirmar o que os contentores vão realmente receber. Lembre-se de que a saída inclui os seus secrets.
depends_on não espera por nada, a menos que adicione um healthcheck
Um depends_on: [db] simples controla apenas a ordem de arranque: o Compose inicia primeiro o Postgres e a aplicação um momento depois, enquanto o Postgres ainda precisa de alguns segundos para aceitar ligações. A aplicação tenta aceder à base de dados, falha e termina ou repete a tentativa, conforme a qualidade da sua implementação.
A versão fiável é a usada pelo ficheiro acima: o serviço db define um healthcheck (o Postgres inclui pg_isready precisamente para este fim), e a aplicação declara depends_on com condition: service_healthy. O Compose inicia a base de dados, verifica o estado a cada 10 segundos e só inicia o Miniflux depois de a verificação passar. Se a base de dados nunca ficar saudável, por exemplo devido a uma password incorreta ou a um volume corrompido, a aplicação não inicia e o Compose indica qual dependência falhou:
dependency failed to start: container miniflux-db-1 is unhealthyEssa mensagem direciona-o para docker compose logs db, onde está o erro real.
restart: unless-stopped
restart: unless-stopped em ambos os serviços significa que os contentores voltam a iniciar depois de uma falha e depois de um reboot do VPS, mas permanecem parados se tiver executado deliberadamente docker compose stop. A alternativa always volta a iniciar os contentores mesmo depois de uma paragem manual, o que raramente é pretendido. Sem uma política de restart, um reboot às 4 a.m. causado por uma atualização do kernel interrompe silenciosamente os serviços até que dê pelo problema.
Os comandos do dia a dia
As tarefas diárias resumem-se a cinco comandos, executados a partir do diretório do projeto.
docker compose up -d # create and start; idempotent, recreates only what changed
docker compose ps # status, ports, and health of this project's containers
docker compose logs -f miniflux # follow one service's logs; --tail 100 for recent history
docker compose pull && docker compose up -d # upgrade to the pinned tags
docker compose down # stop and remove containers and the networkup -d pode ser executado repetidamente com segurança. Compara o ficheiro com o estado real e só altera os serviços cuja configuração ou imagem tenha mudado. O par de comandos de atualização obtém o que as tags fixadas apontam atualmente: versões de correção sob postgres:16-alpine; para uma versão fixada exatamente, não obtém nada até editar essa versão, que é precisamente o objetivo. As imagens antigas acumulam-se depois das atualizações. Recupere espaço em disco com docker image prune -f.
Agora, o comando destrutivo, com toda a clareza: docker compose down é seguro, porque os contentores e a rede são descartáveis, e os seus dados estão no volume. docker compose down -v também elimina os volumes nomeados. Isso elimina a sua base de dados instantaneamente, sem qualquer pedido de confirmação e sem possibilidade de desfazer a operação. A opção -v existe para desmontar experiências. Numa stack que contenha dados reais, trate-a como trata rm -rf. Não existe uma reciclagem sob /var/lib/docker/volumes/.
Para abrir uma shell única dentro de um contentor em execução: docker compose exec db psql -U miniflux abre uma sessão na base de dados e docker compose exec miniflux sh abre uma shell na aplicação.
Onde os seus dados estão realmente armazenados
Os volumes nomeados recebem o prefixo do projeto. Por isso, db-data num diretório chamado miniflux torna-se miniflux_db-data:
docker volume ls
docker volume inspect miniflux_db-dataA saída de inspect inclui a linha relevante:
"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"Esse diretório contém a base de dados, pertence a root no sistema de ficheiros do host e sobrevive a down, atualizações e recriações do contentor. É exatamente esse diretório que as suas cópias de segurança têm de incluir.
Fazer backup de um volume nomeado
O padrão habitual é usar um contentor descartável que monte o volume em modo só de leitura junto de um diretório do host e crie um tar entre os dois:
docker run --rm \
-v miniflux_db-data:/data:ro \
-v "$PWD":/backup \
alpine:3.22 tar czf /backup/miniflux-db-$(date +%F).tar.gz -C /data .Não é necessária nenhuma instalação e não fica nada em execução. A restauração é a operação inversa: tar xzf para um volume vazio novo, invertendo as mesmas montagens.
Existe uma ressalva para bases de dados: criar um tar de um diretório de dados do Postgres em execução pode capturar um estado a meio de uma escrita, que pode não iniciar corretamente. Use docker compose stop durante os segundos necessários para criar o tar ou, melhor ainda, faça um dump lógico, que é consistente por definição:
docker compose exec -T db pg_dump -U miniflux miniflux | gzip > miniflux-$(date +%F).sql.gzA opção -T desativa o pseudo-terminal que o Compose aloca por padrão. Encaminhar a saída do dump através de um TTY pode corrompê-la. Coloque uma destas opções no cron e copie o resultado para fora do VPS. Um backup no mesmo disco dos dados que protege é uma cópia, não um backup. O guia do Nextcloud cria uma rotina agendada completa com base exatamente nestes dois padrões.
Modos de falha e as mensagens que verá
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock, ainda não está no grupo docker ou a sessão é anterior à alteração. id mostra os seus grupos efetivos; newgrp docker corrige a shell atual. Terminar a sessão e iniciar sessão novamente corrige todas as shells.
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?, é outro problema: o daemon está parado. sudo systemctl status docker e sudo journalctl -u docker -n 50 indicam a causa. Numa VPS, a causa clássica é o disco cheio; execute primeiro df -h /var/lib/docker.
Bind for 127.0.0.1:8080 failed: port is already allocated, outro contentor já publicou essa porta do host. docker ps mostra qual; um contentor antigo de um docker run experimental de há algumas semanas é normalmente o culpado. Se docker ps não devolver resultados, um processo que não pertence ao Docker está a ocupar a porta: sudo ss -tlnp | grep 8080 identifica-o.
yaml: line 14: did not find expected key, existe um erro de indentação na linha indicada ou imediatamente acima dela. Os ficheiros Compose são YAML: use indentação de dois espaços, apenas espaços, e nenhum caráter de tabulação. Uma tabulação em qualquer ponto causa uma falha. docker compose config valida o ficheiro sem iniciar nada. Executá-lo depois de cada alteração é uma prática simples e útil.
A surpresa do ufw não apresenta qualquer erro. É isso que a torna perigosa: a implementação funciona, ufw status parece correto e uma análise de portas a partir do exterior encontra a sua base de dados. Reveja a secção de portas acima, verifique cada entrada ports: para confirmar se não falta o prefixo 127.0.0.1: e confirme a partir de uma máquina diferente com curl http://your-vps-ip:8080. A resposta que pretende é connection refused.
A partir daqui, o guia do Traefik transforma esta stack única em várias aplicações atrás de um único ponto de entrada HTTPS, e o que vale a pena alojar por conta própria em 2026 é a lista de serviços a implementar através dela. Quando várias dessas stacks estiverem em execução e cada uma tiver criado o seu próprio formulário de início de sessão, um servidor SSO alojado por conta própria, como o Authentik reúne-as novamente numa única conta atrás do mesmo proxy.
Um servidor de jogos, como um servidor Minecraft numa VPS, é um bom primeiro projeto Compose para praticar. Se preferir aprender com algo que abre todos os dias, o openGym, um gestor de treinos autoalojado, é uma stack fixada numa tag Git, e não numa tag de imagem. Este serviço requer TLS à frente antes de registar a primeira passkey. As fotografias são normalmente o primeiro tipo de dados que se quer retirar da cloud de terceiros, e a comparação entre PhotoPrism e Immich ajuda a determinar a quantidade mínima de RAM e a rotina de backups que terá de manter antes de associar um volume a qualquer um dos dois. Quando dois serviços já não forem suficientes, instalar o AFFiNE como um workspace ao estilo do Notion aplica os mesmos padrões a quatro contentores e testa se as tags fixadas, os healthchecks e os volumes nomeados acima já se tornaram um hábito.
FAQ
Porque recebo "permission denied while trying to connect to the Docker daemon socket"?
O seu utilizador não pertence ao grupo docker, ou foi adicionado depois de a sessão atual ter começado. A associação ao grupo só se aplica no início da sessão. Execute sudo usermod -aG docker $USER e depois newgrp docker, ou termine a sessão e inicie-a novamente. Confirme com id. O grupo concede acesso equivalente ao de root ao sistema anfitrião. Por isso, adicione apenas utilizadores a quem também concederia sudo.
O comando docker compose down apaga os meus dados?
O comando docker compose down simples não apaga os dados. Remove os contentores e a rede do projeto. Os volumes nomeados permanecem, e o comando up -d seguinte volta a associá-los. docker compose down -v é a forma destrutiva. Remove os volumes nomeados, incluindo a base de dados, sem confirmação e sem possibilidade de desfazer a operação. Nunca execute -v numa stack com dados reais sem ter uma cópia de segurança verificada.
Qual é a diferença entre docker-compose e docker compose?
docker-compose, com hífen, é a forma antiga de executar o Compose. Em servidores antigos, normalmente corresponde ao Compose v1, a implementação original em Python cujo suporte terminou oficialmente no Docker. O Docker ainda publica um binário autónomo docker-compose compilado a partir do código Go atual, mas classifica essa instalação como não recomendada e suportada apenas por compatibilidade retroativa. Por isso, não a use em servidores novos. docker compose, com espaço, é o Compose atual, da v2 em diante e numerado como v5 desde dezembro de 2025. É um plugin Go para a CLI do Docker, instalado como docker-compose-plugin a partir do repositório apt do Docker. Os comandos e os ficheiros YAML são quase totalmente compatíveis. Assim, quando um tutorial antigo indicar docker-compose up, escreva docker compose up.
Porque consigo aceder ao meu contentor Docker a partir da Internet, embora o ufw bloqueie a porta?
Porque o Docker publica as portas com regras DNAT na cadeia PREROUTING do iptables. Os pacotes reescritos seguem o caminho FORWARD através das próprias cadeias do Docker e nunca passam pela cadeia INPUT, onde se aplicam as regras do ufw. Por isso, ufw deny 8080 não tem efeito sobre uma porta publicada de um contentor. Corrija o problema na origem: publique em 127.0.0.1: e exponha os serviços através de um reverse proxy.
Devo usar um volume nomeado ou uma montagem bind?
Use volumes nomeados para dados que só o contentor manipula, especialmente bases de dados. O Docker define a propriedade esperada pela imagem, e as permissões funcionam sem configuração adicional. Use montagens bind para ficheiros que também manipula a partir do sistema anfitrião: configurações que edita, ficheiros multimédia que carrega e qualquer conteúdo cujo caminho queira tornar explícito. Se um contentor falhar no arranque com permission denied numa montagem bind, a primeira verificação deve ser a incompatibilidade entre os UID do sistema anfitrião e do contentor.