Pra começar, por que eu escolhi usar Docker?
Uma pergunta que eu faço quando pretendo começar a aprender uma nova ferramenta é: qual problema ela resolve, quais são as alternativas, e como esse problema seria resolvido sem ela?
Dessa forma, o propósito da ferramenta fica claro — assim como se realmente faz sentido usá-la.
O Docker foi amplamente adotado porque resolve uma dor universal: a padronização do ambiente em que uma aplicação roda. Sem ele, caímos na famosa frase: “na minha máquina funciona”.
No segundo post deste blog, Arquitetura e decisões do LearningSea (visão geral) eu cito brevemente a história de uma colega de trabalho que enfrentava dificuldades recorrentes com ambientes de desenvolvimento:
Algum tempo atrás, uma colega de trabalho comentou sobre dificuldades recorrentes com ambientes de desenvolvimento. Dependências que não instalavam corretamente, configurações que quebravam com frequência e inconsistências entre máquinas.
Os problemas envolviam versões de banco de dados, conflitos de portas e diferenças nas versões da linguagem.
Eu queria que meu projeto rodasse de forma independente do ambiente de desenvolvimento, com uma abordagem prática e responsabilidades bem definidas.
O Docker me proporcionou isso — vamos entender como a seguir.
Como Docker resolve esse problema
Com o Docker, a infraestrutura da aplicação passa a ser definida como código explícito. Em vez de instalar manualmente todos os softwares necessários na sua máquina, você instala apenas o Docker — e todo o restante é descrito e executado por ele: sistema base, dependências, configurações e ponto de entrada.
Uma analogia útil para entender o Docker são as máquinas virtuais — com ressalvas importantes. Assim como nelas, você tem um ambiente isolado, com seu próprio filesystem, rede interna e DNS. A diferença é que o Docker é significativamente mais leve e não virtualiza um sistema operacional completo.
O trade-off
É importante entender que o Docker não elimina a complexidade — ele a desloca. Ao adotar a ferramenta, você passa a lidar com uma nova camada, o que de fato adiciona complexidade.
Em troca, você ganha um ambiente previsível, explícito e consistente, que se comporta da mesma forma em qualquer sistema que tenha Docker.
Como é na prática?
Na prática, temos dois arquivos centrais na configuração de um ambiente com Docker: O Dockerfile e o docker-compose.yml.
Scripts de entrypoint também são comuns — eles são pontos de entrada que definem comandos executados na inicialização do container, como checagens de conexão com o banco de dados para evitar race conditions (quando um serviço depende de outro que ainda não está pronto).
Imagem & Dockerfile
Uma imagem é um conjunto de camadas read-only que define o estado de um ambiente. A partir dela, containers são criados como instâncias em execução.
O Dockerfile define como uma imagem é construída. Essa imagem descreve tudo o que a aplicação precisa para funcionar: a base do sistema (como uma imagem com Python), dependências, configurações e comandos necessários para sua execução.
Imagens prontas podem ser encontradas no repositório oficial do Docker, o Docker Hub
Exemplo:
Dockerfile
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y \
libpq-dev \
netcat-openbsd \
&& rm -rf /var/lib/apt/lists/*
COPY src/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ .
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
FROM python:3.12-slim: define a imagem base com Python sobre uma distro Linux minimalistaWORKDIR /app: define o diretório padrão para execução de comandosRUN apt-get update && apt-get install -y ...: instala dependências do sistema necessárias para a aplicaçãolibpq-dev: são headers necessários para compilar os drivers do PostgreSQLnetcat-openbsd: ferramenta utilizada para checar disponibilidade de serviços (ex: porta do DB)rm -rf /var/lib...: remove o cache do apt para reduzir o tamanho da imagemCOPY src/requirements.txt: copia a lista de dependências Python para o containerRUN pip install --no-cache-dir -r requirements.txt: instala as dependências sem cacheCOPY src/ .: copia o código fonte da aplicação para dentro da imagemCOPY docker/entrypoint.sh /entrypoint.sh: copia o script de entrypointRUN chmod +x /entrypoint.sh: torna o script executávelENTRYPOINT ["/entrypoint.sh"]: define ele como processo principal do container
Containers & Docker Compose
Um container é uma instância em execução de uma imagem, com uma camada de escrita sobre ela.
Por isso, a imagem é imutável, enquanto o container representa uma camada efêmera acima dela.
Quando um container é parado, seus processos são encerrados, e qualquer dado não persistido é perdido. A persistência é feita por meio de volumes, que serão explicados adiante.
Um pouco de contexto histórico: o Docker começou focado em containers individuais, e o Compose surgiu como uma camada acima para orquestrar múltiplos serviços — inicialmente separado, mas eventualmente integrado por se tornar parte essencial do fluxo de uso.
O Docker Compose é um subcomando responsável por inicializar e gerenciar os containers definidos no arquivo docker-compose.yml.
Uma forma prática de entender containers é como ambientes isolados em execução, com seu próprio filesystem, rede e processos. Um detalhe importante é que eles compartilham o kernel do sistema operacional do host.
Exemplo:
docker-compose.yml
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 5s
retries: 5
web:
build: .
environment:
DB_HOST: db
DB_NAME: app
DB_USER: app
DB_PASSWORD: app
depends_on:
db:
condition: service_healthy
volumes:
- static_files:/app/staticfiles
- media_files:/app/media
caddy:
image: caddy:2-alpine
ports:
- "80:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- static_files:/srv/static
- media_files:/srv/media
depends_on:
- web
volumes:
db_data:
static_files:
media_files:
servicesdefine os containers que serão orquestrados pelo Compose- db
image: usa a imagem oficial do PostgreSQL baseada em Alpineenvironment: injeta variáveis de configuração no containervolumes: define persistência fora do ciclo de vida do containerhealthcheck: define verificação periódica de saúde do serviço
- web
build: constrói a imagem a partir do Dockerfile localenvironment: variáveis usadas pela aplicação para conexão com o bancodepends_on: define dependência e ordem de inicialização entre serviçoscondition: aguardar o healthcheck do banco antes de iniciarvolumes: monta volumes para persistência e compartilhamento de dadosstatic_files:/app/staticfiles: diretório onde a aplicação escreve arquivos estáticosmedia_files:/app/media: diretório onde a aplicação armazena arquivos de mídia
- caddy
image: servidor web / reverse proxy leveports: mapeia portas do host para o container (host:container)volumes: monta arquivos de configuração e volumes compartilhados no containerstatic_files:/srv/static: acesso aos arquivos estáticos gerados pelo backendmedia_files:/srv/media: acesso aos arquivos de mídia para servir diretamentedepends_on: define ordem de inicialização
Volumes
Os volumes são o que permitem a persistência de dados no Docker. Também são a forma padrão de compartilhar dados entre containers.
Esses volumes são montados em caminhos específicos dentro dos containers, permitindo leitura e escrita.
No fluxo acima (docker-compose.yml):
- O container
webescreve arquivos em/app/staticfilese/app/media - O container
caddyacessa esses mesmos dados em/srv/statice/srv/media
Ou seja, múltiplos containers podem montar o mesmo volume em caminhos diferentes, mantendo acesso ao mesmo conteúdo.
Containers não compartilham estado diretamente — volumes são o mecanismo de troca de dados entre serviços.
O backend escreve arquivos, e o proxy (Caddy) os serve diretamente (configurado no Caddyfile), evitando passar pela aplicação.
Entrypoint
O entrypoint define o comando base do container, funcionando como ponto de entrada para sua execução. Ele é sempre executado ao iniciar o container e pode ser complementado por argumentos.
Exemplo:
entrypoint.sh
#!/bin/sh
while ! nc -z $DB_HOST 5432; do
sleep 1
done
echo "Database is up"
python manage.py migrate
exec gunicorn config.wsgi:application --bind 0.0.0.0:8000
#!/bin/sh: define o interpretador do scriptsh(shebang)nc -z $DB_HOST 5432: testa a abertura de conexão TCP com o bancowhile ! ...; do sleep 1; done: loop de espera até o serviço ficar disponívelpython manage.py migrate: executa migrações antes de inicializar a aplicaçãoexec gunicorn ...: substitui o processo do shell pelo servidor WSGI (PID 1)--bind 0.0.0.0:8000: expõe o serviço em todas as interfaces do container
O que temos até aqui?
Até aqui definimos:
- o ambiente de aplicação de forma explícita (
Dockerfile) - os serviços que compõem a infraestrutura (
docker-compose.yml) - o ponto de entrada da aplicação (
entrypoint.sh)
O fluxo de inicialização
O fluxo completo a partir da execução do docker compose up --build é o seguinte:
- O Docker constrói a imagem da aplicação com base no
Dockerfile - O serviço
dbé iniciado a partir da imagem do PostgreSQL - O healthcheck do banco começa a ser executado
- O serviço
webaguarda até que o banco esteja saudável (service_healthy) e então é iniciado - O
entrypoint.shé executado como processo principal - O script aguarda a disponibilidade do banco (checagem de porta)
- As migrações são aplicadas
- O Gunicorn é iniciado e passa a servir a aplicação
- O serviço
caddyé iniciado e passa a atuar como reverse proxy
Conclusão
Com Docker, todo esse processo deixa de ser implícito e manual, e passa a ser definido de forma declarativa e reproduzível.
Esses exemplos são versões aproximadas do que utilizo no LearningSea. No entanto, várias partes foram removidas ou simplificadas para tornar o conteúdo mais didático e menos extenso.