Módulo 9 · 9.3
Docker compose e a firewall
Módulo 9: Docker e docker compose · Tempo de leitura: 12 minutos
Objetivo de aprendizagem: No final desta lição, saberá descrever serviços num ficheiro compose, iniciá-los, atualizá-los e pará-los, e saberá explicar porque é que as portas publicadas pelo Docker contornam o ufw e como evitar isso.
Conceitos-chave: compose.yaml, services, image, ports, volumes, environment, .env, restart, networks, depends_on, healthcheck, docker compose up -d, pull, logs, down, version: obsoleto, Docker e ufw, DOCKER-USER, publicar em 127.0.0.1, segredos, docker.sock
Porquê o compose?
Com docker run (lição 9.2) inicia um único contentor com uma longa linha de opções. Uma aplicação real é muitas vezes composta por vários contentores: um programa, uma base de dados, um proxy inverso. Memorizar essas linhas e voltar a escrevê-las após um reinício ou uma atualização acaba, mais cedo ou mais tarde, por correr mal.
O Docker Compose descreve todos os contentores de uma aplicação num único ficheiro: compose.yaml. Coloca esse ficheiro numa pasta própria, por exemplo /srv/status/compose.yaml, e gere tudo com docker compose. O ficheiro é ao mesmo tempo a sua documentação: o que está em execução, que imagem, que portas, onde estão os dados. Inclua essa pasta na sua cópia de segurança (lição 8.1) e poderá repor a aplicação num novo servidor em poucos minutos.
O plug-in chama-se docker compose (com um espaço, versão 2). O antigo programa independente docker-compose (com hífen) foi substituído; ainda o encontrará em manuais antigos. Também o campo version: "3.8" no topo de ficheiros antigos está obsoleto: o Compose utiliza sempre o esquema mais recente e, perante version:, apenas emite um aviso.
Ler um ficheiro compose
Um exemplo com um programa e uma base de dados PostgreSQL:
# /srv/notities/compose.yaml
services:
app:
image: ghcr.io/example/notities:1.4
restart: unless-stopped
ports:
- "127.0.0.1:8081:8080"
environment:
DB_HOST: db
DB_USER: notities
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: notities
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U notities"]
interval: 10s
retries: 5
volumes:
db-data:Passo a passo:
- services: cada serviço torna-se um contentor. O nome (
app,db) é também o nome de anfitrião com que os contentores se encontram uns aos outros. O Compose cria uma rede própria para a aplicação, pelo queappse liga à base de dados emdb:5432. - image: sempre com uma etiqueta de versão (lição 9.1).
- restart: o que o Docker faz quando o contentor para ou o servidor reinicia.
unless-stoppedvolta a iniciá-lo, exceto se o tiver parado deliberadamente. Sem política de reinício, a sua aplicação não volta após um reinício (lição 4.2). - ports: apenas a aplicação publica uma porta, e apenas em
127.0.0.1. A base de dados não publica nada: a aplicação chega a ela através da rede interna. Uma base de dados comports: - "5432:5432"é um dos erros mais frequentes (ver abaixo). - environment: definições para o programa. A palavra-passe não está escrita literalmente, mas sim como
${DB_PASSWORD}. - volumes: o volume com nome
db-dataconserva a base de dados quando o contentor é substituído. - depends_on com healthcheck: a aplicação só inicia quando a base de dados está realmente pronta, e não apenas iniciada.
Segredos num ficheiro .env
O Compose lê variáveis como ${DB_PASSWORD} a partir de um ficheiro .env na mesma pasta:
# /srv/notities/.env
DB_PASSWORD=Kies-hier-een-lang-willekeurig-wachtwoordanna@web1:/srv/notities$ sudo chmod 600 .envAssim, a palavra-passe não fica no ficheiro compose que talvez guarde no Git ou cole num fórum para pedir ajuda. Coloque .env em .gitignore. Crie a palavra-passe com um gestor de palavras-passe ou com openssl rand -base64 24.
Utilização diária
Todos os comandos são executados na pasta que contém o ficheiro compose:
anna@web1:/srv/notities$ sudo docker compose config # verificar o ficheiro, variáveis preenchidas
anna@web1:/srv/notities$ sudo docker compose up -d # iniciar tudo (ou atualizar conforme o ficheiro)
anna@web1:/srv/notities$ sudo docker compose ps # estado por serviço
anna@web1:/srv/notities$ sudo docker compose logs -f app
anna@web1:/srv/notities$ sudo docker compose pull # obter novas imagens
anna@web1:/srv/notities$ sudo docker compose up -d # substituir os contentores pelas novas imagens
anna@web1:/srv/notities$ sudo docker compose down # parar e remover os contentoresdown mantém os seus volumes. down -v remove também os volumes da aplicação e, com eles, a sua base de dados. Escreva isso apenas se quiser mesmo recomeçar do zero.
docker compose config é o equivalente de nginx -t e sshd -t: mostra como o Compose interpreta o seu ficheiro, com todas as variáveis preenchidas, antes de iniciar o que quer que seja. Tenha o cuidado de não colar esse resultado em lado nenhum, pois as palavras-passe constam dele.
O Docker contorna o ufw
Chegamos ao ponto de segurança mais importante deste módulo. Na lição 5.2 configurou o ufw para "recusar tudo, exceto SSH, 80 e 443". Seria de esperar que um contentor com ports: - "5432:5432" não ficasse acessível. Não é assim.
A documentação do Docker di-lo textualmente: o Docker e o ufw utilizam regras de firewall de uma forma que não é compatível entre si. Quando publica uma porta, o próprio Docker coloca regras na firewall do kernel. Essas regras reencaminham o tráfego para o contentor (tradução de endereços, DNAT) antes de as regras do ufw serem aplicadas. O pacote contorna, portanto, o ufw. ufw status não mostra nada de especial, enquanto a porta está acessível a partir de toda a Internet. O mesmo se aplica ao firewalld.
Além disso, sem endereço, o Docker publica em 0.0.0.0 e [::], ou seja, em IPv4 e também em IPv6 (lição 9.1).
Três formas de fazer isto corretamente, da mais simples à mais complexa:
- Não publique nada que não precise de ser público. Bases de dados, caches e serviços internos não recebem
ports:. Os contentores da mesma aplicação compose chegam uns aos outros através da rede interna. - Publique em loopback.
"127.0.0.1:8081:8080". Assim, apenas o próprio servidor tem acesso, e coloca à frente um proxy inverso (nginx ou Caddy, módulo 7) que escuta na porta 443. O Docker também pode adotar isto por predefinição: uma definição do daemon faz com que todas as portas publicadas sem endereço fiquem em127.0.0.1. - Regras na cadeia DOCKER-USER. O Docker cria uma cadeia vazia
DOCKER-USERque é percorrida antes das suas próprias regras. Aí pode colocar as suas próprias regras, por exemplo "apenas este endereço IP pode aceder às portas publicadas". Trata-se de trabalho avançado com iptables ou nftables, e é fácil errar. Para a maioria dos servidores pequenos, as opções 1 e 2 são suficientes.
Verifique sempre a partir do exterior, como na lição 5.1: nc -vz -w 3 <ip> 5432 a partir do seu portátil, ou a verificação do curso dos exercícios Z5.1 e Z10.4.
Mais duas coisas que não deve fazer
- Não montar
/var/run/docker.socknum contentor, a menos que saiba exatamente porquê. Um contentor com acesso ao socket do Docker pode ele próprio iniciar contentores, incluindo um com o disco inteiro do anfitrião (lição 9.2). Alguns utilitários (painéis, atualizadores automáticos) pedem-no; na prática, isso equivale a dar root a esse programa. - Não usar
privileged: truenemnetwork_mode: hostsó porque um manual o indica. Ambos retiram em grande parte o isolamento do contentor. Utilize-os apenas se o autor explicar porque é necessário.
Resumo
- Um ficheiro compose (
compose.yaml) descreve todos os contentores de uma aplicação; é ao mesmo tempo documentação e deve fazer parte da sua cópia de segurança. - Utilize etiquetas de versão, uma política de reinício, volumes com nome, um healthcheck sempre que possível e segredos num
.envcom permissões 600. docker compose config,up -d,ps,logs,pull,down;down -vremove também os volumes.- As portas publicadas contornam o ufw e o firewalld. Publique apenas o que tem de ser público e, caso contrário, em
127.0.0.1com um proxy inverso à frente. - Nada de docker.sock,
privilegedounetwork_mode: hostsem uma boa razão.
Questionário
Exercício
Exercício Z9.3: Rever um ficheiro compose. O ficheiro de exemplo compose-nakijken.txt contém (como ficheiro de texto) o ficheiro compose de uma pequena loja online com uma aplicação, uma base de dados, uma cache e uma ferramenta de administração. Contém seis erros que foram abordados nesta lição ou nas lições 9.1 e 9.2. Indique, para cada bloco de linhas numerado, se está correto ou errado e, para cada erro, escolha a correção certa. O programa compara com a chave fixa. Se tiver o seu próprio ficheiro compose, reveja-o depois segundo os mesmos pontos.
Para saber mais
- Documentação do Docker, How Compose works (compose.yaml): https://docs.docker.com/compose/intro/compose-application-model/
- Documentação do Docker, Version and name top-level elements: https://docs.docker.com/reference/compose-file/version-and-name/
- Documentação do Docker, Packet filtering and firewalls (Docker and ufw): https://docs.docker.com/engine/network/packet-filtering-firewalls/
- Documentação do Docker, Port publishing and mapping: https://docs.docker.com/engine/network/port-publishing/
- Documentação do Docker, Interpolation (variáveis e .env): https://docs.docker.com/reference/compose-file/interpolation/
