fr

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

  1. Modes de rendu Astro.js
  2. Images de base Docker — Comparatif
  3. Compatibilité Astro.js + Bun
  4. Adaptateurs Astro (SSR)
  5. Serveurs statiques ultra-légers
  6. Consommation RAM — Benchmarks
  7. Optimisation du build
  8. Dockerfiles de référence
  9. Matrice de décision
  10. Sources

1. Modes de rendu Astro.js

Astro 5 simplifie le modèle en fusionnant hybrid dans static :

ModeComportementImpact 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.
serverToutes 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)

ImageTaille approx.BaselibcShellNotes
node:22~1.1 GBDebian BookwormglibcouiInclut Python, GCC, make. Pour dev/CI uniquement.
node:22-slim~220 MBDebian Bookworm min.glibcouiPas de build tools. Bonne compat native modules.
node:22-alpine~150 MBAlpine LinuxmuslouiLe 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

ImageTaille approx.BaselibcShellNotes
gcr.io/distroless/nodejs22-debian12~141 MBDebian 12 min.glibcnonGoogle. Inclut certs TLS, tzdata. CMD en exec form obligatoire. Variante :debug avec busybox shell.
cgr.dev/chainguard/node~145 MBWolfi OSglibcnonChainguard. 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

ImageTaille approx.BaseNotes
oven/bun~85 MBDebian SlimShell inclus
oven/bun:slim~65 MBDebian Slim trim
oven/bun:alpine~55-60 MBAlpinemusl, même caveats que node:alpine
oven/bun:distroless~50-55 MBDistrolessLe 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)

ApprocheTaille finaleNotes
astefanutti/scratch-node~40-50 MBNode.js compilé statiquement avec musl. ICU partiel (anglais). Communautaire.
BusyBox httpd sur scratch~154 KBServeur HTTP minimal. Pour fichiers statiques uniquement.
darkhttpd sur scratch~300 KBServeur HTTP statique compilé. Plus de features que BusyBox.
thttpd sur scratch~186 KBAlternatif 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 create peut é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-sea simplifié
  • Taille binaire : ~80-90 MB (taille du binaire Node.js + code app)
  • Dynamiquement lié → pas compatible scratch non plus

Verdict Bun

UsageRecommandation
Package manager (bun install)Sûr, plus rapide que npm
Runtime de build (bun --bun run build)Risqué, peut OOM
SSR en productionNon recommandé — adaptateurs communautaires non maintenus, mémoire pire que Node.js
Sites statiquesOK (Bun n’est utilisé qu’au build, pas au runtime)

4. Adaptateurs Astro (SSR)

Adaptateurs officiels (maintenus par l’équipe Astro)

AdaptateurPackageTé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)

PackageStatut
@nurodev/astro-bun (v2.1.2)Semi-actif, 1 mainteneur
astro-bun-adapter (ido-pluto)Inactif (>1 an sans release)
@hedystia/astro-bunAlternatif 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.

ServeurLangageImage DockerRAMGzipTLSNotes
lipanski/docker-static-websiteC~80 KB + fichiers< 1 MBnonnonImage pré-construite basée sur BusyBox httpd.
BusyBox httpd (scratch)C~154 KB + fichiers< 1 MBnonnonLe plus petit possible. Pas de shell.
thttpd (scratch)C~186 KB + fichiers< 1 MBnonnonSimilaire à BusyBox.
darkhttpd (scratch)C~300 KB + fichiers< 1 MBnonnonBinaire statique unique. Directory listing, pages d’erreur custom.
static-web-serverRust~4 MB + fichiers~2 MBouiouiBinaire statique, gzip, brotli, TLS.
goStaticGo~5-6 MB + fichiers~2-3 MBnonnonServeur Go minimaliste pour Docker.
miniserveRust~5-8 MB + fichiers~2-3 MBouinonDirectory listing, upload, auth basique.
nginx:stable-alpine-slimC~12 MB + fichiers~2-5 MBouiouiProduction-grade. Gzip, cache headers, bien documenté.
nginx:alpineC~40 MB + fichiers~2-5 MBouiouiAlpine complet avec plus d’outils.
Caddy (scratch)Go~40-50 MB + fichiers~10-15 MBouioui autoAuto-TLS, HTTP/3. Binaire Go statique.
Node.js serveJS~120 MB + fichiers~30-50 MBouinonMassivement overkill pour du statique.

Recommandations par cas d’usage

  1. Footprint absolu minimum → BusyBox httpd ou lipanski/docker-static-website (~80-154 KB)
  2. Bon compromis taille/featuresnginx:stable-alpine-slim (~12 MB) — gzip, cache headers, battle-tested
  3. Besoin de compression + features légèresstatic-web-server (Rust, ~4 MB) — gzip, brotli, TLS
  4. 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)

RuntimeRSS idleNotes
BusyBox httpd< 1 MB
darkhttpd< 1 MB
static-web-server (Rust)~2 MB
nginx~2-5 MBworker_processes 1
Node.js 22 (empty process)~10-25 MBDépend des modules chargés
Node.js 22 (HTTP server simple)~30-50 MBAvec Express/Fastify
Bun (HTTP server idle)~110-150 MBCommence à ~110 MB, monte à ~145 MB même sans requêtes (issue #21560)

Astro SSR spécifiquement

ConfigurationRSS 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:load ou client: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-size si 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

WorkloadLimite mémoire Docker--max-old-space-size
Serveur statique (BusyBox/nginx)8-16 MBN/A
Astro SSR minimal (standalone)128-256 MB96-192 MB
Astro SSR moyen avec cache256-512 MB192-384 MB
Astro SSR + contenu dynamique lourd512 MB - 1 GB384-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 projetRAM 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
  • .mdx est 6-12x plus lent et gourmand que .md simple
  • 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 à .mdx quand possible (6-12x moins de RAM)
  • Sortir les gros fichiers de données des Content Collections → utiliser fs de 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 (pas npm install) pour des builds reproductibles
  • npm ci --omit=dev ou pnpm install --prod pour 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èreSite 100% statiqueSite SSR / Hybride
Astro outputstatic (défaut)server avec prerender: true où possible
AdaptateurAucun@astrojs/node (standalone)
Image buildnode:22-alpinenode:22-alpine
Image runtimeBusyBox/scratch ou nginx:alpine-slimnode:22-alpine ou distroless
Taille image80 KB – 12 MB + site141 – 200 MB + app
RAM runtime< 1 – 5 MB50 – 150 MB
ComplexitéFaibleMoyenne

Classement des options par taille d’image (runtime)

#OptionTailleRAMPour
1BusyBox httpd / scratch~80-154 KB< 1 MBStatique uniquement
2darkhttpd / scratch~300 KB< 1 MBStatique uniquement
3static-web-server (Rust)~4 MB~2 MBStatique avec gzip/brotli
4nginx:alpine-slim~12 MB~2-5 MBStatique, production-grade
5Bun distroless~50 MB~110-150 MBSSR (risqué)
6Google distroless Node~141 MB~50-150 MBSSR, sécurité maximale
7Chainguard Node~145 MB~50-150 MBSSR, zéro CVE
8Node.js Alpine~150 MB~50-150 MBSSR, polyvalent
9Node.js Slim~220 MB~50-150 MBSSR, 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

Docker & Images

Distroless & Chainguard

Bun

Node.js Memory & Containers

Deno

Astro SSR Issues

Serveurs statiques

Benchmarks & Comparatifs