Optimisation Docker pour sites Astro.js — Recherche complète
Recherche réalisée le 18/02/2026 — 3 agents de recherche parallèles, ~30 sources consultées.
Table des matières
- Modes de rendu Astro.js
- Images de base Docker — Comparatif
- Compatibilité Astro.js + Bun
- Adaptateurs Astro (SSR)
- Serveurs statiques ultra-légers
- Consommation RAM — Benchmarks
- Optimisation du build
- Dockerfiles de référence
- Matrice de décision
- Sources
1. Modes de rendu Astro.js
Astro 5 simplifie le modèle en fusionnant hybrid dans static :
| Mode | Comportement | Impact Docker |
|---|---|---|
static (défaut) | Toutes les pages pré-rendues en HTML/CSS/JS au build. Output dans dist/. | Meilleur pour le footprint minimal. Aucun runtime JS nécessaire — un simple serveur HTTP suffit. Image finale < 1 MB possible. |
server | Toutes les pages rendues à la demande (SSR). Nécessite un adaptateur (Node.js, Deno…). | Plus lourd. Runtime JS obligatoire (~120 MB Node.js alpine, ~55 MB Bun alpine). RAM runtime 50-200 MB. |
| Hybride (Astro 5) | output: 'static' + adaptateur. Pages individuelles opt-in SSR via export const prerender = false. | Runtime JS nécessaire, mais les pages pré-rendues sont servies en statique — réduit la charge CPU/RAM par requête. |
Recommandation : Si le site peut être entièrement statique, utiliser output: 'static' (défaut). C’est le gain le plus important — il élimine le besoin d’un runtime JavaScript en production.
2. Images de base Docker — Comparatif
Images Node.js officielles (Node 22, amd64)
| Image | Taille approx. | Base | libc | Shell | Notes |
|---|---|---|---|---|---|
node:22 | ~1.1 GB | Debian Bookworm | glibc | oui | Inclut Python, GCC, make. Pour dev/CI uniquement. |
node:22-slim | ~220 MB | Debian Bookworm min. | glibc | oui | Pas de build tools. Bonne compat native modules. |
node:22-alpine | ~150 MB | Alpine Linux | musl | oui | Le plus petit avec Node. Attention aux modules natifs (musl vs glibc). |
glibc vs musl — Le tradeoff fondamental
- glibc (Debian/Ubuntu) : la plupart des modules npm natifs publient des binaires pré-compilés pour glibc. Compatibilité quasi-universelle.
- musl (Alpine) : libc légère et conforme aux standards. Certains modules natifs nécessitent des binaires musl séparés ou une compilation depuis les sources.
- sharp (traitement d’images) : supporte glibc et musl sur x64/arm64. Publie des binaires séparés pour chaque.
- Règle générale : si un module natif publie des binaires pré-compilés, vérifier que musl/Alpine est dans leur matrice de build. Sinon, installer les build tools sur Alpine ajoute 100-150 MB et annule l’avantage de taille.
Images Distroless
| Image | Taille approx. | Base | libc | Shell | Notes |
|---|---|---|---|---|---|
gcr.io/distroless/nodejs22-debian12 | ~141 MB | Debian 12 min. | glibc | non | Google. Inclut certs TLS, tzdata. CMD en exec form obligatoire. Variante :debug avec busybox shell. |
cgr.dev/chainguard/node | ~145 MB | Wolfi OS | glibc | non | Chainguard. Zéro CVE connues. :latest-dev inclut shell + apk. |
Avantages distroless : surface d’attaque réduite (pas de shell = pas d’exploit shell), moins de CVEs. Inconvénients : pas de debugging interactif (sauf variante debug), CMD exec form obligatoire, multi-stage build obligatoire.
Images Bun
| Image | Taille approx. | Base | Notes |
|---|---|---|---|
oven/bun | ~85 MB | Debian Slim | Shell inclus |
oven/bun:slim | ~65 MB | Debian Slim trim | |
oven/bun:alpine | ~55-60 MB | Alpine | musl, même caveats que node:alpine |
oven/bun:distroless | ~50-55 MB | Distroless | Le plus petit, pas de shell |
Le binaire Bun fait ~30-35 MB vs ~80-90 MB pour Node.js+V8+ICU → toutes les variantes Bun sont plus petites en taille d’image.
Images scratch (0 octets de base)
| Approche | Taille finale | Notes |
|---|---|---|
astefanutti/scratch-node | ~40-50 MB | Node.js compilé statiquement avec musl. ICU partiel (anglais). Communautaire. |
| BusyBox httpd sur scratch | ~154 KB | Serveur HTTP minimal. Pour fichiers statiques uniquement. |
| darkhttpd sur scratch | ~300 KB | Serveur HTTP statique compilé. Plus de features que BusyBox. |
| thttpd sur scratch | ~186 KB | Alternatif minimaliste. |
3. Compatibilité Astro.js + Bun
Statut actuel (fin 2025 / début 2026)
La doc officielle Astro avertit :
“Using Bun with Astro may reveal rough edges, and some integrations may not work as expected.”
Bun comme package manager (faible risque)
bun install # Remplace npm install — plus rapide
bun run build # Exécute astro build via Node.js par défaut
Même avec bun run, le build utilise Node.js par défaut. Pour forcer le runtime Bun : bun --bun run build.
Bun comme runtime (risqué)
Problèmes connus :
- Astro requiert Node.js installé même sous Bun (issue #9438)
- Échecs de parsing JSON pour les requêtes POST en SSR (bun issue #6807)
- Installation d’intégrations via
bun createpeut échouer (issue #14833) - Mémoire : Contrairement au marketing, Bun peut consommer PLUS de RAM que Node.js sous charge. Un rapport montre Node.js à ~500 MB vs Bun crashant à la limite de 1.2 GB du container (bun issue #17723). Haute consommation RAM au build aussi (bun issue #25487).
Bun --compile (single binary)
Bun peut compiler du TS/JS en un binaire autonome :
bun build --compile --minify --bytecode ./app.ts --outfile myapp
- Le binaire inclut le runtime Bun complet (JavaScriptCore) : ~51-57 MB sur arm64, ~100 MB sur Windows
- Cross-compilation supportée :
--target=bun-linux-x64,--target=bun-linux-arm64 - Les binaires sont dynamiquement liés à glibc → pas compatible scratch, nécessite au minimum une base distroless
- Issue ouverte pour un runtime minimal
Node.js SEA (Single Executable Applications)
- Node.js v20+ : support expérimental SEA
- Node.js v25.5+ (janvier 2026) : nouveau flag
--build-seasimplifié - Taille binaire : ~80-90 MB (taille du binaire Node.js + code app)
- Dynamiquement lié → pas compatible scratch non plus
Verdict Bun
| Usage | Recommandation |
|---|---|
Package manager (bun install) | Sûr, plus rapide que npm |
Runtime de build (bun --bun run build) | Risqué, peut OOM |
| SSR en production | Non recommandé — adaptateurs communautaires non maintenus, mémoire pire que Node.js |
| Sites statiques | OK (Bun n’est utilisé qu’au build, pas au runtime) |
4. Adaptateurs Astro (SSR)
Adaptateurs officiels (maintenus par l’équipe Astro)
| Adaptateur | Package | Téléchargements/sem. |
|---|---|---|
| Node.js | @astrojs/node | ~7.4K |
| Cloudflare | @astrojs/cloudflare | ~424K |
| Vercel | @astrojs/vercel | ~204K |
| Netlify | @astrojs/netlify | ~147K |
Adaptateurs Bun (communautaires, NON officiels)
| Package | Statut |
|---|---|
@nurodev/astro-bun (v2.1.2) | Semi-actif, 1 mainteneur |
astro-bun-adapter (ido-pluto) | Inactif (>1 an sans release) |
@hedystia/astro-bun | Alternatif communautaire |
Il n’existe pas d’adaptateur officiel @astrojs/bun.
Pour Docker SSR : @astrojs/node est le choix clair — officiel, testé, bien documenté. Supporte les modes standalone et middleware.
Mode standalone vs middleware
- Standalone : le build produit un serveur autonome qui démarre tout seul. Idéal pour Docker (pas besoin de code serveur supplémentaire).
- Middleware : le build produit un handler à intégrer dans Express/Fastify. Plus de contrôle, mais plus de code à maintenir.
5. Serveurs statiques ultra-légers
C’est ici que les gains les plus importants sont possibles. Une fois Astro buildé en statique, il faut juste le plus petit serveur HTTP possible.
| Serveur | Langage | Image Docker | RAM | Gzip | TLS | Notes |
|---|---|---|---|---|---|---|
| lipanski/docker-static-website | C | ~80 KB + fichiers | < 1 MB | non | non | Image pré-construite basée sur BusyBox httpd. |
| BusyBox httpd (scratch) | C | ~154 KB + fichiers | < 1 MB | non | non | Le plus petit possible. Pas de shell. |
| thttpd (scratch) | C | ~186 KB + fichiers | < 1 MB | non | non | Similaire à BusyBox. |
| darkhttpd (scratch) | C | ~300 KB + fichiers | < 1 MB | non | non | Binaire statique unique. Directory listing, pages d’erreur custom. |
| static-web-server | Rust | ~4 MB + fichiers | ~2 MB | oui | oui | Binaire statique, gzip, brotli, TLS. |
| goStatic | Go | ~5-6 MB + fichiers | ~2-3 MB | non | non | Serveur Go minimaliste pour Docker. |
| miniserve | Rust | ~5-8 MB + fichiers | ~2-3 MB | oui | non | Directory listing, upload, auth basique. |
| nginx:stable-alpine-slim | C | ~12 MB + fichiers | ~2-5 MB | oui | oui | Production-grade. Gzip, cache headers, bien documenté. |
| nginx:alpine | C | ~40 MB + fichiers | ~2-5 MB | oui | oui | Alpine complet avec plus d’outils. |
| Caddy (scratch) | Go | ~40-50 MB + fichiers | ~10-15 MB | oui | oui auto | Auto-TLS, HTTP/3. Binaire Go statique. |
| Node.js serve | JS | ~120 MB + fichiers | ~30-50 MB | oui | non | Massivement overkill pour du statique. |
Recommandations par cas d’usage
- Footprint absolu minimum → BusyBox httpd ou
lipanski/docker-static-website(~80-154 KB) - Bon compromis taille/features →
nginx:stable-alpine-slim(~12 MB) — gzip, cache headers, battle-tested - Besoin de compression + features légères →
static-web-server(Rust, ~4 MB) — gzip, brotli, TLS - Besoin de TLS auto → Caddy (~40 MB) — mais dans un setup Traefik, le TLS est géré par Traefik
Note Traefik : Avec Traefik comme reverse proxy, le TLS est déjà géré en amont. Le serveur statique n’a besoin que de servir du HTTP. Cela élimine le besoin de Caddy/nginx pour le TLS et permet d’utiliser les serveurs les plus légers.
6. Consommation RAM — Benchmarks
Baseline (process idle, serveur HTTP minimal)
| Runtime | RSS idle | Notes |
|---|---|---|
| BusyBox httpd | < 1 MB | |
| darkhttpd | < 1 MB | |
| static-web-server (Rust) | ~2 MB | |
| nginx | ~2-5 MB | worker_processes 1 |
| Node.js 22 (empty process) | ~10-25 MB | Dépend des modules chargés |
| Node.js 22 (HTTP server simple) | ~30-50 MB | Avec Express/Fastify |
| Bun (HTTP server idle) | ~110-150 MB | Commence à ~110 MB, monte à ~145 MB même sans requêtes (issue #21560) |
Astro SSR spécifiquement
| Configuration | RSS idle estimé |
|---|---|
| Astro SSR (Node adapter, standalone) | ~50-80 MB |
Astro SSR avec hydratation client (client:load) | ~80-150 MB, peut croître dans le temps |
| Astro SSR (Deno adapter) | ~40-70 MB (estimé, même moteur V8) |
- Problème connu : avec
client:loadouclient:idle, la mémoire augmente continûment (possible memory leak, issue #11951) - Le mode streaming aide à libérer la mémoire plus rapidement entre les requêtes
- Standalone mode est le plus léger — il évite de charger Express/Fastify entièrement
Bun vs Node.js — La réalité
Contrairement au marketing de Bun :
- RSS idle Bun ~110-150 MB vs Node.js ~10-50 MB pour un serveur simple
- Sous charge, Bun peut consommer significativement plus de RAM que Node.js
- Un cas documenté : Node.js à ~500 MB vs Bun crashant à 1.2 GB pour la même app (issue #17723)
Tuning V8 pour réduire la RAM (Node.js SSR)
# --max-old-space-size : contrôle la taille max du Old Space (objets long-lived). Knob principal.
# --max-semi-space-size : contrôle le New Space (young gen). Défaut : 16 MB par semi-space.
# Réduire à 2 MB économise ~28 MB mais augmente la fréquence du GC.
NODE_OPTIONS="--max-old-space-size=192 --max-semi-space-size=2"
Règle : fixer --max-old-space-size à 75% de la limite mémoire Docker. Ex : avec --memory=256m, utiliser --max-old-space-size=192. Les 25% restants couvrent la mémoire non-heap (buffers natifs, stack, objets C++).
Node.js container-aware memory (v20+)
Node.js 20+ est “container-aware” pour le heap V8 :
- Le heap max est automatiquement limité à 50% de la mémoire du container, jusqu’à 4 Gi
- Au-delà de 4 Gi, le heap plafonne à 2 Gi
- Compatible cgroups v2 à partir de Node.js 20, amélioré en Node.js 22
- Pas besoin de
--max-old-space-sizesi les limites Docker sont correctement configurées - Mais fixer explicitement permet d’utiliser 75% au lieu de 50% → meilleure utilisation de la RAM allouée
Limites Docker recommandées
| Workload | Limite mémoire Docker | --max-old-space-size |
|---|---|---|
| Serveur statique (BusyBox/nginx) | 8-16 MB | N/A |
| Astro SSR minimal (standalone) | 128-256 MB | 96-192 MB |
| Astro SSR moyen avec cache | 256-512 MB | 192-384 MB |
| Astro SSR + contenu dynamique lourd | 512 MB - 1 GB | 384-768 MB |
Docker Compose — Limiter la mémoire
services:
web:
image: my-astro-site
deploy:
resources:
limits:
memory: 256M # Maximum absolu — OOM kill au-delà
cpus: '0.5'
reservations:
memory: 128M # Garanti par le système
cpus: '0.25'
# Syntaxe v2.4 (fonctionne sans Swarm) :
# mem_limit: 256m
# mem_reservation: 128m
Notes OOM Killer :
- Quand un container dépasse sa limite, le kernel envoie SIGKILL — pas de shutdown gracieux possible
- Désactiver le swap pour un comportement prévisible :
--memory-swap=--memory - Recommandation : 20-30% de marge au-dessus de l’usage constaté en test de charge
7. Optimisation du build
Multi-stage builds (obligatoire)
Toujours séparer les étapes : dépendances → build → runtime. Le build stage est jeté — seul le runtime stage compte pour la taille finale.
.dockerignore
node_modules
dist
.git
.env*
*.md
.vscode
.idea
tests/
coverage/
Consommation RAM de astro build
| Taille du projet | RAM estimée au build |
|---|---|
| Petit (dizaines de pages) | 200-500 MB |
| Moyen (centaines de pages, MDX) | 1-2.5 GB |
| Gros (10 000+ pages, content collections) | 2.5-4+ GB, risque d’OOM |
Facteurs aggravants :
- Content Collections charge tous les fichiers en mémoire
.mdxest 6-12x plus lent et gourmand que.mdsimple- Les références circulaires dans le contenu empêchent le garbage collection
Node.js — Tuning mémoire pour gros sites
Pour les sites avec des centaines/milliers de pages :
ENV NODE_OPTIONS="--max_old_space_size=2048"
RUN npm run build
Autres conseils pour réduire la consommation au build :
- Préférer
.mdà.mdxquand possible (6-12x moins de RAM) - Sortir les gros fichiers de données des Content Collections → utiliser
fsde Node.js directement - S’assurer que la machine de build dispose d’au moins 1-2 GB de RAM disponible
Astro — Concurrence du build
Dans astro.config.mjs :
export default defineConfig({
build: {
concurrency: 2, // Plus bas = moins de RAM, build plus lent
},
});
Bun pour l’install seulement (approche hybride)
Bun comme package manager dans le build stage, Node.js pour le build :
FROM oven/bun:alpine AS deps
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
pnpm — Cache mount BuildKit
FROM node:22-alpine AS builder
RUN corepack enable
WORKDIR /app
COPY pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
pnpm fetch
COPY . .
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --offline
RUN pnpm run build
Bonnes pratiques communes
npm ci(pasnpm install) pour des builds reproductiblesnpm ci --omit=devoupnpm install --prodpour les deps de production- Séparer l’install des deps de la copie du code pour le cache Docker :
COPY package*.json ./ RUN npm ci COPY . . # Le cache n'est invalidé que si package.json change - Combiner les RUN pour minimiser les layers :
RUN npm ci --omit=dev && npm cache clean --force - USER non-root en production :
USER node # Alpine/Debian # ou pour distroless : FROM gcr.io/distroless/nodejs22-debian12:nonroot
8. Dockerfiles de référence
A. Site statique — Footprint minimum absolu (~154 KB + site)
# Build stage
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime stage — BusyBox httpd sur scratch
FROM busybox:musl AS busybox
FROM scratch
COPY --from=busybox /bin/busybox /bin/busybox
COPY --from=build /app/dist/ /var/www/
EXPOSE 3000
CMD ["/bin/busybox", "httpd", "-f", "-v", "-p", "3000", "-h", "/var/www"]
Ou avec l’image pré-construite :
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM lipanski/docker-static-website:latest
COPY --from=build /app/dist/ .
B. Site statique — Bon compromis (nginx ~12 MB + site)
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:stable-alpine-slim
COPY --from=build /app/dist/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
Avec nginx.conf minimal :
worker_processes 1;
events { worker_connections 512; }
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /404.html =404;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
}
C. Site SSR — Node.js Alpine (~150 MB + app)
# Dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Build
FROM deps AS build
COPY . .
RUN npm run build
# Production deps only
FROM node:22-alpine AS prod-deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
# Runtime
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER node
EXPOSE 4321
CMD ["node", "dist/server/entry.mjs"]
D. Site SSR — Distroless (~141 MB + app)
FROM node:22 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM deps AS build
COPY . .
RUN npm run build
FROM node:22 AS prod-deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM gcr.io/distroless/nodejs22-debian12
WORKDIR /app
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
EXPOSE 4321
CMD ["dist/server/entry.mjs"]
9. Matrice de décision
Par type de site
| Critère | Site 100% statique | Site SSR / Hybride |
|---|---|---|
| Astro output | static (défaut) | server avec prerender: true où possible |
| Adaptateur | Aucun | @astrojs/node (standalone) |
| Image build | node:22-alpine | node:22-alpine |
| Image runtime | BusyBox/scratch ou nginx:alpine-slim | node:22-alpine ou distroless |
| Taille image | 80 KB – 12 MB + site | 141 – 200 MB + app |
| RAM runtime | < 1 – 5 MB | 50 – 150 MB |
| Complexité | Faible | Moyenne |
Classement des options par taille d’image (runtime)
| # | Option | Taille | RAM | Pour |
|---|---|---|---|---|
| 1 | BusyBox httpd / scratch | ~80-154 KB | < 1 MB | Statique uniquement |
| 2 | darkhttpd / scratch | ~300 KB | < 1 MB | Statique uniquement |
| 3 | static-web-server (Rust) | ~4 MB | ~2 MB | Statique avec gzip/brotli |
| 4 | nginx:alpine-slim | ~12 MB | ~2-5 MB | Statique, production-grade |
| 5 | Bun distroless | ~50 MB | ~110-150 MB | SSR (risqué) |
| 6 | Google distroless Node | ~141 MB | ~50-150 MB | SSR, sécurité maximale |
| 7 | Chainguard Node | ~145 MB | ~50-150 MB | SSR, zéro CVE |
| 8 | Node.js Alpine | ~150 MB | ~50-150 MB | SSR, polyvalent |
| 9 | Node.js Slim | ~220 MB | ~50-150 MB | SSR, compat native modules |
La question clé
Le site a-t-il besoin de SSR ?
- Non → Site statique. Image < 12 MB, RAM < 5 MB. C’est un ordre de grandeur de différence.
- Oui → Maximiser les pages
prerender: true. Utiliser Node.js Alpine ou distroless. ~150 MB image, ~50-150 MB RAM.
10. Sources
Documentation officielle Astro
- Build your Astro site with Docker
- Use Bun with Astro
- On-Demand Rendering
- Astro 5.0 Release
- Node.js Adapter
- Scaling Astro to 10,000+ Pages
Docker & Images
- The smallest Docker image to serve static websites
- lipanski/docker-static-website (GitHub)
- Building an Ultra-lightweight Web Server with Busybox
- iximiuz Labs: How to Choose Node.js Container Image
- Container Images Guide: Alpine, Slim, Distroless
- Reducing Docker Image Size: From 1.2GB to 150MB (BetterStack)
- Snyk: Choosing the Best Node.js Docker Image
- Snyk: 10 Best Practices to Containerize Node.js
- pnpm Docker Guide
- Depot: Optimal Dockerfile for Node.js with pnpm
- How to Choose Between Scratch and Distroless
Distroless & Chainguard
Bun
- oven/bun Docker Hub
- Bun Single-file Executables
- Bun Issue #17723: Container CPU/memory spike
- Bun Issue #25487: High RAM during build
- Bun Issue #21560: RSS grows while idle
- Bun Issue #14546: Minimal runtime for compiled binaries
Node.js Memory & Containers
- Node.js Understanding and Tuning Memory
- Optimising Node.js: max-semi-space-size (Nearform)
- Node.js 20+ memory management in containers (Red Hat)
- cgroups v2 impact on Node.js (Red Hat)
- Node.js 20 HEAP issues with Kubernetes (Deezer)
- Node.js Resource Management Tips
- Node.js SEA Documentation
- scratch-node: Distroless Node.js Docker Images
Deno
Astro SSR Issues
- Astro Issue #11951: Memory leak in SSR
- Astro Issue #10485: Out of memory during build
- Astro Issue #14893: OOM errors building medium blogs
- JavaScript Heap Out Of Memory With Astro
Serveurs statiques
- docker-darkhttpd (GitHub)
- goStatic (GitHub)
- static-web-server (Docker Hub)
- Docker Compose Deploy Specification