Introdução ao Docker e TypeScript: por que a conteinerização é necessária
O desenvolvimento web moderno é impensável sem ferramentas que garantam a uniformidade do ambiente. Docker tornou-se o padrão de facto para empacotar, entregar e executar aplicações. Em combinação com TypeScript — uma linguagem estritamente tipada que compila para JavaScript — a conteinerização oferece uma ferramenta poderosa para criar serviços escaláveis e confiáveis.
Por que conteinerizar uma aplicação TypeScript? Primeiro, resolve o problema "funciona na minha máquina". Segundo, simplifica a implantação em produção e CI/CD. Terceiro, permite isolar dependências e versões do Node.js. Neste artigo, vamos abordar como criar uma imagem Docker para uma aplicação TypeScript, configurar uma build multi-stage e usar docker-compose para desenvolvimento local.
Fundamentos do Docker para o desenvolvedor TypeScript
Antes de escrever código, vamos lembrar os conceitos básicos do Docker: image (imagem) — modelo para criar um container; container — instância em execução de uma imagem; Dockerfile — instrução para construir a imagem.
Para uma aplicação TypeScript em Node.js, precisaremos de:
- Node.js runtime (geralmente a imagem
node:18-alpineounode:20-alpinepara tamanho mínimo) - Instalação de dependências (
npm installouyarn install) - Compilação de TypeScript para JavaScript (
tsc) - Execução da aplicação compilada
Vamos considerar um exemplo simples de aplicação TypeScript (arquivo src/index.ts):
import express from 'express';
const app = express();const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => { res.send('Hello from TypeScript + Docker!');});
app.listen(PORT, () => { console.log(`Server running on port ${PORT}`);});E o tsconfig.json:
{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true }, "include": ["src"], "exclude": ["node_modules", "dist"]}Agora vamos criar o Dockerfile para esta aplicação.
Criando um Dockerfile para aplicação TypeScript
Um Dockerfile básico pode ser assim:
FROM node:18-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
FROM node:18-alpine AS runnerWORKDIR /appCOPY --from=builder /app/dist ./distCOPY --from=builder /app/package*.json ./COPY --from=builder /app/node_modules ./node_modulesEXPOSE 3000CMD ["node", "dist/index.js"]Aqui é usado multi-stage build (build em múltiplos estágios). O primeiro estágio (builder) compila TypeScript para JavaScript, o segundo (runner) contém apenas o código compilado e as dependências de runtime. Isso reduz o tamanho da imagem final — os arquivos TypeScript fonte e as dependências de desenvolvimento não entram nela.
Importante: executamos npm ci (instalação limpa) em vez de npm install para reprodutibilidade e velocidade.
Otimizando o Dockerfile: cache de camadas
Docker armazena em cache cada camada (instrução). Para acelerar a build, copie arquivos que mudam raramente primeiro. Por exemplo, package.json e package-lock.json são copiados antes de todos os fontes, portanto, alterações no código não causam reinstalação de dependências.
Adicionalmente, pode-se adicionar um .dockerignore:
node_modulesdist.git*.md.gitignore.envIsso impede que arquivos desnecessários entrem no contexto da build.
docker-compose para desenvolvimento local
Para facilitar o desenvolvimento local, use docker-compose.yml. Ele permite executar a aplicação com recarga automática (usando nodemon ou