DevPath · Aprenda a programar ESPTEN

Docker e CI/CD

Contêineres e Docker

O problema: "na minha máquina funciona"

Uma aplicação depende de um ambiente: uma versão específica do Node, certas variáveis, bibliotecas do sistema... Se esse ambiente difere entre seu notebook e o servidor, surgem erros difíceis de reproduzir. Os contêineres resolvem isso empacotando a aplicação junto com seu ambiente para que ela seja executada igual em qualquer lugar.

Imagem vs contêiner

É a distinção mais importante do tema:

De uma mesma imagem você pode lançar muitos contêineres. A imagem é construída uma vez; os contêineres são criados, parados e destruídos continuamente.

O Dockerfile

Uma imagem é definida com um Dockerfile: uma receita de instruções que são executadas de cima para baixo.

# Imagem base da qual partimos
FROM node:20-alpine

# Pasta de trabalho dentro do contêiner
WORKDIR /app

# Copiamos primeiro os manifestos (para aproveitar o cache, veja abaixo)
COPY package.json package-lock.json ./
RUN npm ci

# Copiamos o resto do código-fonte
COPY . .

# Comando que é executado ao iniciar o contêiner
CMD ["node", "server.js"]

As instruções principais:

Camadas e cache de build

Cada instrução gera uma camada. O Docker faz cache de cada camada: se uma instrução e tudo que vem antes não mudaram, ele reutiliza a camada em vez de reexecutá-la. Por isso copiamos package.json e fazemos npm ci antes de copiar o resto do código: enquanto as dependências não mudam, essa camada (a lenta) é reutilizada mesmo que você edite seu código-fonte.

Regra prática: ponha o que muda pouco em cima e o que muda muito embaixo.

Builds multi-stage

Para construir você precisa de ferramentas (compiladores, dev-dependencies) que não quer na imagem final. Um build multi-stage usa uma etapa para construir e outra, limpa e pequena, para executar:

# Etapa 1: construir
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Etapa 2: imagem final, somente com o necessário para executar
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]

A imagem final não contém o código-fonte nem as dev-dependencies: apenas o resultado do build (dist) e as dependências de produção. Resultado: imagens mais pequenas, rápidas de implantar e com menor superfície de ataque.

Vários serviços: docker-compose

Uma app real combina vários contêineres (API, banco de dados, cache...). docker-compose os descreve e orquestra em um único arquivo:

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/app
    depends_on:
      - db
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: pass
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Com docker compose up você sobe toda a pilha de uma vez. Os serviços se veem entre si pelo seu nome (db), e os volumes persistem dados mesmo que o contêiner seja destruído.

Coloque isto em prática

O DevPath é um curso prático: aqui você lê a teoria; no app você a coloca em prática com exercícios que rodam de verdade, offline.

Comece grátis no app →
Integração contínua (CI) →