Déployer une application NestJS
Docker + GitHub Actions = déploiement automatisé et reproductible
Containeriser l'application NestJS avec Docker garantit un environnement identique entre dev, staging et production. GitHub Actions automatise les tests et le déploiement.
Dockerfile de base
# Dockerfile simple (développement)
FROM node:20-alpine
WORKDIR /app
# Copier les fichiers de dépendances en premier (cache layer)
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/main"]
.dockerignore
node_modules
dist
.git
*.md
.env
.env.*
!.env.example
coverage
test
*.spec.ts
Multi-stage build — production
# ── Stage 1 : Builder ────────────────────────────────
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Supprimer les devDependencies
RUN npm prune --production
# ── Stage 2 : Runner (image finale légère) ────────────
FROM node:20-alpine AS runner
RUN addgroup -g 1001 -S nodejs && \
adduser -S nestjs -u 1001
WORKDIR /app
# Copier uniquement ce qui est nécessaire
COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nestjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nestjs:nodejs /app/package.json ./
# Utiliser un utilisateur non-root (sécurité)
USER nestjs
EXPOSE 3000
# Démarrer l'application
CMD ["node", "dist/main"]
Taille de l'image : Le multi-stage build réduit typiquement l'image de ~800MB (avec devDependencies) à ~150MB (production only). L'image alpine est 3× plus petite que l'image debian.
Docker Compose
docker-compose.yml — développement
version: '3.9'
services:
api:
build:
context: .
dockerfile: Dockerfile
target: builder # Utiliser le stage builder pour le dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules # Volume anonyme pour protéger node_modules
environment:
- NODE_ENV=development
env_file:
- .env
command: npm run start:dev
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks:
- app-network
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASS:-postgres}
POSTGRES_DB: ${DB_NAME:-nestjs_db}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- app-network
volumes:
postgres_data:
redis_data:
networks:
app-network:
driver: bridge
docker-compose.prod.yml
version: '3.9'
services:
api:
build:
context: .
target: runner # Stage production uniquement
restart: unless-stopped
environment:
NODE_ENV: production
env_file:
- .env.production
deploy:
replicas: 2
resources:
limits:
memory: 512m
restart_policy:
condition: on-failure
max_attempts: 3
Variables d'environnement
.env.example
# Application
NODE_ENV=development
PORT=3000
# Base de données
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASS=postgres
DB_NAME=nestjs_db
DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}
# JWT
JWT_SECRET=change-this-to-a-random-256-bit-secret
JWT_REFRESH_SECRET=change-this-to-another-random-256-bit-secret
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# Mail (SMTP)
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_USER=postmaster@domain.com
MAIL_PASS=secret
Validation de la config avec Joi
npm install joi
// app.module.ts — valider les variables d'env au démarrage
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
PORT: Joi.number().default(3000),
DATABASE_URL: Joi.string().required(),
JWT_SECRET: Joi.string().min(32).required(),
JWT_REFRESH_SECRET: Joi.string().min(32).required(),
REDIS_HOST: Joi.string().default('localhost'),
REDIS_PORT: Joi.number().default(6379),
}),
validationOptions: {
allowUnknown: true,
abortEarly: false, // Afficher toutes les erreurs, pas juste la première
},
})
Health checks
npm install @nestjs/terminus @nestjs/axios
// health.module.ts
import { TerminusModule } from '@nestjs/terminus';
@Module({
imports: [TerminusModule],
controllers: [HealthController],
})
export class HealthModule {}
// health.controller.ts
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
private http: HttpHealthIndicator,
private memory: MemoryHealthIndicator,
private disk: DiskHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), // 200 MB
() => this.http.pingCheck('external-api', 'https://api.example.com/health'),
]);
}
}
// Réponse JSON
// GET /health
// {
// "status": "ok",
// "info": { "database": { "status": "up" }, "memory_heap": { "status": "up" } },
// "error": {},
// "details": { ... }
// }
GitHub Actions CI/CD
# .github/workflows/ci.yml
name: CI/CD NestJS
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ── Job 1 : Tests ──────────────────────────
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: nestjs_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Setup Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Unit tests
run: npm run test:cov
env:
NODE_ENV: test
DATABASE_URL: postgresql://test:test@localhost:5432/nestjs_test
JWT_SECRET: test-secret-key-for-ci-at-least-32-chars
JWT_REFRESH_SECRET: refresh-test-secret-key-for-ci-32chars
- name: E2E tests
run: npm run test:e2e
env:
NODE_ENV: test
DATABASE_URL: postgresql://test:test@localhost:5432/nestjs_test
- name: Upload coverage
uses: codecov/codecov-action@v4
# ── Job 2 : Build & Push Docker ────────────
build-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=sha-
type=raw,value=latest
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ── Job 3 : Deploy ─────────────────────────
deploy:
needs: build-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
cd /opt/nestjs-app
docker-compose pull
docker-compose up -d --no-deps --build api
docker system prune -f
Monitoring & Observabilité
Métriques avec Prometheus
npm install @willsoto/nestjs-prometheus prom-client
// app.module.ts
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
@Module({
imports: [PrometheusModule.register()],
})
export class AppModule {}
// Métriques dans un service
import { Counter, Histogram } from 'prom-client';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
@Injectable()
export class MetricsService {
constructor(
@InjectMetric('http_requests_total')
private requestsCounter: Counter<string>,
@InjectMetric('http_request_duration_seconds')
private requestDuration: Histogram<string>,
) {}
trackRequest(method: string, route: string, statusCode: number, duration: number) {
this.requestsCounter.inc({ method, route, status: statusCode });
this.requestDuration.observe({ method, route }, duration / 1000);
}
}
Stack complète : Prometheus (collecte métriques) + Grafana (dashboards) + Loki (logs) + Tempo (traces) = observabilité complète. Docker Compose les intègre facilement.