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

  1. 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 arquivo requirements.txt e defina o comando python app.py. Inclua um HEALTHCHECK simples.
  2. ✓ Resposta:
    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"]
    
  3. No Compose do projeto, adicione um serviço de cache Redis. Configure a variável de ambiente REDIS_URL no serviço web para se conectar ao Redis.
  4. ✓ Resposta:
    services:
      web:
        environment:
          - REDIS_URL=redis://redis:6379
      redis:
        image: redis:7-alpine
        ports:
          - "6379:6379"
    
  5. Explique como usar um arquivo .env no Compose e por que ele não deve ser versionado. Dê um exemplo de conteúdo.
  6. ✓ Resposta: O arquivo .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.
  7. 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.
  8. FROM node:18
    WORKDIR /app
    COPY . .
    RUN npm install
    EXPOSE 3000
    CMD ["npm", "start"]
    

    ✓ Resposta:
    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"]
    
  9. Escreva um comando para construir a imagem a partir do Dockerfile otimizado e executar o container mapeando a porta 3000.
  10. ✓ Resposta:
    docker build -t minha-app .
    docker run -p 3000:3000 minha-app
    

Referências