Docker: como usar para facilitar o desenvolvimento de projetos
Olá leitor, se você já teve problemas ao rodar um projeto feito na máquina de um colega(o famoso “Na minha máquina funciona”), teve problemas ao fazer deploy, porque o ambiente de era completamente diferente do seu ambiente de desenvolvimento, ou precisava fazer instalação de dependências para o projeto na sua máquina, mas provavelmente nem iria usar depois, este artigo irá te explicar como o Docker soluciona todos estes problemas.
Mas, o que é Docker?
O Docker é uma plataforma de código aberto que automatiza a implantação, escala e gerenciamento de aplicações dentro de ambientes isolados e portáteis chamados contêineres.
Em termos simples, o Docker permite que os desenvolvedores empacotem uma aplicação com todas as suas dependências (bibliotecas, configurações, variáveis de ambiente e o código) em um único pacote padrão que pode rodar de forma consistente em qualquer lugar.
O objetivo principal do Docker é resolver o problema de inconsistência de ambiente ("funciona na minha máquina, mas não na produção"). Ao garantir que o ambiente de execução da aplicação (o contêiner) seja idêntico em desenvolvimento, testes e produção, ele simplifica e acelera todo o ciclo de vida de desenvolvimento de software (DevOps).
Conceitos Chave
- Contêineres: São unidades de software leves, autossuficientes e executáveis que empacotam o código da aplicação e suas dependências. Eles compartilham o kernel do sistema operacional host, o que os torna muito mais rápidos e eficientes em recursos do que máquinas virtuais tradicionais.
- Imagens: São modelos somente leitura usados para criar contêineres. Uma imagem contém tudo o que é necessário para executar uma aplicação: o sistema operacional base, bibliotecas, dependências e o código da aplicação.
- Dockerfile: É um arquivo de texto que contém todas as instruções e comandos necessários para construir uma Imagem Docker. É a "receita" para o ambiente da sua aplicação.
Por Que Usar Docker?
O Docker revolucionou a forma como desenvolvemos, distribuímos e executamos aplicações. Ele resolve o clássico problema de "funciona na minha máquina, mas não na produção" ao empacotar a aplicação e todas as suas dependências (bibliotecas, configurações, variáveis de ambiente) em um contêiner padronizado.
A principal vantagem é a consistência e portabilidade: o contêiner Docker garante que sua aplicação se comporte exatamente da mesma forma em qualquer ambiente, seja no seu laptop, em um servidor de testes ou em produção. Além disso, ele oferece isolamento (separando sua aplicação do sistema operacional host e de outras aplicações) e facilita a escalabilidade, tornando a implantação (deployment) muito mais eficiente e rápida.
Como o Docker Funciona?
O Docker opera com base em três conceitos principais: Imagens, Contêineres e o Docker Engine.
1. Imagens Docker
Uma Imagem Docker é um modelo somente leitura que contém o sistema de arquivos da aplicação, o código, as bibliotecas, as dependências e as configurações necessárias para rodar o software.
- Pense nela como uma planta ou classe: Ela define exatamente como o ambiente da aplicação deve ser construído.
- Imagens são criadas a partir de um arquivo chamado Dockerfile, que é um script com uma série de instruções para montar a imagem (por exemplo, "Comece com um sistema operacional Linux base", "Copie meu código-fonte", "Instale estas dependências", "Defina esta variável de ambiente").
- Imagens são leves e são construídas em camadas (layers), o que permite o compartilhamento e a reutilização eficiente entre diferentes imagens.
2. Contêineres Docker
Um Contêiner Docker é a instância executável de uma Imagem Docker. É o ambiente isolado onde sua aplicação realmente roda.
- Pense nele como o objeto ou a máquina virtual leve: Quando você executa uma imagem, o Docker cria um contêiner baseado nela.
- Cada contêiner é isolado do sistema operacional hospedeiro (host) e dos outros contêineres, mas compartilha o kernel do sistema operacional host, o que o torna muito mais leve e rápido de iniciar do que uma máquina virtual tradicional.
- O contêiner contém tudo o que é necessário para a aplicação funcionar e garante que o ambiente de execução seja sempre o mesmo, independentemente de onde o contêiner estiver rodando.
3. Docker Engine
O Docker Engine é o coração do sistema. É uma aplicação cliente-servidor que é instalada no seu sistema operacional host (laptop, servidor, etc.).
- Daemon (Servidor): É o processo em segundo plano que gerencia a construção, o envio e a execução de Imagens e Contêineres.
- API REST: Interface que permite que o cliente se comunique com o Daemon.
- Cliente (CLI): É a interface de linha de comando (docker run, docker build, etc.) que o desenvolvedor usa para interagir com o Daemon e controlar o ciclo de vida dos contêineres.
Em resumo, você define as instruções da sua aplicação no Dockerfile, o Docker Engine usa o Dockerfile para construir uma Imagem (camadas de arquivos estáticos), e quando você executa essa Imagem, o Docker Engine cria um Contêiner (o ambiente de execução isolado) onde sua aplicação irá rodar de forma consistente.
Botando a mão na massa: exemplo para uma aplicação Node.js
Agora, vamos mostrar um exemplo de imagem para uma aplicação node.js, para isso, vamos configurar um Dockerfile no exemplo abaixo:
# 1. Imagem Base
# Começamos com uma imagem oficial do Node.js.
# '18-alpine' é uma versão leve (baseada no Alpine Linux)
FROM node:18-alpine
# 2. Diretório de Trabalho
# Cria e define o diretório de trabalho dentro do contêiner
WORKDIR /app
# 3. Copiar package.json (Otimização de Cache)
# Copiamos *apenas* os arquivos de dependência primeiro.
# O Docker armazena essa camada em cache.
COPY package.json package-lock.json ./
# 4. Instalar Dependências
# Se os arquivos package*.json não mudaram, o Docker
# reutiliza o cache da camada anterior e pula este passo.
RUN npm install
# 5. Copiar o Código-Fonte
# Agora, copiamos o resto do código da nossa aplicação.
COPY . .
# 6. Expor a Porta
# Informa ao Docker que o contêiner escuta na porta 3000
EXPOSE 3000
# 7. Comando de Inicialização
# O comando para iniciar a aplicação (assumindo que seu
# arquivo principal se chama 'server.js')
CMD ["node", "server.js"]
Explicando:
- FROM node:18-alpine: Define a imagem base. É como dizer ao Docker: "Preciso de um ambiente que já tenha o Node.js versão 18 instalado". alpine é popular por ser muito pequena.
- WORKDIR /app: Cria uma pasta /app dentro do contêiner e define que todos os próximos comandos (como COPY e RUN) serão executados a partir dela.
- COPY package*.json ./ e RUN npm install: Esta é a parte inteligente. Copiamos package.json e package-lock.json separadamente do resto do código. O Docker funciona em camadas (layers). Se o package.json não mudou desde a última vez, o Docker reutiliza a camada onde o npm install já foi executado, tornando a build muito mais rápida.
- COPY . .: Copia todo o resto (seu server.js, rotas, etc.) para o diretório /app.
- EXPOSE 3000: Apenas "documenta" que a aplicação dentro do contêiner usará a porta 3000.
- CMD ["node", "server.js"]: O comando que o contêiner executará assim que for iniciado.
Otimizando o Dockerfile para produção
O exemplo acima mostra um exemplo de Dockerfile para ser usado em desenvolvimento, porém, para produção, temos duas preocupações principais: segurança e tamanho da imagem. Não queremos devDependencies (como nodemon, jest, etc.) na nossa imagem final. Para isso, usamos um "multi-stage build" (build em múltiplos estágios).
# --- ESTÁGIO 1: "Build" ---
# Usamos um estágio com todas as ferramentas de dev
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
# Instala TODAS as dependências, incluindo as 'devDependencies'
RUN npm install
# Copia todo o código-fonte
COPY . .
# (Opcional: Se seu projeto Express usasse TypeScript,
# você rodaria o 'npm run build' aqui para compilar)
# RUN npm run build
# --- ESTÁGIO 2: "Production" ---
# Começamos do zero com uma imagem limpa
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
# Agora, instalamos APENAS as dependências de produção
RUN npm install --production
# Copiamos os arquivos da aplicação do estágio 'builder'
# Isso garante que não trazem nenhuma 'devDependency'
COPY --from=builder /app/ .
EXPOSE 3000
CMD ["node", "server.js"]
Este Dockerfile tem duas seções FROM, criando dois "estágios" temporários:
- Estágio builder (Construtor):
- Instala todas as dependências (npm install), incluindo as de desenvolvimento.
- (Se fosse um projeto TypeScript, como NestJS, este é o estágio onde você compilaria o código de .ts para .js).
- Estágio production (Final):
- Começamos com uma imagem node:18-alpine "limpa" novamente.
- Instalamos apenas as dependências de produção (npm install --production). Isso é crucial para a segurança e o tamanho.
- Usamos COPY --from=builder /app/ . para copiar apenas o código-fonte necessário do estágio builder para a nossa imagem final.
O resultado é uma imagem final muito menor e mais segura, pois ela não contém as devDependencies.
Criando a Imagem
Agora, com o Dockerfile pronto, podemos buildar a imagem com o seguinte comando:
docker build -t nome-da-sua-imagem:tag-opcional .
Onde:
- docker build: É o comando principal para iniciar o processo de construção.
- -t (tag): Permite que você nomeie e marque sua imagem.
- nome-da-sua-imagem: O nome que você dará à sua imagem (ex: minha-app-node).
- :tag-opcional: Uma tag (rótulo/versão) opcional (ex: :latest ou :1.0.0).
- . (ponto): Indica que o Dockerfile está no diretório atual. Este ponto especifica o contexto da construção, ou seja, de onde o Docker deve procurar os arquivos a serem copiados (como seu código-fonte).
Rodando a Imagem
Depois de construir a imagem, você usa o comando docker run para criar e iniciar o Contêiner.
docker run -p 3000:3000 nome-da-sua-imagem:tag
Onde:
- docker run: É o comando principal para iniciar o container.
- -p (port): Permite expor a porta do container no host.
Dica: adicione a flag -d para executar o container em segundo plano.
Outros comandos úteis
Além de docker build e docker run, existem diversos comandos essenciais para gerenciar seus contêineres e imagens.
Gerenciamento de Contêineres
Lista todos os contêineres em execução (rodando).
docker ps
Lista todos os contêineres (incluindo os parados/encerrados).
docker ps -a
Para um contêiner em execução de forma suave.
docker stop [ID ou NOME do Contêiner]
Inicia um contêiner que foi parado.
docker start [ID ou NOME do Contêiner]
Para e inicia novamente um contêiner.
docker restart [ID ou NOME do Contêiner]
Remove um contêiner parado.
docker rm [ID ou NOME do Contêiner]
Exibe os logs (saída de console) de um contêiner.
docker logs [ID ou NOME do Contêiner]
Gerenciamento de Imagens
Lista todas as imagens baixadas ou construídas localmente.
docker images
Remove uma imagem local. (Necessário parar e remover contêineres baseados nela primeiro).
docker rmi [ID ou NOME da Imagem]
Baixa uma imagem de um registro (como o Docker Hub).
docker pull nome/imagem:tag
Limpeza
Remove dados não utilizados: contêineres parados, redes não utilizadas, imagens sem tag e o cache de build.
docker system prune
Indo Além: Orquestrando Serviços com Docker Compose
Até agora, vimos como "buildar" e rodar um contêiner (nossa aplicação Node.js). Mas, na realidade, a maioria das aplicações depende de outros serviços, como um banco de dados, um sistema de cache (Redis) ou outros microsserviços.
Gerenciar múltiplos comandos docker run com diferentes redes e volumes é complexo. É aqui que entra o Docker Compose.
O Docker Compose é uma ferramenta para definir e rodar aplicações Docker com múltiplos contêineres. Com um único arquivo de configuração (docker-compose.yml), você pode definir todos os serviços da sua aplicação e iniciá-los com um único comando.
Exemplo: Rodando o Express.js com um Banco de Dados PostgreSQL
Vamos imaginar que nossa aplicação Node.js precise se conectar a um banco de dados PostgreSQL. Em vez de instalar o Postgres na nossa máquina, vamos "conteinerizá-lo" também.
Crie um arquivo chamado docker-compose.yml na raiz do seu projeto (ao lado do seu Dockerfile):
# Define a versão da sintaxe do Compose
version: '3.8'
# 'services' é onde definimos todos os nossos contêineres
services:
# Serviço 1: Nossa aplicação Express.js
app:
# 'build: .' diz ao Compose para construir a imagem
# usando o 'Dockerfile' na pasta atual.
build: .
ports:
# Mapeia a porta 3000 do seu computador (host)
# para a porta 3000 do contêiner (onde o Express roda).
- "3000:3000"
environment:
# Passa variáveis de ambiente para a aplicação.
# É assim que sua app saberá onde o banco está.
- DATABASE_URL=postgres://user:password@db:5432/mydb
depends_on:
# Diz ao Compose para iniciar o serviço 'db' ANTES
# de iniciar o serviço 'app'.
- db
# Serviço 2: O banco de dados PostgreSQL
db:
# Usa uma imagem oficial do Postgres direto do Docker Hub
image: postgres:14-alpine
environment:
# Essas variáveis são usadas pelo Postgres
# para inicializar o banco pela primeira vez.
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=mydb
volumes:
# A parte mais importante!
# Persiste os dados do banco em um "volume" chamado 'db-data'.
# Se você destruir o contêiner, seus dados NÃO serão perdidos.
- db-data:/var/lib/postgresql/data
# Define os volumes nomeados
volumes:
db-data:
driver: local
Explicando o docker-compose.yml
- services: Define os contêineres que compõem a aplicação. Temos dois: app e db.
- app.build: .: Instrui o Compose a construir a imagem do serviço app usando o Dockerfile encontrado no diretório atual (.).
- app.ports: Mapeia a porta do host para a porta do contêiner, assim como o docker run -p.
- app.environment: Define variáveis de ambiente. Note o DATABASE_URL:
- O host do banco é db. Este é o nome do serviço do banco de dados. O Docker Compose cria uma rede interna e permite que os contêineres se comuniquem usando seus nomes de serviço.
- app.depends_on: Garante que o contêiner db seja iniciado antes do contêiner app, evitando erros de conexão na inicialização.
- db.image: Em vez de construir, ele baixa a imagem postgres:14-alpine pronta do Docker Hub.
- db.volumes: Esta é a chave para a persistência de dados. A linha db-data:/var/lib/postgresql/data mapeia a pasta interna de dados do Postgres para um "volume" gerenciado pelo Docker chamado db-data. Isso garante que, mesmo se você parar e remover o contêiner (docker-compose down), seus dados estarão seguros e serão recarregados na próxima vez que você subir o serviço.
Comandos do Docker Compose
Agora, em vez de docker build e docker run, você usa:
Para construir e iniciar todos os serviços: (Rode no terminal, na pasta do arquivo docker-compose.yml)
docker-compose up
- (Adicione -d para rodar em segundo plano: docker-compose up -d)
Para parar e remover os contêineres e a rede:
docker-compose down
Resumindo e Próximos Passos
O Docker se estabeleceu como uma ferramenta fundamental, proporcionando consistência e portabilidade inigualáveis no ciclo de vida do desenvolvimento de software. Sua eficácia reside no empacotamento de aplicações em Contêineres leves e isolados. Estes, por sua vez, são criados a partir de Imagens padronizadas, cuja definição é feita via Dockerfile. Com isso, o Docker não só elimina o recorrente problema de inconsistência de ambiente, mas também acelera consideravelmente o processo de implantação.
Uma boa prática essencial, destacada pelo uso da construção em múltiplos estágios (multi-stage build), como no exemplo de produção Node.js, é a garantia de segurança (excluindo devDependencies) e a redução drástica do tamanho das imagens finais.
Além disso, para orquestrar múltiplas aplicações em contêineres que trabalham juntas (como um backend, frontend e um banco de dados), o Docker Compose se torna indispensável. Ele permite definir e executar aplicações multi-contêiner utilizando um único arquivo YAML, simplificando significativamente o gerenciamento e o ciclo de vida de ambientes de desenvolvimento e teste complexos.
Boas Práticas Essenciais com Contêineres:
- Use Imagens Base Oficiais e Leves: Sempre comece seu Dockerfile com imagens base mantidas oficialmente (como node:18-alpine), que são verificadas e otimizadas para tamanho mínimo.
- Otimize o Cache de Build: Organize seu Dockerfile (como copiar package.json antes do código-fonte) para que o Docker possa reutilizar camadas (layers) não alteradas, acelerando builds futuros.
- Use Variáveis de Ambiente: Evite hardcoding de configurações. Use variáveis de ambiente (E.g., com a instrução ENV no Dockerfile) para configurações específicas de ambiente (produção, staging, etc.).
- Não Execute como Root: Por motivos de segurança, sempre defina um usuário não-root (usando a instrução USER) dentro do seu contêiner, especialmente em produção.
- Utilize o Docker Compose: Para projetos que dependem de múltiplos serviços (como uma aplicação Node.js e um banco de dados), use o Docker Compose para orquestrar e gerenciar o ambiente completo de forma declarativa e simples.
Ao adotar o Docker, você padroniza seus ambientes e adquire a capacidade de rodar sua aplicação de forma idêntica em qualquer lugar do mundo.
Para aprofundar seus conhecimentos e explorar todas as funcionalidades do Docker, consulte a documentação oficial:
Documentação Oficial do Docker: https://docs.docker.com/
Conclusão
Deixe sua opinião sobre o Docker, o que você gostou e se já utiliza para o desenvolvimento, e como têm ajudado no seu dia-a-dia como dev.
No mais, agradeço pela oportunidade de ler este artigo :)



