shared-server-docker-deploy

Déployer une app Node (avec des dépendances natives ou Chromium) sur un serveur Docker qui héberge déjà d'autres applications, sans rien casser. Couvre : libération d'espace disque avant la construction, limites de mémoire par service, healthcheck uniquement sur les services HTTP, version des modules natifs selon Node, publication derrière un Caddy partagé avec authentification de base, et validation/rechargement du proxy sans interruption de service.

Qu'est-ce que shared-server-docker-deploy ?

Cette compétence est conçue pour les déploiements sur un serveur Docker existant qui héberge déjà d'autres applications, en veillant à ne pas interrompre les services en cours. Elle est particulièrement utile pour les applications Node.js qui ont des dépendances natives (comme `better-sqlite3`, `sharp`) ou un navigateur (Chromium), et qui doivent être déployées sur un serveur partagé avec d'autres projets. La compétence couvre l'ensemble du processus : libération de l'espace disque avant la construction pour éviter les échecs, définition de limites de mémoire par service (mem_limit) pour prévenir les OOM, configuration d'un healthcheck uniquement pour les services HTTP, et vérification de la compatibilité des versions des modules natifs avec la version de Node.js. Elle gère également la publication derrière un proxy inverse Caddy partagé avec authentification de base (basic auth) et une procédure de rechargement sans interruption de service. De plus, elle fournit une méthode de déploiement multiplateforme (à partir de Windows) utilisant tar et SSH. Les plateformes cibles incluent les serveurs Docker avec un proxy existant (comme Caddy). En résumé, c'est une boîte à outils pour des déploiements sécurisés, contrôlés et non perturbateurs dans un environnement multi-utilisateurs.

Compatible avec~Claude Code~Codex CLI~Cursor
Part ofMobile
npx skills add https://github.com/leoarayas/agent-skills/tree/main/shared-server-docker-deploy

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Déploiement sur un serveur Docker partagé

Quand l'utiliser

  • Le serveur n'est pas dédié : il exécute déjà les déploiements d'autres personnes ou d'autres projets.
  • L'application a une dépendance native (better-sqlite3, sharp, canvas) ou un navigateur (Chromium).
  • Un proxy inverse (Caddy ou nginx) existe déjà et publie plusieurs domaines.
  • Toute erreur EADDRINUSE, OOM ou disque plein affecterait les autres stacks.

Règle d'or : la nouvelle application ne peut pas être la cause de l'arrêt d'une autre. C'est pourquoi tout est délimité et mesuré au préalable.


0. Avant de construire : mesurer et libérer le disque

Une construction avec Chromium télécharge environ 2 Go d'image. Si le disque est à 78%, la construction échoue ou laisse le serveur à la limite, et le prochain prune supprime les images de production.

ssh SERVEUR 'docker system df; df -h /'

Ce qui traîne généralement et peut être libéré en toute sécurité :

docker builder prune -af      # cache de build (souvent le plus volumineux)
docker image prune -af        # images inutilisées
docker container prune -f     # conteneurs arrêtés

Avant et après, compter les conteneurs pour confirmer que rien n'est tombé :

docker ps -q | wc -l   # sauvegarder le nombre, comparer après

Mesuré dans un cas réel : 78% → 32% récupérés, avec les 41 conteneurs de production intacts.


1. Modules natifs et version de Node

Un fichier .node compilé pour une version majeure différente de Node ne produit pas une erreur claire : il interrompt le processus avec une assertion interne et une trace de pile native inutile.

Symptôme typique :

# node.exe[61084]: void __cdecl node::RemoveEnvironmentCleanupHook(Isolate *, CleanupHook, void *)
# Assertion failed: (env) != nullptr
----- Native stack trace -----

Cause : better-sqlite3@11 avec Node 24. Le support arrive dans better-sqlite3@12.

Règles :

  • Vérifier la version du module natif par rapport à la version majeure de Node avant de l'intégrer.
  • Dans le Dockerfile de construction, installer la chaîne d'outils (python3 make g++) pour qu'un binaire précompilé qui n'existe pas puisse être compilé à la volée.
  • Au moment de l'exécution, ne pas supposer que la chaîne d'outils est nécessaire : préférer une image finale plus petite.
  • Si le module est importé depuis plusieurs paquets de l'espace de travail, le tester dans un processus qui ne fait rien d'autre : un crash dû à un ramasse-miettes différé peut apparaître des heures plus tard.

2. Séparer le processus lourd de celui qui répond

Chromium et un module natif dans le même processus que le serveur HTTP provoquent un plantage et, en plus, bloquent la boucle d'événements pendant les tâches longues.

api     : HTTP + SSE, mémoire faible, healthcheck HTTP
worker  : file d'attente + navigateur, mémoire élevée, sans ports, sans healthcheck HTTP

Ils communiquent via un stockage partagé (SQLite, Redis, Postgres). L'API n'a pas besoin de savoir si le worker est actif.

Le healthcheck est défini uniquement sur celui qui sert HTTP. L'hériter depuis le Dockerfile dans le worker le laisse unhealthy pour toujours, car il n'ouvre jamais de port :

worker:
  healthcheck:
    disable: true

3. Délimiter la mémoire par service

api:
  mem_limit: 400m
worker:
  mem_limit: 1600m
  mem_reservation: 400m
  volumes:
    - /dev/shm:/dev/shm:rw,size=256m   # Chromium utilise /dev/shm ; 64 Mo le casse
  tmpfs:
    - /tmp:size=256m
  security_opt:
    - no-new-privileges:true

