Conteneurisez votre app Angular avec un Dockerfile multi-stage : build Node, Nginx Alpine, gzip, sécurité non-root et image finale sous 30 MB.
Pourquoi un Dockerfile multi-stage ?
Lorsqu'on dockerise une application Angular pour la première fois, la tentation est de copier node_modules, lancer ng serve et exposer le port 4200. C'est exactement ce qu'il ne faut pas faire en production. ng serve est un serveur de développement non optimisé, qui recompile les assets à la volée et expose des ressources de débogage. Le résultat ? Une image Docker dépassant le gigaoctet, vulnérable, lente, et inadaptée au trafic réel.
Le pattern multi-stage build résout ce problème en exécutant deux phases distinctes dans un seul Dockerfile : une étape builder qui compile l'application avec Node.js, puis une étape runtime qui copie uniquement les artefacts statiques produits dans une image Nginx légère. L'image finale ne contient ni Node, ni node_modules, ni le code source TypeScript : juste le HTML, le CSS, le JavaScript bundlé et le serveur web.
Comparaison des tailles d'image
| Stratégie | Image de base | Taille finale | Surface d'attaque | Production-ready |
|---|---|---|---|---|
Single-stage avec ng serve |
node:20 |
~ 1.2 GB | Très large (Node + npm + sources) | Non |
Single-stage avec http-server |
node:20-alpine |
~ 220 MB | Moyenne (Node runtime restant) | Non recommandé |
| Multi-stage Node + Nginx Alpine | nginx:1.27-alpine |
~ 25 MB | Minimale (Nginx + assets) | Oui |
| Multi-stage + distroless | gcr.io/distroless/nginx |
~ 18 MB | Quasi-nulle (pas de shell) | Oui (avancé) |
Architecture du Dockerfile multi-stage
Le diagramme logique est simple : deux FROM séparés, le second copiant uniquement le dist/ du premier via la directive COPY --from=builder.
# Vue d'ensemble (à détailler dans les sections suivantes)
# ┌─────────────────────────────┐
# │ STAGE 1 : builder │
# │ FROM node:20-alpine │
# │ npm ci → ng build │
# │ → produit /app/dist/ │
# └──────────────┬──────────────┘
# │ COPY --from=builder
# ▼
# ┌─────────────────────────────┐
# │ STAGE 2 : runtime │
# │ FROM nginx:1.27-alpine │
# │ COPY dist/ → /usr/share/... │
# │ EXPOSE 8080 + CMD nginx │
# └─────────────────────────────┘
Stage 1 — Build Node.js de l'application Angular
Le premier stage utilise node:20-alpine comme image de base. Alpine Linux est une distribution minimaliste (~5 MB) qui réduit considérablement la taille de l'étape builder. La cible : produire le dossier dist/<nom-app>/browser/ contenant les bundles optimisés pour la production.
Le Dockerfile complet — premier stage
# syntax=docker/dockerfile:1.7
# ============================================
# STAGE 1 : Build de l'application Angular
# ============================================
FROM node:20-alpine AS builder
# Métadonnées utiles pour le registry
LABEL org.opencontainers.image.title="Angular App Builder"
LABEL org.opencontainers.image.source="https://github.com/votre-org/votre-app"
# Définit le répertoire de travail dans le container
WORKDIR /app
# Copier UNIQUEMENT package.json et package-lock.json en premier
# Cette astuce permet au cache Docker de ne pas réinstaller les deps
# tant que le lockfile ne change pas (gain : 30-60s par build)
COPY package.json package-lock.json ./
# npm ci est plus rapide et déterministe que npm install pour CI/CD
# --no-audit et --no-fund évitent des appels réseau inutiles
RUN npm ci --no-audit --no-fund
# Maintenant on copie le reste du code source
# (les modifications de code n'invalident pas le cache des deps)
COPY . .
# Argument de build pour la configuration Angular (production, staging, etc.)
ARG BUILD_CONFIG=production
ENV NODE_ENV=production
# Build de production avec optimisations Angular activées
# - AOT compilation, tree-shaking, minification, hashing des assets
RUN npm run build -- --configuration=${BUILD_CONFIG}
# À ce stade, /app/dist/<nom-app>/browser/ contient les fichiers à servir
Le fichier .dockerignore — indispensable
Sans .dockerignore, la commande COPY . . envoie l'intégralité de votre dossier de travail au démon Docker — y compris node_modules local, .git, les fichiers IDE, les .env contenant des secrets. Le contexte de build peut atteindre plusieurs gigaoctets et fuiter des informations sensibles dans l'image.
# .dockerignore — placer à la racine du projet
# Ignore tout ce qui n'est pas indispensable au build
# Dépendances installées localement (réinstallées dans le container)
node_modules
npm-debug.log*
yarn-error.log*
# Build local (recompilé dans le container)
dist
.angular
# Outils de développement
.git
.github
.gitignore
.vscode
.idea
*.iml
# Configuration locale et secrets
.env
.env.local
.env.*.local
*.pem
*.key
# Tests et fichiers temporaires
coverage
.nyc_output
*.log
.DS_Store
Thumbs.db
# Documentation et CI (pas nécessaires au build)
README.md
CHANGELOG.md
docs/
*.md
docker build --no-cache -t test . 2>&1 | head -1. Si vous voyez "Sending build context" supérieur à 10 MB, votre .dockerignore est probablement incomplet.
Optimisation BuildKit — cache des node_modules
Avec Docker BuildKit (activé par défaut depuis Docker 23+), on peut utiliser un cache mount pour conserver le cache npm entre builds successifs, accélérant npm ci de 60-80% sur les builds répétés.
# Variante optimisée du stage builder avec cache mount BuildKit
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
# --mount=type=cache : monte un cache persistant pour npm
# Le cache survit entre builds mais n'est pas inclus dans l'image finale
RUN --mount=type=cache,target=/root/.npm \
npm ci --no-audit --no-fund --prefer-offline
COPY . .
ARG BUILD_CONFIG=production
RUN npm run build -- --configuration=${BUILD_CONFIG}
Stage 2 — Servir avec Nginx Alpine
Le second stage repart de zéro avec une image nginx:1.27-alpine (~ 20 MB). On ne copie que les fichiers compilés, pas les sources, pas Node, pas node_modules. C'est ce qui rend l'image finale si légère.
Stage runtime complet
# ============================================
# STAGE 2 : Runtime Nginx
# ============================================
FROM nginx:1.27-alpine AS runtime
# Métadonnées
LABEL org.opencontainers.image.title="Angular App Runtime"
LABEL org.opencontainers.image.description="Production-ready Angular SPA"
# Supprimer la configuration Nginx par défaut
RUN rm -rf /etc/nginx/conf.d/default.conf /usr/share/nginx/html/*
# Copier notre configuration Nginx personnalisée
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
# Copier UNIQUEMENT le résultat du build depuis le stage 'builder'
# Remplacer 'votre-app' par le nom du projet dans angular.json
COPY --from=builder /app/dist/votre-app/browser /usr/share/nginx/html
# Exposer le port 8080 (non-privilégié pour fonctionner non-root)
EXPOSE 8080
# Healthcheck : Docker pingue / toutes les 30s pour vérifier la santé du container
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:8080/ || exit 1
# Démarrage de Nginx en foreground (obligatoire pour Docker)
CMD ["nginx", "-g", "daemon off;"]
Vérifier la taille finale
# Construire l'image
docker build -t mon-angular:1.0.0 .
# Vérifier la taille
docker images mon-angular:1.0.0
# REPOSITORY TAG IMAGE ID CREATED SIZE
# mon-angular 1.0.0 a1b2c3d4e5f6 2 minutes ago 25.4MB
# Inspecter les couches pour identifier les optimisations possibles
docker history mon-angular:1.0.0
# Lancer le container en local pour test
docker run -d -p 8080:8080 --name angular-test mon-angular:1.0.0
# Tester l'accès
curl -I http://localhost:8080/
# HTTP/1.1 200 OK
# Server: nginx/1.27.x
# Content-Type: text/html
# Nettoyer
docker stop angular-test && docker rm angular-test
fr/, en/, de/). Vous pouvez soit copier tous les locales dans la même image et router via Nginx, soit créer une image par locale (image plus petite, déploiement par région).
Configuration nginx.conf : gzip, cache, SPA routing
La configuration Nginx par défaut est insuffisante pour une SPA Angular en production : pas de fallback index.html pour le routing client-side, pas de compression gzip, pas de headers de cache long-terme sur les assets hashés. Voici une configuration complète et commentée.
Le fichier docker/nginx.conf
# docker/nginx.conf — configuration Nginx pour Angular SPA en production
server {
# Port non-privilégié (autorise l'exécution non-root)
listen 8080;
listen [::]:8080;
server_name _;
# Racine des fichiers statiques (correspond à COPY dans le Dockerfile)
root /usr/share/nginx/html;
index index.html;
# Sécurité : cache headers protecteurs
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# ================================
# Compression gzip (optionnelle si front proxy le fait déjà)
# ================================
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_proxied any;
gzip_types
text/plain
text/css
text/javascript
text/xml
application/javascript
application/json
application/xml
application/xml+rss
application/wasm
image/svg+xml;
# ================================
# Cache long-terme pour les assets hashés (Angular génère main.abc123.js)
# ================================
location ~* \.(?:js|css|woff2?|ttf|eot|otf|svg|png|jpg|jpeg|gif|webp|ico)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
try_files $uri =404;
}
# ================================
# index.html : JAMAIS en cache (point d'entrée mis à jour à chaque déploiement)
# ================================
location = /index.html {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# ================================
# SPA routing : toute URL non trouvée renvoie index.html
# Permet au routeur Angular de gérer /dashboard, /profile, /products/42, etc.
# ================================
location / {
try_files $uri $uri/ /index.html;
}
# Bloquer l'accès aux fichiers cachés (.git, .env, etc.)
location ~ /\. {
deny all;
return 404;
}
# Page d'erreur custom (optionnel)
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
Configuration nginx.conf principale (optionnelle, pour non-root)
Pour faire fonctionner Nginx en utilisateur non-root, il faut aussi adapter le fichier /etc/nginx/nginx.conf principal afin que Nginx écrive ses fichiers temporaires et son pid dans des dossiers accessibles.
# docker/nginx-main.conf — fichier principal pour exécution non-root
worker_processes auto;
pid /tmp/nginx.pid;
error_log /var/log/nginx/error.log warn;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logs accessibles en non-root
access_log /var/log/nginx/access.log;
# Performances
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off; # Masque la version Nginx dans les headers
# Chemins temporaires en /tmp (writable par non-root)
client_body_temp_path /tmp/client_body;
fastcgi_temp_path /tmp/fastcgi_temp;
proxy_temp_path /tmp/proxy_temp;
scgi_temp_path /tmp/scgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
# Inclure les configurations de sites
include /etc/nginx/conf.d/*.conf;
}
try_files $uri $uri/ /index.html; est l'unique ligne qui active le SPA routing. Si vous l'oubliez, l'utilisateur qui rafraîchit /dashboard obtiendra un 404 — bug le plus fréquent en production Angular.
Sécurité : utilisateur non-root et healthcheck
Par défaut, les containers Docker s'exécutent en root. Si une faille permet de s'échapper du container, l'attaquant hérite des privilèges root sur l'hôte. La pratique recommandée : créer un utilisateur dédié et passer en mode non-root via USER.
Dockerfile complet, sécurisé et production-ready
# syntax=docker/dockerfile:1.7
# ====================================================
# Dockerfile production pour application Angular
# Multi-stage + non-root + healthcheck + image < 30MB
# ====================================================
# ---------- STAGE 1 : Build ----------
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --no-audit --no-fund --prefer-offline
COPY . .
ARG BUILD_CONFIG=production
ENV NODE_ENV=production
RUN npm run build -- --configuration=${BUILD_CONFIG}
# ---------- STAGE 2 : Runtime ----------
FROM nginx:1.27-alpine AS runtime
# Installer wget pour le healthcheck (déjà présent dans alpine, on le garde)
RUN apk add --no-cache wget
# Configurations Nginx custom
RUN rm -rf /etc/nginx/conf.d/default.conf /usr/share/nginx/html/*
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY docker/nginx-main.conf /etc/nginx/nginx.conf
# Copier les assets compilés depuis le stage builder
COPY --from=builder /app/dist/votre-app/browser /usr/share/nginx/html
# ---------- Sécurité : exécution non-root ----------
# Créer un user nginx-app non-privilégié
RUN addgroup -g 101 -S nginx-app 2>/dev/null || true \
&& adduser -S -D -H -u 101 -s /sbin/nologin -G nginx-app nginx-app 2>/dev/null || true
# Donner les bons droits sur les dossiers nécessaires à Nginx
RUN chown -R nginx-app:nginx-app /usr/share/nginx/html \
&& chown -R nginx-app:nginx-app /var/cache/nginx \
&& chown -R nginx-app:nginx-app /var/log/nginx \
&& mkdir -p /tmp/client_body /tmp/fastcgi_temp /tmp/proxy_temp /tmp/scgi_temp /tmp/uwsgi_temp \
&& chown -R nginx-app:nginx-app /tmp
# Basculer en utilisateur non-root
USER nginx-app
# Port non-privilégié (1024+, autorisé pour utilisateurs non-root)
EXPOSE 8080
# Healthcheck Docker — utilisé par Docker Swarm, Kubernetes (livenessProbe), etc.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:8080/ || exit 1
# Démarrer Nginx en foreground
CMD ["nginx", "-g", "daemon off;"]
Vérifier que le container tourne en non-root
# Construire et lancer
docker build -t mon-angular:1.0.0 .
docker run -d -p 8080:8080 --name angular-secure mon-angular:1.0.0
# Vérifier l'utilisateur effectif dans le container
docker exec angular-secure id
# uid=101(nginx-app) gid=101(nginx-app) groups=101(nginx-app)
# Vérifier le statut healthcheck
docker inspect --format='{{.State.Health.Status}}' angular-secure
# healthy
# Vérifier les processus
docker exec angular-secure ps aux
# PID USER COMMAND
# 1 nginx-app nginx: master process nginx -g daemon off;
# 7 nginx-app nginx: worker process
docker-compose.yml pour le développement
# docker-compose.yml — orchestration locale pour dev/staging
version: '3.9'
services:
angular-app:
build:
context: .
dockerfile: Dockerfile
args:
BUILD_CONFIG: production
image: mon-angular:latest
container_name: angular-app
restart: unless-stopped
ports:
- "8080:8080"
environment:
- TZ=Europe/Paris
# Limites de ressources (équivalents Kubernetes resources.limits)
deploy:
resources:
limits:
cpus: '0.5'
memory: 128M
reservations:
cpus: '0.1'
memory: 32M
# Sécurité supplémentaire
read_only: true
tmpfs:
- /tmp
- /var/cache/nginx
- /var/run
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
read_only: true rend le filesystem en lecture seule, cap_drop: ALL retire toutes les capabilities Linux, no-new-privileges empêche l'escalade de privilèges via setuid. Combinés à l'utilisateur non-root, vous obtenez un container quasi-incassable.
- Image de base récente (
nginx:1.27-alpine) et tag fixé (jamais:latesten prod) - Utilisateur non-root via
USER nginx-app - Port non-privilégié (8080, 8443) plutôt que 80/443
- Headers de sécurité : X-Frame-Options, X-Content-Type-Options, Referrer-Policy
server_tokens offpour masquer la version Nginx- Healthcheck Docker configuré (
HEALTHCHECK) .dockerignoreexclut.env,.git,node_modules- Pas de secrets dans les
ARGouENV(utiliser Docker secrets ou Kubernetes secrets) - Image scannée régulièrement (Trivy, Snyk)
Push registry Docker Hub / GHCR + scan Trivy
Une fois l'image construite, il faut la publier sur un registry pour que vos environnements (staging, prod, Kubernetes) puissent la déployer. Les options principales : Docker Hub (gratuit pour images publiques), GitHub Container Registry (GHCR) (gratuit, intégré aux repos GitHub), AWS ECR, Azure Container Registry, Google Artifact Registry.
Push sur Docker Hub
# 1. Se connecter à Docker Hub (utilise un token, pas le mot de passe)
docker login -u votreuser
# Password: dckr_pat_xxxxxxxxxxxxxxxxxxxxx
# 2. Tagger l'image avec le namespace correct
docker tag mon-angular:1.0.0 votreuser/mon-angular:1.0.0
docker tag mon-angular:1.0.0 votreuser/mon-angular:latest
# 3. Pousser les tags
docker push votreuser/mon-angular:1.0.0
docker push votreuser/mon-angular:latest
# 4. Vérifier en pull depuis une autre machine
docker pull votreuser/mon-angular:1.0.0
Push sur GitHub Container Registry (GHCR)
# GHCR est gratuit pour les repos publics et utilise vos credentials GitHub
# 1. Créer un Personal Access Token (PAT) avec scope write:packages
# https://github.com/settings/tokens
# 2. Login (CR_PAT contient votre token)
echo $CR_PAT | docker login ghcr.io -u votre-handle --password-stdin
# 3. Tagger pour ghcr.io
docker tag mon-angular:1.0.0 ghcr.io/votre-handle/mon-angular:1.0.0
# 4. Push
docker push ghcr.io/votre-handle/mon-angular:1.0.0
# 5. Pour rendre l'image publique : Settings du package → Change visibility → Public
Scanner les vulnérabilités avec Trivy
Trivy est un scanner open-source d'Aqua Security qui détecte les CVE des packages OS, des dépendances applicatives, et des fichiers de configuration. Léger, rapide, intégrable en CI.
# Installation Trivy (macOS / Linux via brew)
brew install trivy
# Ou via Docker (sans installation locale)
alias trivy='docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy'
# Scan basique de l'image
trivy image mon-angular:1.0.0
# Scan avec filtre sur les vulnérabilités HIGH et CRITICAL uniquement
trivy image --severity HIGH,CRITICAL mon-angular:1.0.0
# Scan + exit code 1 si CVE critique détecté (utile en CI/CD pour bloquer)
trivy image --severity HIGH,CRITICAL --exit-code 1 mon-angular:1.0.0
# Génération d'un rapport JSON pour intégration outil
trivy image --format json --output trivy-report.json mon-angular:1.0.0
# Ignorer les CVE non corrigeables (qui n'ont pas de patch disponible)
trivy image --ignore-unfixed --severity HIGH,CRITICAL mon-angular:1.0.0
Comparer deux versions avant push
# Comparer les vulnérabilités entre l'ancienne et la nouvelle version
trivy image --severity HIGH,CRITICAL mon-angular:0.9.0 > old.txt
trivy image --severity HIGH,CRITICAL mon-angular:1.0.0 > new.txt
diff old.txt new.txt
# Si la nouvelle version introduit des CVE critiques → ne pas pousser
docker push. Une image compromise sur un registry public peut être tirée par n'importe qui — y compris des CI tiers — et exposer votre infrastructure.
CI/CD avec GitHub Actions
L'objectif : à chaque push sur main, GitHub Actions construit l'image, la scanne avec Trivy, et la pousse sur GHCR avec le tag de la version Git. Aucune intervention manuelle.
Workflow GitHub Actions complet
# .github/workflows/docker-build-push.yml
name: Build & Push Docker Image
on:
push:
branches: [main]
tags: ['v*.*.*']
pull_request:
branches: [main]
# Permissions nécessaires pour GHCR (write:packages)
permissions:
contents: read
packages: write
security-events: write # Pour uploader les résultats Trivy dans Security tab
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
# 1. Récupérer le code
- name: Checkout
uses: actions/checkout@v4
# 2. Configurer Docker Buildx (nécessaire pour cache mounts et multi-arch)
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# 3. S'authentifier sur GHCR
- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 4. Extraire les métadonnées (tags + labels)
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-
# 5. Build et push
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILD_CONFIG=production
# 6. Scan Trivy de l'image construite
- name: Run Trivy vulnerability scanner
if: github.event_name != 'pull_request'
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'HIGH,CRITICAL'
exit-code: '0' # Ne pas échouer le build, juste reporter
# 7. Uploader les résultats dans GitHub Security tab
- name: Upload Trivy results to GitHub Security
if: github.event_name != 'pull_request'
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
# 8. Affichage info image
- name: Image digest
if: github.event_name != 'pull_request'
run: |
echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
echo "Tags: ${{ steps.meta.outputs.tags }}"
echo "Digest: ${{ steps.build.outputs.digest }}"
Job de déploiement (exemple : SSH vers VPS)
# Suite de docker-build-push.yml — déploiement sur un VPS
deploy-prod:
needs: build-and-push
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to VPS via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
# Récupérer la nouvelle image
docker pull ghcr.io/${{ github.repository }}:${{ github.ref_name }}
# Arrêter l'ancien container
docker stop angular-prod 2>/dev/null || true
docker rm angular-prod 2>/dev/null || true
# Lancer la nouvelle version
docker run -d \
--name angular-prod \
--restart unless-stopped \
-p 80:8080 \
--health-cmd="wget --quiet --tries=1 --spider http://localhost:8080/ || exit 1" \
--health-interval=30s \
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
# Nettoyer les vieilles images (garde les 3 dernières)
docker image prune -af --filter "until=168h"
Conclusion
Dockeriser une application Angular en production exige bien plus qu'un simple FROM node. Le pattern multi-stage build avec node:20-alpine en builder et nginx:1.27-alpine en runtime divise la taille de l'image par 50, élimine la surface d'attaque inutile et accélère drastiquement les déploiements. La clé : ne jamais exposer Node.js et node_modules dans l'image finale.
Couplé à une nginx.conf avec try_files pour le SPA routing, à un utilisateur non-root, à un healthcheck Docker, à un .dockerignore rigoureux et à un scan Trivy automatisé, vous obtenez une image de moins de 30 MB, conforme aux bonnes pratiques OWASP et prête pour Kubernetes, AWS ECS, Azure Container Apps ou Google Cloud Run.
- Dockerfile multi-stage :
builderNode +runtimeNginx Alpine - Image finale < 30 MB (vérifier avec
docker images) - Fichier
.dockerignoreexcluantnode_modules,.env,.git nginx.confavectry_files $uri $uri/ /index.html;- Headers de sécurité : X-Frame-Options, X-Content-Type-Options, Referrer-Policy
- Compression gzip activée pour
js,css,svg,json - Cache long-terme sur les assets hashés (1 an, immutable)
index.htmlenno-storepour récupérer les nouvelles versions immédiatement- Utilisateur
nginx-appnon-root (USER) - Port non-privilégié 8080 (pas 80)
HEALTHCHECKDocker actif- Image taggée avec la version Git (jamais
:latesten prod) - Scan Trivy intégré au pipeline CI/CD (bloquant sur HIGH/CRITICAL)
- Push sur registry privé ou GHCR avec tag versionné
docker-compose.ymlavecread_only,cap_drop: ALL,no-new-privileges