Comment Dockeriser une Application Bun
Dockerisez une app Bun avec un Dockerfile prêt pour la production, .dockerignore, liaison 0.0.0.0, healthchecks, Compose et arrêt SIGTERM.
Un Dockerfile crédible pour la production d’un service Bun tient en quelques lignes, mais la différence entre un Dockerfile qui fonctionne en local et un qui survit à un déploiement repose sur quatre éléments : une image de base épinglée, un utilisateur non-root, un service lié à 0.0.0.0, et un gestionnaire SIGTERM qui draine les requêtes en cours. Cet article vous fournit le Dockerfile fonctionnel, puis comble chacune de ces lacunes avec du code prêt à copier-coller.
Vous disposez d’une application Bun — une API, un worker, ou un backend-for-frontend — qui fonctionne sur votre machine. Vous devez la conteneuriser sans apprendre les particularités Docker de Bun à la dure. Voici ce que vous trouverez ci-dessous : le Dockerfile minimal, la mise en cache des layers, un binaire compilé multi-stage, le durcissement pour la production, Docker Compose avec Postgres, un gestionnaire d’arrêt gracieux pour le serveur intégré de Bun, et un tableau des quatre échecs silencieux les plus courants avec leurs correctifs.
Points Clés
- Un service Bun qui écoute uniquement sur
127.0.0.1à l’intérieur d’un conteneur est inaccessible via un port publié ; liez-le à0.0.0.0(toutes les interfaces IPv4) pour que la publication de ports Docker puisse lui transférer le trafic. - Bun 1.2, sorti le 22 janvier 2025, a modifié le format par défaut du lockfile en faveur du format texte
bun.lock; migrez depuisbun.lockbavecbun install --save-text-lockfile --frozen-lockfile --lockfile-only. - Les références d’images Docker utilisent par défaut
latestlorsqu’aucun tag n’est spécifié, ce qui peut modifier implicitement votre runtime Bun entre les déploiements ; épinglez un tag explicite tel queoven/bun:1. - Copiez
package.jsonetbun.lockavant le code source de l’application afin que le cache de layers Docker ignorebun installlors des reconstructions où seul le code source a changé. - Sans gestionnaire SIGTERM, Docker envoie SIGTERM, attend jusqu’à son délai d’arrêt par défaut (10 secondes pour les conteneurs Linux), puis envoie SIGKILL si le processus ne s’est pas terminé, ce qui risque de perdre des requêtes en cours de traitement.
Le Dockerfile Minimal
Quatre lignes suffisent pour placer une application Bun dans un conteneur. C’est le chemin le plus rapide entre « ça fonctionne en local » et une image construite.
FROM oven/bun
COPY . .
RUN bun install
CMD ["bun", "run", "src/index.ts"]
Construisez-la :
docker build -t bun-app .
docker run -p 3000:3000 bun-app
Bun exécute TypeScript directement, il n’y a donc pas d’étape de build tsc — le point d’entrée est votre fichier .ts. Remplacez src/index.ts par votre véritable point d’entrée. L’image de base est l’image officielle oven/bun sur Docker Hub.
Utilisez ceci pour un environnement de test, pas pour la production. La ligne FROM oven/bun tire latest implicitement, copie l’intégralité de votre répertoire (y compris node_modules et .env), s’exécute en tant que root, et reconstruit les dépendances à chaque modification du code. La suite de cet article corrige chacun de ces points.
Ajouter un .dockerignore
Discover how at OpenReplay.com.
Un .dockerignore allège votre contexte de build et empêche les secrets et artefacts locaux d’être copiés dans l’image. Créez-le avant toute autre chose.
node_modules
.git
.gitignore
.env
.env.*
dist
*.log
.vscode
README.md
Deux entrées sont particulièrement importantes. node_modules est exclu car bun install le régénère à l’intérieur de l’image à partir de votre lockfile — copier le node_modules de votre hôte embarquerait des binaires spécifiques à la plateforme qui pourraient ne pas correspondre au conteneur. .env et .env.* sont exclus pour que les secrets locaux n’atterrissent jamais dans un layer d’image, où ils persistent même si un layer ultérieur supprime le fichier.
Dois-je Utiliser bun.lock ou bun.lockb dans Mon Dockerfile ?
Optez par défaut pour bun.lock. Bun 1.2, sorti le 22 janvier 2025, a modifié le format par défaut du lockfile en faveur d’un fichier bun.lock textuel, remplaçant l’ancien bun.lockb binaire. Les anciens tutoriels font encore référence à bun.lockb, et copier le mauvais nom de fichier dans votre Dockerfile produit une erreur --frozen-lockfile difficile à interpréter.
Si votre dépôt possède encore un lockfile binaire, migrez-le une fois pour toutes :
bun install --save-text-lockfile --frozen-lockfile --lockfile-only
# puis supprimez bun.lockb et committez bun.lock
Cette commande est documentée dans la documentation du lockfile Bun. Après la migration, référencez bun.lock partout dans votre Dockerfile.
Mise en Cache des Layers : Séparer les Dépendances du Code Source
Copiez package.json et bun.lock avant votre code source applicatif afin que le cache de layers Docker ignore bun install à chaque reconstruction où seuls les fichiers source ont changé — c’est la modification à l’impact le plus élevé pour la vitesse d’itération en local.
FROM oven/bun:1
WORKDIR /app
# Copier le lockfile et le manifeste en premier — ce layer n'est invalidé
# que lorsque les dépendances changent
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
# Copier les sources en dernier — modifier le code ne reconstruit qu'à partir d'ici
COPY src ./src
COPY tsconfig.json ./
CMD ["bun", "run", "src/index.ts"]
Docker met en cache chaque instruction sous forme de layer et réutilise les layers mis en cache lorsque leurs entrées sont inchangées. Étant donné que COPY package.json bun.lock précède COPY src, modifier un fichier source laisse le layer des dépendances intact et bun install est entièrement ignoré. bun install --frozen-lockfile fait échouer le build si bun.lock n’est pas synchronisé avec package.json — comportement correct dans un Dockerfile, car il révèle les dérives du lockfile au moment du build plutôt qu’à l’exécution. Le comportement de ce flag est documenté dans la documentation CLI de bun install.
Notez la ligne COPY tsconfig.json. Bun lit tsconfig.json pour les alias de chemins et les options du compilateur ; si votre application utilise le mapping de chemins et que vous oubliez de le copier, la résolution des modules échoue au démarrage dans le conteneur alors qu’elle fonctionne en local.
Build Multi-Stage : Compiler vers un Binaire Autonome
bun build --compile crée un exécutable autonome qui regroupe votre code, les packages npm, les assets et le runtime Bun, de sorte que l’image finale n’a pas besoin de Bun installé séparément. Utilisez-le dans un build multi-stage pour ne copier que le binaire dans une base minimale, afin d’obtenir une image finale plus légère.
# --- Étape de build ---
FROM oven/bun:1 AS build
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY src ./src
COPY tsconfig.json ./
# Compiler vers un exécutable unique qui inclut le runtime Bun
RUN bun build ./src/index.ts --compile --outfile server
# --- Étape d'exécution ---
FROM debian:12-slim
WORKDIR /app
COPY --from=build /app/server /app/server
USER nobody
EXPOSE 3000
CMD ["/app/server"]
L’étape de build installe les dépendances et compile. L’étape d’exécution part de debian:12-slim et n’y copie que le binaire server compilé — pas d’image Bun, pas de node_modules, pas de source. Le binaire inclut le runtime, il s’exécute donc sans Bun présent. Mesurez le delta de taille sur votre propre application avant de citer un chiffre ; il dépend du nombre de dépendances et de la taille des assets.
Durcissement pour la Production
Épinglez un tag Bun explicite, exécutez en tant qu’utilisateur non-root, et installez uniquement les dépendances de production. Ces trois modifications transforment un Dockerfile fonctionnel en un Dockerfile déployable.
FROM oven/bun:1-alpine
WORKDIR /app
# Créer un utilisateur non-root (syntaxe Alpine)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
COPY src ./src
COPY tsconfig.json ./
RUN chown -R appuser:appgroup /app
USER appuser
ENV NODE_ENV=production
EXPOSE 3000
CMD ["bun", "run", "src/index.ts"]
Épinglez le tag. Les références d’images Docker utilisent par défaut latest lorsqu’aucun tag n’est spécifié, ce qui signifie qu’un docker pull peut modifier implicitement votre runtime Bun entre les déploiements. Épinglez un tag Bun explicite pour que les mises à jour du runtime restent délibérées et traçables — le guide Docker officiel de Bun utilise oven/bun:1. La variante 1-alpine existe sur Docker Hub pour une empreinte plus légère ; vérifiez qu’elle convient à vos dépendances avant de l’adopter, car Alpine utilise musl libc plutôt que glibc.
Exécutez en tant que non-root. L’utilisateur par défaut du conteneur est root. Créer appuser et basculer vers cet utilisateur avec USER appuser limite le rayon d’impact en cas de compromission du processus — une pratique de base des recommandations de sécurité Docker.
Installez uniquement les dépendances de production. bun install --frozen-lockfile --production ignore les devDependencies, ce qui réduit la taille de l’image et la surface d’attaque.
Pourquoi Mon Conteneur Bun Est-il Inaccessible sur Son Port Publié ?
Un service Bun qui écoute uniquement sur
127.0.0.1à l’intérieur d’un conteneur Docker ne sera pas accessible via un port publié. Liez-le à0.0.0.0(toutes les interfaces IPv4) pour que la publication de ports Docker puisse lui transférer le trafic.
C’est l’échec silencieux le plus courant dans un service Bun dockerisé. Le conteneur démarre, les logs semblent sains, docker ps affiche le port mappé — et chaque requête depuis l’hôte reste en attente ou retourne une erreur de connexion. La cause est l’adresse d’écoute.
Pour Bun.serve() brut, définissez hostname explicitement :
// src/index.ts
const port = Number(process.env.PORT) || 3000;
Bun.serve({
port,
hostname: "0.0.0.0", // requis à l'intérieur de Docker
fetch(req) {
return new Response(JSON.stringify({ status: "ok" }), {
headers: { "Content-Type": "application/json" },
});
},
});
console.log(`Listening on http://0.0.0.0:${port}`);
Les options de Bun.serve() sont documentées dans la documentation de l’API HTTP Bun. Pour Elysia, configurez serve.hostname: "0.0.0.0". Pour les autres frameworks fonctionnant sur Bun, assurez-vous que le serveur Bun sous-jacent est configuré pour écouter sur 0.0.0.0.
Les replays de session des frontends communiquant avec un service Bun peuvent révéler cet échec à un niveau inférieur, dans le Dockerfile. Un service lié à localhost se manifeste dans le navigateur par une cascade de timeouts fetch/XHR immédiatement après un déploiement — le type de symptôme qui ressemble à un bug frontend mais qui trouve son origine dans l’adresse d’écoute du conteneur. Des outils comme OpenReplay capturent ce pattern d’échec côté client, là où cette mauvaise configuration est généralement remarquée en premier.
Docker Compose avec Postgres
Un fichier Docker Compose exécute votre application Bun aux côtés de PostgreSQL, attend que la base de données passe un healthcheck avant de démarrer l’application, et redémarre l’application en cas d’échec.
# compose.yaml
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
env_file:
- .env.production
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata:
depends_on avec condition: service_healthy retient le service api jusqu’à ce que Postgres signale un état sain, ce qui nécessite un healthcheck sur la dépendance — sans lui, service_healthy ne se résout jamais. restart: unless-stopped redémarre l’application après un crash mais respecte un arrêt manuel.
Notez l’absence de clé version: au niveau racine. La Spécification Compose marque la propriété version de niveau supérieur comme obsolète ; Compose l’ignore pour la sélection du schéma et émet un avertissement si elle est présente. Omettez-la.
Variables d’Environnement : Ne Pas Intégrer les Secrets dans l’Image
Transmettez la configuration via env_file à l’exécution plutôt que de copier des secrets dans un layer d’image. Les layers d’image sont persistants et inspectables, donc un secret écrit dans l’un d’eux reste récupérable même si une instruction ultérieure le supprime.
Le service Compose ci-dessus référence déjà .env.production via env_file. Lisez ces valeurs dans le code depuis process.env :
// src/config.ts
export const config = {
port: Number(process.env.PORT) || 3000,
databaseUrl: process.env.DATABASE_URL ?? "",
jwtSecret: process.env.JWT_SECRET ?? "",
};
Gardez .env.production hors de l’image grâce à l’entrée .env.* dans .dockerignore, et hors du contrôle de version avec une entrée correspondante dans .gitignore. L’image reste générique ; les secrets sont injectés à l’exécution.
Workflow de Développement : Rechargement à Chaud avec un Bind Mount
Pour le développement local, utilisez un Dockerfile séparé qui exécute bun --hot et montez votre source en tant que volume pour que les sauvegardes rechargent instantanément à l’intérieur du conteneur. Gardez ceci distinct de votre Dockerfile de production.
# Dockerfile.dev
FROM oven/bun:1-alpine
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
EXPOSE 3000
CMD ["bun", "--hot", "src/index.ts"]
# compose.dev.yaml
services:
api-dev:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
env_file:
- .env.development
Le flag --hot active le rechargement à chaud de Bun : sauvegarder un fichier met à jour le serveur en cours d’exécution sans redémarrage complet du processus. Le bind mount .:/app rend les modifications de l’hôte visibles à l’intérieur du conteneur, et le volume anonyme /app/node_modules empêche le répertoire hôte de masquer les dépendances installées lors du build. Lancez-le avec docker compose -f compose.dev.yaml up --build.
Arrêt Gracieux : Gérer SIGTERM
Ajoutez un gestionnaire SIGTERM pour que les requêtes en cours se terminent avant la sortie du processus. Sans lui, Docker envoie SIGTERM, attend jusqu’à son délai d’arrêt par défaut (10 secondes pour les conteneurs Linux), puis envoie SIGKILL si le processus ne s’est pas terminé, risquant de perdre des requêtes encore en cours de traitement.
Bun.serve() retourne un objet serveur avec une méthode .stop(). Attendez-la sur SIGTERM pour laisser les requêtes en cours se terminer avant de quitter :
// src/index.ts
const server = Bun.serve({
port: Number(process.env.PORT) || 3000,
hostname: "0.0.0.0",
fetch() {
return new Response("ok");
},
});
const shutdown = async (signal: string) => {
console.log(`Received ${signal}, draining connections`);
await server.stop();
process.exit(0);
};
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
Le handle server et sa méthode .stop() font partie de l’API HTTP Bun. Le principe fondamental est de capturer l’instance serveur retournée par Bun.serve() et de l’arrêter à la réception d’un signal de terminaison.
Estimation de la Taille des Images
L’image de base que vous choisissez détermine l’essentiel de la taille finale de l’image. Docker Hub publie les tailles compressées pour chaque variante oven/bun ; les chiffres ci-dessous proviennent de la page des tags Docker Hub pour oven/bun:1.3.14 (linux/amd64).
| Image de base | Variante | Taille compressée |
|---|---|---|
oven/bun:1.3.14 | Basée sur Debian | 81,93 Mo |
oven/bun:1.3.14-slim | Debian slim | 63,32 Mo |
oven/bun:1.3.14-alpine | Alpine | 40,87 Mo |
oven/bun:1.3.14-distroless | Distroless | 40,52 Mo |
Ce sont des tailles d’images de base, pas celles de votre image finale. Votre image ajoute des dépendances et du code source par-dessus. Pour mesurer la vôtre, exécutez :
docker images --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"
La variante multi-stage avec binaire compilé sur debian:12-slim peut réduire davantage l’image finale car elle exclut l’image Bun complète et node_modules, mais le delta exact dépend de l’empreinte de vos dépendances — mesurez-le plutôt que de citer un pourcentage générique.
Quels Sont les Échecs les Plus Courants dans un Conteneur Bun Docker ?
Les quatre raisons les plus courantes pour lesquelles un conteneur Bun dockerisé échoue silencieusement sont : un chemin de point d’entrée incorrect, une liaison sur localhost, un lockfile obsolète, et une étape de copie de tsconfig.json manquante. Chacun a un symptôme distinct et un correctif en une ligne.
| Symptôme | Cause racine | Correctif |
|---|---|---|
| Le conteneur se termine immédiatement, code 1 | Chemin de point d’entrée incorrect dans CMD, ou fichier d’entrée non copié | Vérifiez que le chemin dans CMD ["bun", "run", "src/index.ts"] correspond au fichier copié ; vérifiez que COPY src ./src a été exécuté |
| Port mappé mais inaccessible | Service lié à 127.0.0.1/localhost | Liez à 0.0.0.0 dans Bun.serve({ hostname: "0.0.0.0" }) |
Le build s’interrompt pendant bun install | bun.lock désynchronisé avec package.json | Exécutez bun install en local, committez le bun.lock mis à jour ; --frozen-lockfile passe ensuite |
| La résolution des modules échoue au démarrage | Alias de chemins tsconfig.json non résolus — fichier non copié | Ajoutez COPY tsconfig.json ./ avant le CMD |
Lorsqu’un conteneur se termine avant que vous puissiez l’inspecter, ouvrez un shell sur l’image pour vérifier ce qui a réellement été copié :
docker run -it --rm --entrypoint sh bun-app
# à l'intérieur : ls /app, cat package.json, etc.
Le cas de désynchronisation du lockfile est celui le plus souvent interprété à tort comme un problème Docker. C’est un problème Bun révélé par Docker — bun install --frozen-lockfile fait son travail en refusant de builder contre un lockfile dérivé, exactement comme le décrit la documentation de bun install.
Conclusion
Vous disposez maintenant de tous les éléments : un Dockerfile de production épinglé, non-root ; une variante multi-stage avec binaire compilé ; une stack Compose avec un Postgres doté d’un healthcheck ; des outils de développement avec rechargement à chaud ; et un gestionnaire SIGTERM qui draine les requêtes proprement. Commencez par remplacer oven/bun par oven/bun:1 et en ajoutant la liaison 0.0.0.0 — ces deux modifications éliminent les deux échecs les plus susceptibles de vous affecter lors de votre premier déploiement, puis intégrez le reste au fur et à mesure que votre service évolue.
FAQ
Ai-je besoin d'une étape de build séparée pour exécuter TypeScript dans un conteneur Bun Docker ?
Non. Bun exécute TypeScript directement, il n'y a donc pas d'étape de compilation tsc ou tsx dans le build Docker. Votre CMD pointe directement vers le fichier d'entrée .ts, par exemple CMD ['bun', 'run', 'src/index.ts']. La seule mise en garde concerne les alias de chemins : si votre application utilise le mapping de chemins tsconfig.json, vous devez copier tsconfig.json dans l'image, sinon la résolution des modules échoue au démarrage alors qu'elle fonctionne en local.
Quand dois-je utiliser un binaire compilé avec bun build --compile plutôt que d'exécuter bun run dans le conteneur ?
Utilisez le build multi-stage avec binaire compilé lorsque vous souhaitez une image finale plus légère et aucune installation de Bun dans l'étape d'exécution. bun build --compile produit un exécutable autonome regroupant votre code, les packages npm, les assets et le runtime Bun, vous pouvez donc copier uniquement ce binaire sur une base minimale comme debian:12-slim sans node_modules ni image Bun. Utilisez bun run classique pour des builds plus simples ou lorsque vous dépendez de fonctionnalités d'exécution comme le rechargement à chaud pendant le développement.
La variante alpine de l'image oven/bun fonctionne-t-elle avec toutes les dépendances ?
Pas toujours. La variante oven/bun:1-alpine utilise musl libc au lieu de glibc, tandis que l'image Debian par défaut oven/bun:1 utilise glibc. Les dépendances natives compilées contre glibc, ou les packages distribuant des binaires précompilés uniquement pour glibc, peuvent échouer sur Alpine. L'image Alpine est plus légère, mais vérifiez que votre arbre de dépendances se compile et s'exécute correctement avant de l'adopter en production plutôt que de supposer un remplacement direct.
Pourquoi mon build de conteneur échoue-t-il pendant bun install avec une erreur frozen-lockfile ?
Le build s'interrompt car bun.lock n'est pas synchronisé avec package.json et bun install --frozen-lockfile refuse de builder contre un lockfile dérivé. C'est le comportement correct dans un Dockerfile : il révèle les dérives du lockfile au moment du build plutôt qu'à l'exécution. Corrigez-le en exécutant bun install en local pour régénérer bun.lock, puis committez le fichier mis à jour. Si votre Dockerfile référence encore l'ancien bun.lockb binaire, remplacez-le par bun.lock, car Bun 1.2 a fait du format textuel le format par défaut.