De plus, dans le code, réduire la charge : le scraper devrait avoir un DETAIL_MAX / MAX_PAGES plus bas sur le serveur qu'en local. Si un jour la RAM vient à manquer sur le serveur, le coupable est presque toujours ce nombre, pas Docker.

Mesurer avant et après une recherche réelle :

docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}' <api> <worker>
free -m | head -2

4. Construction : étages et caches qui fonctionnent

FROM mcr.microsoft.com/playwright:v1.63.0-noble AS build   # version fixée au paquet npm
...
FROM mcr.microsoft.com/playwright:v1.63.0-noble AS runtime
  • Ficher la version de l'image à celle du paquet npm installé. Si elles ne correspondent pas, le navigateur n'est pas là où Playwright le cherche.
  • Copier d'abord les package.json, puis le code, afin que COPY . . n'invalide pas la couche install.
  • Au moment de l'exécution, pnpm/npm sont nécessaires si les scripts de démarrage les utilisent → corepack enable.
  • Si node_modules est copié depuis la construction et que les permissions ne correspondent pas, pnpm essaie de le "réparer" à chaque démarrage et échoue. On le désactive avec verify-deps-before-run=false dans un .npmrc de l'image.
  • Utilisateur non root au moment de l'exécution. Les images Playwright fournissent déjà pwuser.

pnpm-lock.yaml doit être commité et l'installation de la construction doit se faire avec --frozen-lockfile.


5. Publier via un proxy qui existe déjà

Si un Caddy existe déjà et publie plusieurs domaines, ne pas lancer un autre proxy ni ouvrir un autre port public : on ajoute un bloc au Caddyfile existant et on se connecte au réseau Docker.

services:
  api:
    networks: [edge_proxy]        # réseau externe, déjà créé
networks:
  edge_proxy:
    external: true
    name: edge_proxy

Découvrir le réseau et où se trouve le Caddyfile :

docker inspect <proxy> --format '{{range .Mounts}}{{.Source}} -> {{.Destination}} rw={{.RW}}{{println}}{{end}}'

Modifier le fichier sur l'hôte : si le montage lié est rw=false, écrire à l'intérieur du conteneur échoue avec Resource busy ou est perdu au redémarrage.

Procédure sécurisée (ne jamais modifier à l'aveugle) :

cp "$CADDYFILE" "$CADDYFILE.bak-$(date +%Y%m%d-%H%M%S)"   # 1. sauvegarde
# 2. ajouter le bloc
docker exec <proxy> caddy validate --config /etc/caddy/Caddyfile  # 3. valider
docker exec <proxy> caddy reload  --config /etc/caddy/Caddyfile   # 4. recharger (sans interruption)

Si validate échoue, restaurer la sauvegarde. Le rechargement ne coupe pas les connexions existantes.

Authentification de base (basic auth)

prospector.exemple.dev {
	basic_auth {
		leo $2a$14$...        # bcrypt, JAMAIS en clair
	}
	reverse_proxy api:3000 {
		flush_interval -1     # indispensable pour SSE
	}
	encode zstd gzip
	header {
		X-Frame-Options DENY
		-Server
	}
}
  • Générer le hash sur le serveur : docker exec <proxy> caddy hash-password --plaintext '...'.
  • Sauvegarder le mot de passe en clair uniquement dans un fichier root (/root/<app>-password.txt, 600) pour pouvoir le récupérer ou le faire tourner, et jamais dans le dépôt.
  • Ne pas utiliser le proxy Cloudflare devant si l'application utilise SSE : le délai d'expiration de 100 s et la mise en mémoire tampon coupent la progression. DNS-only (nuage gris) permet à l'origine d'émettre le vrai certificat.
  • basic_auth protège uniquement ce qui passe par le proxy. Si l'application est également exposée sur un autre port, elle reste sans protection : le documenter.

6. Déployer depuis Windows

rsync n'existe pas sur Windows. Ce qui fonctionne et est multiplateforme, c'est tar via un tube SSH :

const tar = spawn('tar', ['-cf', '-', '--exclude', './node_modules', ...excludes, '.'], { cwd: ROOT });
const ssh = spawn('ssh', [HOST, `mkdir -p ${DIR} && tar -xf - -C ${DIR}`], { stdio: ['pipe', 'inherit', 'inherit'] });
tar.stdout.pipe(ssh.stdin);

spawn avec shell: true est obligatoire pour résoudre pnpm sur Windows (c'est un .cmd), mais alors on ne peut pas lui passer de descripteurs de fichier pour rediriger la sortie : utiliser la variable d'environnement LOG_FILE et laisser l'application elle-même écrire le journal.


7. Avant de toucher au serveur partagé

  • docker ps -q | wc -l sauvegardé, pour comparer à la fin
  • Espace disque libre suffisant après la libération
  • mem_limit sur tous les nouveaux services
  • Le processus lourd dans son propre conteneur, sans ports
  • Healthcheck uniquement sur celui qui sert HTTP
  • Réseau externe du proxy déclaré, pas un nouveau réseau
  • Sauvegarde du Caddyfile et caddy validate avant le rechargement
  • Journal dans un fichier (le processus reste sans console)
  • Script de déploiement idempotent : check / push / build / up / logs / stats / backup
  • Volume avec sauvegarde testée et restaurée, pas seulement créée

Diagnostic rapide

docker stats --no-stream --format '{{.Name}} {{.MemUsage}} {{.CPUPerc}}' <api> <worker>
docker logs --tail 80 <worker>
docker exec <api> node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>r.text()).then(console.log)"
free -m && df -h /

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

Skills associés