Projeto: containerizar uma aplicação
LidoNesta aula, você aprenderá a containerizar uma aplicação do início ao fim, cobrindo desde a criação de um Dockerfile eficiente até a orquestração com Docker Compose, passando pelo uso de variáveis de ambiente e técnicas de otimização de imagem. O projeto prático consolida os conceitos de DevOps, preparando você para ambientes de produção.
Containerizar uma aplicação é uma habilidade essencial no mundo DevOps. Nesta aula, vamos construir, passo a passo, a containerização de uma aplicação web simples, utilizando Docker e Docker Compose. Você aprenderá a criar um Dockerfile robusto, a configurar serviços com Compose, a gerenciar variáveis de ambiente e a otimizar suas imagens para produção.
Este projeto prático vai além do básico: abordaremos boas práticas como camadas enxutas, multi-stage builds e segurança. Ao final, você terá um container pronto para ser implantado em qualquer ambiente, com configuração flexível e eficiente.
Dockerfile
O Dockerfile é o coração da containerização. Ele define a imagem base, as dependências, o código e os comandos necessários para executar sua aplicação. Um bom Dockerfile deve ser declarativo, reproduzível e otimizado.
Vamos começar com um exemplo de aplicação Node.js simples. Nosso Dockerfile terá as seguintes etapas: definir a imagem base, copiar os arquivos de dependências, instalar as dependências, copiar o código e definir o comando de inicialização.
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Note que usamos a imagem node:18-alpine, que é leve e segura. O WORKDIR define o diretório de trabalho. Copiamos os arquivos package*.json primeiro para aproveitar o cache de camadas do Docker: se eles não mudarem, o npm install não será reexecutado em builds futuros. Depois copiamos o restante do código e definimos o comando padrão.
Para aplicações mais complexas, como uma API em Python, o Dockerfile segue a mesma lógica, mas com comandos diferentes, como pip install. O importante é entender a estrutura e as boas práticas.
Compose
O Docker Compose permite definir e gerenciar múltiplos containers como uma única aplicação. Com um arquivo docker-compose.yml, você especifica serviços, redes, volumes e dependências entre eles.
No nosso projeto, vamos criar um serviço para a aplicação web e outro para um banco de dados PostgreSQL. O Compose cuida da comunicação entre eles e facilita a escalabilidade.
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DB_HOST=db
- DB_PORT=5432
- DB_USER=postgres
- DB_PASSWORD=secret
depends_on:
- db
networks:
- app-network
db:
image: postgres:14-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=mydb
volumes:
- db-data:/var/lib/postgresql/data
networks:
- app-network
volumes:
db-data:
networks:
app-network:
Com esse arquivo, você pode subir toda a aplicação com docker-compose up. O Compose cria uma rede isolada, monta volumes para persistência e define as variáveis de ambiente. Isso simplifica o desenvolvimento e a implantação.
Para desenvolvimento, você pode usar o docker-compose.override.yml para montar volumes de código e habilitar hot-reload. Em produção, você usa o arquivo docker-compose.prod.yml com configurações otimizadas.
Variáveis
Variáveis de ambiente são fundamentais para configurar aplicações em diferentes ambientes (dev, staging, prod) sem alterar o código. No Docker, você pode defini-las no Dockerfile, no Compose ou em um arquivo .env.
No Dockerfile, você pode usar ENV para definir valores padrão, mas é recomendado usar ARG para valores de build e ENV para valores de runtime. No Compose, a seção environment define variáveis diretamente.
# .env (exemplo)
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=secret
DB_NAME=mydb
No Compose, você pode referenciar as variáveis do arquivo .env usando a sintaxe ${VARIAVEL}. Isso permite manter segredos fora do arquivo de configuração e facilita a troca de valores por ambiente.
# docker-compose.yml (trecho)
services:
web:
environment:
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=${DB_NAME}
Para valores sensíveis, como senhas, evite expô-los no Dockerfile ou no Compose. Use segredos do Docker Swarm ou ferramentas como Vault. Em desenvolvimento, o arquivo .env deve ser adicionado ao .gitignore para não ser versionado.
Otimização
Imagens menores são mais rápidas para baixar, mais seguras e consomem menos recursos. Várias técnicas ajudam a otimizar suas imagens Docker.
Primeiro, use imagens base menores e específicas, como alpine ou slim. Segundo, combine comandos para reduzir o número de camadas. Terceiro, use multi-stage builds para compilar e depois copiar apenas os artefatos necessários para a imagem final.
# Exemplo de multi-stage build
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY package*.json ./
RUN npm install --omit=dev
EXPOSE 3000
CMD ["npm", "start"]
No exemplo acima, a primeira etapa compila o código e a segunda etapa copia apenas o resultado, sem as dependências de desenvolvimento. Isso reduz drasticamente o tamanho da imagem final.
Outras dicas: use .dockerignore para excluir arquivos desnecessários (como node_modules, .git), limpe caches de pacotes dentro do Dockerfile e use camadas de cache de forma inteligente. Verifique o tamanho com docker images e analise com docker history.
Boas práticas e observações finais
Containerizar não é apenas criar um Dockerfile; é pensar em segurança, eficiência e manutenibilidade. Sempre rode containers como usuário não-root, use imagens oficiais e mantenha as dependências atualizadas.
Teste sua aplicação localmente antes de subir para produção. Use ferramentas como docker-compose config para validar o arquivo. E documente seu processo de build e deploy para facilitar a colaboração.
Exercícios
- Crie um Dockerfile para uma aplicação Python (Flask) que escuta na porta 5000. Use a imagem
python:3.11-slim, copie o código, instale as dependências do arquivorequirements.txte defina o comandopython app.py. Inclua umHEALTHCHECKsimples. - No Compose do projeto, adicione um serviço de cache Redis. Configure a variável de ambiente
REDIS_URLno serviço web para se conectar ao Redis. - Explique como usar um arquivo
.envno Compose e por que ele não deve ser versionado. Dê um exemplo de conteúdo. - Otimize o Dockerfile abaixo, que instala dependências e copia o código, mas gera uma imagem grande. Sugira melhorias usando multi-stage e limpeza de cache.
- Escreva um comando para construir a imagem a partir do Dockerfile otimizado e executar o container mapeando a porta 3000.
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
HEALTHCHECK CMD curl --fail http://localhost:5000/ || exit 1
CMD ["python", "app.py"]
services:
web:
environment:
- REDIS_URL=redis://redis:6379
redis:
image: redis:7-alpine
ports:
- "6379:6379"
.env armazena variáveis de ambiente em formato CHAVE=valor. O Compose o lê automaticamente e substitui as referências ${VAR}. Ele não deve ser versionado porque contém segredos (senhas, chaves) e configurações específicas do ambiente. Exemplo: DB_PASSWORD=secret.FROM node:18
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY package*.json ./
RUN npm install --omit=dev
EXPOSE 3000
CMD ["npm", "start"]
docker build -t minha-app .
docker run -p 3000:3000 minha-app