Introdução: por que containerizar aplicações TypeScript?
O desenvolvimento moderno de software é impensável sem containerização. Docker se tornou o padrão de facto para empacotar, distribuir e executar aplicações em ambientes isolados. Em combinação com TypeScript — um superconjunto estritamente tipado de JavaScript — o Docker garante previsibilidade, portabilidade e simplifica os processos de CI/CD.
Neste artigo, vamos abordar como containerizar corretamente uma aplicação TypeScript: desde um Dockerfile básico até uma compilação pronta para produção usando docker-compose e compilação em múltiplos estágios. Você aprenderá como evitar erros comuns, reduzir o tamanho da imagem e acelerar a implantação.
Fundamentos: TypeScript e Node.js no Docker
TypeScript é uma linguagem que compila para JavaScript. Isso significa que, para executar uma aplicação TypeScript em um contêiner, primeiro precisamos compilar o código-fonte e depois executar o JavaScript resultante com Node.js. Docker é ideal para automatizar esse processo.
Criando uma aplicação TypeScript simples
Para começar, vamos criar uma aplicação mínima em TypeScript. Estrutura do projeto:
my-ts-app/
├── src/
│ └── index.ts
├── package.json
├── tsconfig.json
└── Dockerfile
src/index.ts:
import express, { Request, Response } from 'express';
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req: Request, res: Response) => {
res.json({ message: 'Hello from TypeScript + Docker!' });
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
package.json:
{
"name": "my-ts-app",
"version": "1.0.0",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.11.0",
"typescript": "^5.3.3",
"ts-node": "^10.9.2"
}
}
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Dockerfile básico
A primeira versão, mais simples, do Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Este Dockerfile copia todo o projeto, instala as dependências, compila o TypeScript e executa a aplicação. No entanto, essa abordagem tem desvantagens: o tamanho da imagem é grande e o cache de camadas não é eficiente.
Otimização: compilação em múltiplos estágios e redução de tamanho
Para ambientes de produção, é recomendado usar a compilação em múltiplos estágios (multi-stage build). Isso permite separar as etapas de compilação e execução, mantendo apenas os arquivos necessários na imagem final.
Dockerfile de produção com compilação em múltiplos estágios
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
Vantagens dessa abordagem:
- Menor tamanho da imagem — a camada final contém apenas o código compilado e as dependências de produção.
- Segurança — o código-fonte TypeScript não permanece no contêiner, dev