Ajouter des services à côté d'un bot existant
Supposons qu’un VPS héberge déjà un bot Discord et que vous souhaitiez ajouter RSSHub, une API personnelle ou un site web. Il faut garder des limites claires pour la configuration, le cycle de vie, le réseau et les données de chaque service. Pouvoir lancer un conteneur de plus avec Docker ne suffit pas à régler ces questions.
Privilégier un projet Compose par application
Par exemple, chaque application peut avoir son propre répertoire :
/srv/discord-bot/:compose.yaml, un fichier.envexclu de Git etdata/./srv/rsshub/:compose.yamlet.env./srv/example-api/:compose.yamletdata/./srv/caddy/:compose.yamletCaddyfile.
Compose tire normalement le nom du projet du répertoire, ce qui regroupe ses conteneurs, son réseau par défaut et les noms de ses ressources, tout en isolant les ressources et les réseaux par défaut des différents projets. Vous pouvez aussi définir name: discord-bot au premier niveau du fichier Compose. Avec des projets séparés, mettre à jour le bot ne recrée pas RSSHub, et le périmètre des journaux et du retour arrière reste clair.
Un grand fichier compose.yaml commun peut convenir, mais il rattache des services indépendants au même cycle de vie. Pour un serveur personnel sur une seule machine, des projets séparés sont généralement un point de départ plus facile à comprendre.
Déterminer d’abord si le service a besoin d’un port entrant
Un bot utilisant Discord Gateway établit généralement une connexion WebSocket sortante persistante vers Discord. S’il utilise uniquement la Gateway, il n’a normalement besoin d’aucun port ouvert sur l’adresse publique du VPS :
name: discord-bot
services:
bot:
image: ghcr.io/example/discord-bot:<pinned-version>
restart: unless-stopped
env_file:
- .env
volumes:
- ./data:/app/data
Un bot qui reçoit des interactions HTTP ou des webhooks a besoin d’une route entrante ; une interface d’administration peut aussi nécessiter un accès HTTP entrant. Dans ce cas, privilégiez Caddy comme unique point d’entrée public plutôt que de publier des ports arbitraires pour chaque conteneur.
Trois types d’accès réseau
Pour que Caddy accède à une application d’un autre projet Compose, créez un réseau Docker externe nommé explicitement :
docker network create ingress
Chaque projet déclare ce réseau :
networks:
ingress:
external: true
Rattachez à ingress uniquement les services auxquels le proxy doit accéder. Les bases de données et les caches doivent rester sur les réseaux internes de leurs projets, sans publication de ports de base de données sur Internet.
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 -> hôte 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 par un tunnel de redirection locale, sans ajouter de route publique :
ssh -i "$HOME/.ssh/id_ed25519" \
-o IdentitiesOnly=yes \
-L 3001:127.0.0.1:3001 \
"$VPS_USER@$TAILSCALE_IPV4"
Consultez ensuite 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 des liaisons acceptée pour RSSHub est {}. Si docker ps affiche seulement 1200/tcp, il s’agit d’un port interne au conteneur, pas d’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. Il y a exposition sur l’hôte uniquement lorsque le mappage Compose, docker inspect ou les sockets de l’hôte indiquent une liaison HostIP/HostPort.
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.
Secrets et configuration
Le token d’un bot Discord est un secret sensible :
- Le conserver dans un fichier
.envaux permissions restreintes ou dans un gestionnaire de secrets adapté. - Ajouter
.envà.gitignoreet ne jamais le committer. - Ne pas exposer les valeurs développées dans la sortie de
docker compose config, les captures d’écran ou les journaux partagés. - En cas de suspicion de fuite, renouveler le token dans le Discord Developer Portal ; le supprimer d’un fichier ne suffit pas.
Par exemple :
cd /srv/discord-bot
touch .env
chmod 600 .env
Dans les notes, ne consignez que les noms de variables, comme DISCORD_TOKEN, jamais leurs valeurs réelles.
Procédure d'ajout répétable
1. Inventorier les ressources et les conflits
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker network ls
docker volume ls
ss -lntup
free -h
df -h /
Vérifiez que les ports nécessaires sont libres, que la mémoire et le disque ont de la marge, et que l’emplacement des données du nouveau service est connu.
2. Créer un répertoire distinct
# Créer d’abord /srv/new-service selon la procédure avec contrôles du guide d’exploitation.
# Y créer et vérifier compose.yaml ainsi que les fichiers de secrets nécessaires.
cd /srv/new-service || exit 1
test -f compose.yaml || exit 1
mkdir -p data
3. Vérifier la configuration Compose résolue
docker compose -f ./compose.yaml config --quiet
docker compose config --services
docker compose config --images
docker compose config peut développer les variables d’environnement. Ne copiez pas sa sortie complète dans un espace public.
4. Télécharger les images et démarrer
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail 100
En production, privilégiez une version précise ou un digest plutôt que de vous fier aveuglément à latest. Une mise à jour d’image peut introduire des changements incompatibles.
5. Vérifier la santé du service lui-même
L’état running indique seulement que le processus principal du conteneur ne s’est pas arrêté. Vérifiez aussi le comportement de l’application : le bot doit se connecter à la Gateway et répondre à une commande de test, un service HTTP doit renvoyer le contenu attendu via Caddy, et les données doivent être écrites dans le volume prévu.
6. Consigner un point de retour arrière
Avant une mise à jour, notez la version actuelle de l’image et le diff Compose. Le retour arrière consiste généralement à restaurer l’ancienne version, relancer docker compose up -d et rétablir des données compatibles. Si une migration a modifié le format de la base de données, revenir à l’ancienne image peut ne pas suffire.
Exploiter chaque projet séparément
cd /srv/discord-bot
docker compose ps
docker compose logs -f --tail 100
docker compose pull
docker compose up -d
cd /srv/rsshub
docker compose ps
Depuis n’importe quel répertoire, vous pouvez aussi utiliser :
docker compose -f /srv/discord-bot/compose.yaml ps
Depuis le répertoire /srv ou tout autre répertoire où le projet visé est incertain, ne lancez pas sans vérification docker compose down. N’utilisez pas docker system prune -a pour résoudre un problème courant de disque ou de déploiement. La première commande risque de viser le mauvais projet ; la seconde effectue un nettoyage étendu.
Mettre à jour un service sans toucher aux autres
cd /srv/discord-bot
docker compose pull bot
docker compose up -d --no-deps bot
docker compose ps
docker compose logs --tail 100 bot
Seul le service nommé est recréé. Vérifiez néanmoins les dépendances, les migrations de base de données et les notes de version une à une : --no-deps ne garantit pas à lui seul une mise à jour sans risque.
Quand envisager de regrouper des projets
Regroupez deux composants dans un projet Compose uniquement s’ils partagent le même cycle de publication, doivent démarrer et revenir à la version précédente ensemble, et relèvent de la même configuration. Une application et son worker dédié peuvent partager un projet ; un bot Discord et un RSSHub indépendant n’ont généralement pas besoin d’être regroupés.
À mesure que le serveur grandit, des limites claires épargnent plus de travail que quelques fichiers YAML en moins. Le nombre de conteneurs n’est pas le problème ; ne plus savoir quelle commande redémarre quel service l’est.
Le déploiement miroir de RSSHub en est un exemple concret sur ce serveur : image officielle fixée par digest, un fichier d’environnement protégé, cache en mémoire, infra-edge, aucun port hôte, canari authentifié et procédure de retour arrière au périmètre défini.