Ajouter des services à côté d'un bot existant
Privilégiez un projet Compose par application exploitée de manière indépendante :
/srv/discord-bot/{compose.yaml,.env,data/}
/srv/rsshub/{compose.yaml,.env}
/srv/example-api/{compose.yaml,data/}
/srv/caddy/{compose.yaml,Caddyfile}
Compose dérive normalement le nom du projet à partir du répertoire, isolant ainsi les noms de ressources et les réseaux par défaut. Un champ name: de premier niveau peut le rendre explicite. Des projets séparés permettent de mettre à jour un Bot sans recréer RSSHub ; ne combinez des composants que lorsqu'ils partagent véritablement le déploiement et le retour arrière.
Un bot Discord Gateway initie généralement une connexion WebSocket sortante et n'a besoin d'aucun port entrant publié :
name: discord-bot
services:
bot:
image: ghcr.io/example/discord-bot:<pinned-version>
restart: unless-stopped
env_file: [.env]
volumes: [./data:/app/data]
Les bots recevant des interactions HTTP ou des webhooks nécessitent une route entrante. Pour les applications web publiques, privilégiez Caddy comme unique point d'entrée 80/443 plutôt que de publier des ports applicatifs arbitraires. Les services réservés à l'administration peuvent être liés au bouclage local (loopback) ou à Tailscale. Les bases de données et les caches doivent rester sur les réseaux internes des projets.
Pour le proxying entre projets, créez un réseau externe explicite tel que docker network create ingress, déclarez-le external: true, et n'y associez que les services faisant face au proxy.
Un modèle d'entrée partagée validé
Les phases 2 et 3 sur ce VPS ont mis le modèle général en pratique :
Internet -> host TCP 80/443 -> Caddy -> infra-edge -> RSSHub:1200
WSL2 -> Tailscale SSH -> VPS 127.0.0.1:3001 -> Uptime Kuma
Caddy est le seul service Docker publié sur les adresses publiques de l'hôte. Le pont externe réutilisable s'appelle infra-edge ; Caddy et RSSHub en font partie, et Caddy atteint RSSHub via l'alias rsshub-prod. RSSHub ne publie aucun port hôte. Uptime Kuma reste délibérément en dehors du chemin d'entrée public et publie exactement 127.0.0.1:3001:3001.
Ouvrez l'interface de supervision avec un tunnel de redirection locale au lieu d'ajouter une route publique :
ssh -i "$LOCAL_SSH_PRIVATE_KEY" \
-o IdentitiesOnly=yes \
-L 3001:127.0.0.1:3001 \
"$VPS_USER@$TAILSCALE_IPV4"
Naviguez ensuite vers http://127.0.0.1:3001. Le tunnel protège la route de gestion sans publier d'identifiants, de cookies ou l'identité de l'administrateur.
UFW ne constitue pas l'audit d'exposition de Docker
Un jeu de règles UFW actif est nécessaire, mais ne prouve pas qu'un port publié par Docker soit privé. La documentation officielle de Docker sur le pare-feu explique que le trafic des conteneurs publiés est détourné avant les chaînes normalement utilisées par UFW. La liaison dans Compose fait donc partie de la frontière de sécurité : incluez explicitement 127.0.0.1 pour un service réservé au bouclage local, et vérifiez le résultat depuis Docker, l'hôte et une autre machine.
Vérifiez la version de Docker Engine avant de réutiliser ce modèle d'isolation. Docker indique que, dans les versions antérieures à 28.0.0, les hôtes du même segment réseau de couche 2 pouvaient accéder aux ports publiés sur localhost. Avec ces versions, une liaison à 127.0.0.1 ne suffit pas à elle seule à prouver l'isolation.
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}'
sudo ss -lntup
sudo ufw status verbose
sudo ufw status numbered
docker network inspect infra-edge
KUMA_ID=$(docker compose -f /srv/uptime-kuma/compose.yaml ps -q uptime-kuma)
docker inspect "$KUMA_ID" --format '{{json .HostConfig.PortBindings}}'
RSSHUB_ID=$(docker compose -f /srv/rsshub/compose.yaml ps -q rsshub)
docker inspect "$RSSHUB_ID" --format '{{json .HostConfig.PortBindings}}'
La valeur d'inspection acceptée pour Kuma est :
{"3001/tcp":[{"HostIp":"127.0.0.1","HostPort":"3001"}]}
La table de liaison acceptée pour RSSHub est {}. Un simple 1200/tcp dans la liste des ports de l'image est interne, pas une publication sur l'hôte.
La mention 443/udp ou 2019/tcp dans la liste des ports affichés d'une image Caddy ne signifie pas en soi que l'hôte publie ces ports. Considérez-le comme une exposition uniquement lorsque le mappage Compose, l'inspection Docker ou les sockets de l'hôte indiquent une liaison d'hôte.
Depuis WSL2, vérifiez à la fois les chemins autorisés et refusés. Une réponse sur l'IP publique sur le port 3001 ou une connexion SSH réussie sur l'IP publique constitue un échec de validation.
curl --fail https://infra.example.com/healthz
curl --connect-timeout 5 --max-time 7 http://"$VPS_PUBLIC_IPV4":3001/
curl --connect-timeout 5 --max-time 7 http://"$VPS_PUBLIC_IPV4":1200/
curl --connect-timeout 5 --max-time 7 http://"$TAILSCALE_IPV4":1200/
ssh -o ConnectionAttempts=1 -o ConnectTimeout=5 \
"$VPS_USER@$VPS_PUBLIC_IPV4"
La phase 2 a utilisé un redémarrage complet délibéré pour prouver le socle de l'hôte. Une application nouvellement ajoutée doit d'abord faire l'objet d'un redémarrage contrôlé plus restreint : redémarrez uniquement ce service Compose, attendez l'état sain, puis répétez les vérifications des points de terminaison, du contrôle d'accès, des liaisons Docker et des dépendances. Ne redémarrez l'ensemble de l'hôte que lorsqu'une tâche ultérieure nécessite spécifiquement une preuve de récupération au niveau de l'hôte.
Traitez DISCORD_TOKEN comme un secret : conservez-le dans un fichier .env ignoré avec les permissions en mode 600 ou dans un gestionnaire de secrets, ne committez ni ne collez jamais la sortie développée de Compose, et renouvelez-le sur le portail développeur en cas de suspicion d'exposition.
Procédure d'ajout répétable
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker network ls; docker volume ls
ss -lntup; free -h; df -h /
mkdir -p /srv/new-service/data
cd /srv/new-service
docker compose config --quiet
docker compose config --services
docker compose config --images
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail 100
docker compose config peut développer les secrets, ne partagez donc pas sa sortie complète. Épinglez des versions ou des digests plutôt que de vous fier aveuglément à latest. Un conteneur en cours d'exécution ne constitue pas un contrôle de santé : vérifiez que le Bot se connecte et répond, ou que le service web fonctionne via Caddy et écrit dans le volume attendu.
Enregistrez l'ancienne image et le diff Compose avant les mises à jour. Pour mettre à jour un seul service :
cd /srv/discord-bot
docker compose pull bot
docker compose up -d --no-deps bot
docker compose ps
docker compose logs --tail 100 bot
Vérifiez les exigences de dépendances et de migrations de base de données avant d'utiliser --no-deps. Évitez d'exécuter docker compose down depuis un répertoire ambigu et n'utilisez jamais docker system prune -a comme procédure de dépannage de routine. Des frontières de responsabilité claires importent davantage que d'économiser quelques fichiers YAML.
Le déploiement miroir de RSSHub est l'exemple concret de ce flux de travail : image officielle épinglée par digest, un fichier d'environnement protégé, cache en mémoire, infra-edge, aucun port hôte, canari authentifié et retour arrière borné.