16 min de leitura · Guia técnico
Otimizar Docker Compose para liberar portas só para serviços internos significa publicar serviços sensíveis com bind em 127.0.0.1 ou usar expose na rede interna, em vez de abrir portas em todas as interfaces do host. Assim, bancos de dados, caches e backends continuam acessíveis entre containers ou localmente no servidor, sem ficarem expostos à internet.
- Identifique quais serviços precisam de acesso externo e quais devem ficar internos.
- Troque
PORTA:PORTApor127.0.0.1:PORTA:PORTAnos serviços locais ao host. - Use
exposepara comunicação entre containers na mesma rede Docker. - Valide a stack com
docker compose confige confira os binds comss -tlnp. - Recrie os containers com
docker compose up -de teste o acesso externo.
Pré-requisitos para otimizar o Docker Compose com portas restritas
- Docker Engine instalado (versão 24 ou superior recomendada) e Docker Compose v2 disponível via
docker compose(plugin integrado). - Acesso root ou usuário com permissão no grupo
dockerno servidor. - Um arquivo
docker-compose.ymlfuncional com ao menos dois serviços definidos. - Conhecimento básico de redes Docker (bridge, host, overlay).
- Servidor Linux — os exemplos foram testados no Debian 12 (Bookworm), Ubuntu 24.04 LTS e Rocky Linux 9, mas o conceito de otimizar o Docker Compose dessa forma também se aplica a versões mais novas em 2026.
- Opcional: UFW ou nftables configurado para entender a interação com o Docker.
Por que o Docker Compose expõe portas para a internet por padrão
Quando você usa a diretiva ports no formato simples "8080:80", o daemon do Docker cria uma regra no iptables que vincula a porta 8080 a todas as interfaces de rede do host — incluindo a interface pública. Esse comportamento é independente do UFW: o Docker insere suas regras na chain DOCKER antes das chains gerenciadas pelo UFW, contornando qualquer bloqueio configurado nele.
O resultado prático é que um banco de dados MySQL rodando na porta 3306 com a diretiva "3306:3306" fica acessível para qualquer IP na internet, mesmo que você tenha uma regra ufw deny 3306 ativa. Esse é um dos erros de segurança mais comuns em stacks Docker em produção.
Para entender melhor como o Docker se integra a um servidor VPS Linux, consulte o artigo Dicas de Otimização de Servidores Linux, que cobre ajustes de kernel e rede relevantes para esse cenário.
Como otimizar o Docker Compose com bind de host 127.0.0.1
A forma mais direta de impedir que uma porta seja exposta publicamente é vincular o bind de host à interface de loopback. Isso garante que somente processos rodando no próprio servidor consigam acessar aquela porta — containers, serviços locais como Nginx atuando como proxy reverso, ou conexões SSH tuneladas.
Veja um exemplo completo de docker-compose.yml com uma aplicação web (Node.js), um banco de dados (MySQL 8.0) e um cache (Redis):
services:
app:
image: node:20-alpine
ports:
- "0.0.0.0:3000:3000"
networks:
- interna
depends_on:
- db
- cache
db:
image: mysql:8.0
ports:
- "127.0.0.1:3306:3306"
environment:
MYSQL_ROOT_PASSWORD: senha_segura
MYSQL_DATABASE: minha_app
networks:
- interna
cache:
image: redis:7-alpine
ports:
- "127.0.0.1:6379:6379"
networks:
- interna
networks:
interna:
driver: bridge
Neste exemplo:
- O serviço
appestá acessível externamente na porta3000(bind em0.0.0.0). - O MySQL está vinculado apenas ao loopback — acessível via
127.0.0.1:3306no host, mas invisível para a internet. - O Redis segue o mesmo padrão: porta
6379disponível apenas localmente. - Todos os serviços compartilham a rede interna
interna, permitindo comunicação entre containers pelo nome do serviço.
Após salvar o arquivo, aplique a configuração:
docker compose up -d
Verifique quais portas estão abertas e em quais interfaces:
ss -tlnp | grep -E '3000|3306|6379'
LISTEN 0 128 0.0.0.0:3000 0.0.0.0:* users:(("docker-proxy",pid=12345,fd=4))
LISTEN 0 128 127.0.0.1:3306 0.0.0.0:* users:(("docker-proxy",pid=12346,fd=4))
LISTEN 0 128 127.0.0.1:6379 0.0.0.0:* users:(("docker-proxy",pid=12347,fd=4))
O output confirma que 3306 e 6379 estão vinculados apenas ao loopback, enquanto 3000 está acessível em todas as interfaces.
Como usar expose para otimizar o Docker Compose internamente
A diretiva expose é a alternativa mais segura quando dois containers precisam se comunicar, mas nenhuma porta deve ser publicada no host. Diferente de ports, o expose apenas documenta e disponibiliza a porta dentro da rede Docker interna — sem criar nenhuma regra no iptables do host.
Exemplo de stack onde o PHP-FPM se comunica com o MySQL sem expor nada externamente:
services:
php:
image: php:8.3-fpm
expose:
- "9000"
networks:
- backend
db:
image: mariadb:11.4
expose:
- "3306"
environment:
MARIADB_ROOT_PASSWORD: senha_forte
MARIADB_DATABASE: producao
networks:
- backend
nginx:
image: nginx:1.26-alpine
ports:
- "0.0.0.0:80:80"
- "0.0.0.0:443:443"
networks:
- backend
networks:
backend:
driver: bridge
Nesta configuração, o Nginx recebe requisições externas nas portas 80 e 443, repassa para o PHP-FPM via socket na porta 9000, e o PHP-FPM acessa o MariaDB pelo hostname db na porta 3306 — tudo dentro da rede backend, sem nenhuma dessas portas internas aparecer no host.
Para verificar que nenhuma porta interna foi publicada:
docker compose ps
NAME IMAGE COMMAND SERVICE STATUS PORTS
php php:8.3-fpm "docker-php-entrypoi…" php running 9000/tcp
db mariadb:11.4 "docker-entrypoint.s…" db running 3306/tcp
nginx nginx:1.26-alpine "/docker-entrypoint.…" nginx running 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
Observe que 9000/tcp e 3306/tcp aparecem sem o prefixo 0.0.0.0:, confirmando que são apenas portas internas da rede Docker.
Como liberar uma porta do Docker apenas para um IP específico
Em cenários onde um serviço precisa ser acessível por um IP externo específico — como um servidor de monitoramento ou um IP de escritório — o Docker Compose permite especificar o IP de destino diretamente na diretiva ports.
A sintaxe é: "IP_ESPECIFICO:PORTA_HOST:PORTA_CONTAINER"
services:
prometheus:
image: prom/prometheus:latest
ports:
- "203.0.113.10:9090:9090"
networks:
- monitoramento
grafana:
image: grafana/grafana:latest
ports:
- "203.0.113.10:3000:3000"
networks:
- monitoramento
networks:
monitoramento:
driver: bridge
Atenção: o IP 203.0.113.10 no exemplo acima é o IP do host de destino (a interface do servidor que receberá as conexões), não o IP de origem do cliente. Para filtrar por IP de origem, você ainda precisará de regras adicionais no firewall. Essa configuração restringe em qual interface do servidor a porta será vinculada.
Valide a configuração antes de aplicar:
docker compose config
O comando exibe o arquivo docker-compose.yml após interpolação de variáveis e validação de sintaxe, permitindo revisar tudo antes de subir os containers.
Usando redes Docker isoladas para segmentar serviços
Uma estratégia avançada de isolamento de portas no Docker Compose é criar múltiplas redes com propósitos distintos: uma rede pública para os serviços que precisam de acesso externo e uma rede privada para a comunicação entre backends. Containers em redes diferentes não conseguem se comunicar diretamente, mesmo que estejam na mesma stack.
services:
nginx:
image: nginx:1.26-alpine
ports:
- "0.0.0.0:80:80"
- "0.0.0.0:443:443"
networks:
- publica
- privada
app:
image: python:3.12-slim
expose:
- "8000"
networks:
- privada
db:
image: postgres:16-alpine
expose:
- "5432"
networks:
- privada
networks:
publica:
driver: bridge
privada:
driver: bridge
internal: true
O parâmetro internal: true na rede privada é crucial: ele impede que containers nessa rede façam requisições para fora do host, criando um isolamento bidirecional. O Nginx está em ambas as redes — recebe tráfego externo pela rede publica e repassa para a aplicação pela rede privada. O banco de dados PostgreSQL está apenas na rede privada e não tem nenhuma rota para a internet.
Para listar as redes criadas e verificar quais containers estão em cada uma:
docker network ls
docker network inspect nome_da_stack_privada
Problemas comuns e como resolver
Sintoma: porta ainda aparece acessível externamente após configurar 127.0.0.1
Causa: O container foi iniciado antes da alteração no docker-compose.yml, ou há um container antigo ainda rodando com a configuração anterior.
Solução: Pare completamente a stack e suba novamente, forçando a recriação dos containers:
docker compose down
docker compose up -d
Após reiniciar, confirme com ss -tlnp que o bind está correto. Se o problema persistir, verifique se há containers órfãos com docker ps -a e remova-os com docker rm -f CONTAINER_ID.
Sintoma: containers na mesma rede não conseguem se comunicar pelo nome do serviço
Causa: Os containers estão em redes diferentes, ou a resolução DNS interna do Docker não está funcionando corretamente. Isso ocorre com frequência quando se usa a rede padrão default sem definir redes explícitas.
Solução: Defina redes explícitas no docker-compose.yml e certifique-se de que ambos os serviços estão na mesma rede. Teste a resolução de nomes de dentro de um container:
docker compose exec app ping db
Se o ping falhar, verifique com docker network inspect se ambos os containers aparecem na mesma rede.
Sintoma: UFW bloqueou a porta, mas ela ainda está acessível externamente
Causa: O Docker manipula o iptables diretamente e insere regras na chain DOCKER, que tem precedência sobre as regras do UFW. Isso é um comportamento documentado do Docker e não um bug.
Solução: Não confie apenas no UFW para bloquear portas publicadas pelo Docker. Use o bind de host 127.0.0.1 no docker-compose.yml como primeira linha de defesa. Se precisar de controle granular via firewall, configure as regras diretamente no iptables na chain DOCKER-USER, que é respeitada pelo Docker:
iptables -I DOCKER-USER -p tcp --dport 3306 -j DROP
iptables -I DOCKER-USER -s 203.0.113.10 -p tcp --dport 3306 -j ACCEPT
Atenção: regras inseridas diretamente no iptables não persistem após reboot sem configuração adicional. Use o pacote iptables-persistent no Debian/Ubuntu para salvar as regras.
Sintoma: erro "bind: address already in use" ao subir a stack
Causa: Outro processo no host já está usando a porta especificada — pode ser um container anterior, um serviço do sistema (como MySQL nativo) ou outra instância do Docker Compose.
Solução: Identifique o processo que está usando a porta:
ss -tlnp | grep 3306
Se for um serviço do sistema, pare-o antes de subir o container: systemctl stop mysql. Se for outro container, remova-o com docker rm -f CONTAINER_ID.
Perguntas frequentes sobre Docker Compose e portas internas
Como impedir que o Docker Compose exponha uma porta para a internet?
No arquivo docker-compose.yml, use a sintaxe longa de ports com o bind de host 127.0.0.1:PORTA:PORTA em vez de apenas PORTA:PORTA. Isso faz o daemon do Docker vincular a porta somente à interface de loopback, tornando-a inacessível externamente mesmo que o firewall UFW esteja ativo. Essa é a abordagem mais confiável e não depende de regras de firewall externas para funcionar.
Qual a diferença entre ports e expose no Docker Compose?
A diretiva ports publica a porta no host, tornando-a acessível de fora do container — e potencialmente da internet, dependendo do bind configurado. A diretiva expose apenas documenta e disponibiliza a porta entre containers na mesma rede Docker interna, sem publicar nada no host. Para comunicação exclusivamente entre serviços da mesma stack, expose é a opção mais segura e semanticamente correta.
O Docker Compose ignora as regras do UFW?
Sim, por padrão o Docker manipula diretamente as regras do iptables e pode contornar o UFW ao publicar portas. O Docker insere suas regras na chain DOCKER, que tem precedência sobre as chains gerenciadas pelo UFW. Por isso, vincular portas a 127.0.0.1 no docker-compose.yml é a forma mais confiável de restringir o acesso, independentemente das regras do UFW. Para controle via firewall, use a chain DOCKER-USER.
Como dois containers se comunicam sem expor portas para o host?
Basta colocar ambos os containers na mesma rede Docker definida no docker-compose.yml e usar o nome do serviço como hostname. Por exemplo, um container PHP pode acessar o MySQL pelo hostname db na porta 3306 sem que essa porta seja publicada no host. O Docker possui um servidor DNS interno que resolve automaticamente os nomes de serviço para os IPs dos containers na mesma rede.
É possível liberar uma porta do Docker apenas para um IP específico?
Sim. Na diretiva ports do docker-compose.yml, use o formato IP_ESPECIFICO:PORTA_HOST:PORTA_CONTAINER, por exemplo 203.0.113.10:8080:80. Isso vincula a porta à interface do host que possui aquele IP, restringindo em qual interface as conexões serão aceitas. Para filtrar por IP de origem do cliente, combine essa configuração com regras na chain DOCKER-USER do iptables.
Conclusão
Controlar quais portas o Docker Compose expõe é uma das práticas de segurança mais importantes para stacks em produção. Ao aplicar as técnicas descritas neste guia para otimizar o Docker Compose, você garante que apenas os serviços que realmente precisam de acesso externo fiquem visíveis na internet.
- Use
127.0.0.1:PORTA:PORTApara qualquer serviço que deva ser acessível apenas localmente no host (bancos de dados, caches, filas de mensagens). - Prefira
exposeaportspara comunicação entre containers na mesma stack — é mais seguro e não cria regras noiptablesdo host. - Crie redes Docker separadas com
internal: truepara isolar completamente backends sensíveis, impedindo que containers façam conexões de saída para a internet.
Para aprofundar o conhecimento em segurança de servidores Linux com Docker, veja também o artigo Acessando servidores VPS Linux da AviraHost, que cobre boas práticas de acesso SSH ao ambiente onde sua stack Docker roda.
Leia também
- Otimizar Docker Compose e Nginx no mesmo servidor AlmaLinux 9
- configurar rede Docker para isolar serviços no VPS
- Entenda o Checklist de Segurança do Docker antes de ir ao ar
Precisa de ajuda com Docker e infraestrutura de containers?
Configurar stacks Docker com segurança exige um servidor com recursos adequados e suporte técnico disponível quando você precisar. Na AviraHost, os planos de VPS Linux oferecem acesso root completo, IPs dedicados e suporte para você rodar suas aplicações em containers com controle total sobre a rede e as portas expostas.