Skip to content

Troubleshooting VPS / déploiement ​

Recueil des pièges rencontrés en bootstrappant le VPS — provisioning, durcissement sécurité, et déploiement Tailscale. Chaque entrée donne le symptôme, la cause réelle, le fix, et pourquoi le fix marche — pour que la prochaine fois on évite l'aller-retour.

La plupart de ces problèmes apparaissent quand on exécute du shell non-interactif (Claude/CI/script) sur une stack pensée pour un humain devant un TTY. La règle générale est : tout ce qui demande un prompt visuel doit être désactivé explicitement.


1. Docker : permission denied sur /var/run/docker.sock après usermod -aG docker ​

Symptôme ​

$ docker ps
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock

…alors que getent group docker confirme que ton user est bien dans le groupe :

$ getent group docker
docker:x:988:kael

…mais id ne le voit pas dans la session courante :

$ id
uid=1000(kael) gid=1000(kael) groups=1000(kael),27(sudo),100(users)

Cause ​

Les groupes Unix d'un processus sont fixés au moment de son fork()/exec() et ne sont jamais rechargés ensuite. usermod -aG docker kael modifie /etc/group mais n'a aucun effet sur les processus déjà lancés.

Quand on bootstrappe le VPS, la chaîne typique est :

SSH login (groups: kael, sudo, users — pas docker, parce que docker n'existait pas encore)
 └─ tmux server (hérite des groupes du SSH)
     └─ bash dans un pane (hérite des groupes de tmux)
         └─ on lance ici sudo apt install docker-ce && sudo usermod -aG docker kael
         └─ on lance ensuite Claude (hérite des groupes du bash → toujours pas docker)
             └─ Claude fork() un sh -c "docker ps" → permission denied

Important : tmux attach depuis un nouveau client SSH ne respawn aucun processus. Ça ne fait que reconnecter ton terminal à un serveur tmux qui existe déjà. Les bash et processus enfants gardent leurs groupes d'origine. Donc se déconnecter/reconnecter SSH puis tmux attach ne corrige pas le problème.

Fix — 3 options ​

(a) Recharger pour de vrai (propre, mais relance les processus dans tmux)

Pour qu'un processus voie le nouveau groupe, il faut qu'il soit lancé depuis un parent qui l'a déjà. Concrètement :

bash
# 1. Quitter Claude/le shell tmux actuel
exit

# 2. Tuer le serveur tmux entièrement (sinon les nouveaux pane héritent encore de l'ancien bash)
tmux kill-server

# 3. Relancer ssh pour repartir d'une session de login fraîche
ssh kael@<vps>

# 4. Vérifier que les groupes sont là
id | grep docker

# 5. Relancer tmux et Claude depuis ce nouveau bash
tmux new -s claude
claude

C'est l'option à privilégier en fin de bootstrap, avant de commencer un long déploiement.

(b) sg docker -c "..." — workaround pour la session courante

sg (substitute group) lance une commande dans un sous-shell qui hérite d'un groupe additionnel temporairement, sans toucher aux processus parents.

bash
sg docker -c "docker ps"
sg docker -c "docker compose -f docker/compose.yml up -d --build"

Le wrapping s'applique à chaque commande individuellement. Utile quand on veut continuer une session Claude/tmux en cours sans la redémarrer.

(c) sudo docker ... — déconseillé

Ça marche toujours, mais le démon docker tourne déjà privilégié et ajouter un sudo par-dessus complique la lecture des logs et casse certains scripts qui supposent l'exécution non-root. À éviter sauf urgence.

Vérification après fix ​

bash
id | tr ',' '\n' | grep docker   # doit lister 'docker' dans les groupes
docker run --rm hello-world      # doit afficher "Hello from Docker!" sans sudo

2. apt upgrade qui hang sur un prompt whiptail ​

Symptôme ​

sudo apt upgrade -y reste figé indéfiniment, sans erreur. Aucun output après les premières lignes "Reading package lists…".

Cause ​

Certains paquets (mises à jour kernel, libc, openssh-server) déclenchent un prompt whiptail ("Pending kernel upgrade", "Configuration file modified", "Restart services?"). Le prompt s'affiche sur un TTY interactif, mais Claude/cron/CI tournent sans TTY → le prompt attend une touche qui ne vient jamais.

Fix ​

Forcer le mode non-interactif et dire à dpkg/needrestart de garder les configs existantes par défaut :

bash
sudo DEBIAN_FRONTEND=noninteractive \
     NEEDRESTART_MODE=a \
     apt upgrade -y \
       -o Dpkg::Options::="--force-confdef" \
       -o Dpkg::Options::="--force-confold"
  • DEBIAN_FRONTEND=noninteractive : supprime les whiptail
  • NEEDRESTART_MODE=a : needrestart redémarre automatiquement les services impactés au lieu de demander
  • --force-confdef + --force-confold : si un fichier de conf en place a été modifié localement, on garde la version locale

Pour un upgrade complet incluant un kernel pending :

bash
sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y
sudo reboot   # uniquement si un nouveau kernel a été installé

3. gpg --dearmor : invalid radix64 character quand on pipe curl ​

Symptôme ​

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
gpg: invalid radix64 character XX skipped
gpg: invalid radix64 character XX skipped
...
gpg: no valid OpenPGP data found.

Cause ​

Le pipe curl ... | sudo gpg ... peut corrompre l'encoding du flux (notamment si sudo réécrit la stdin, ou si une redirection ajoute des caractères de contrôle). Dans certains cas le shell ou un proxy (RTK, etc.) altère le flux entre les deux commandes.

Fix ​

Sauver la clé dans un fichier d'abord, puis dearmor à partir du fichier :

bash
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /tmp/docker.gpg
file /tmp/docker.gpg   # doit dire "PGP public key block" — sanity check
sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg /tmp/docker.gpg
rm /tmp/docker.gpg

C'est aussi plus debuggable : si le curl rate (mauvaise URL, proxy, certif expiré), on le voit avant d'invoquer gpg.


4. gpg --dearmor : cannot open '/dev/tty' ​

Symptôme ​

$ sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg /tmp/docker.gpg
gpg: cannot open '/dev/tty': No such device or address

Cause ​

Par défaut, gpg attend de pouvoir afficher des messages d'état sur le TTY. Sans TTY (Claude, CI, cron) il échoue tôt, avant même de traiter le fichier.

Fix ​

Toujours invoquer gpg avec --batch --yes pour le mode non-interactif :

bash
sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg /tmp/docker.gpg
  • --batch : pas de prompt, pas d'écriture sur TTY
  • --yes : assume "oui" pour les questions de type "écraser le fichier ?"

À appliquer à toute invocation gpg dans un script non-interactif (dearmor, import, sign, etc.).


5. RTK proxy : sudo: rtk: command not found ​

Symptôme ​

Certaines commandes sudo échouent avec :

$ sudo systemctl status fail2ban
sudo: rtk: command not found

…alors que which rtk montre que rtk est bien installé pour ton user.

Cause ​

RTK (Rust Token Killer) est un hook Claude Code qui réécrit les commandes en rtk <cmd> pour économiser des tokens. Le hook n'est pas conscient du contexte sudo :

  • L'utilisateur tape : sudo systemctl status fail2ban
  • Le hook réécrit en : sudo rtk systemctl status fail2ban
  • sudo cherche rtk dans le secure_path de root (/usr/sbin:/usr/bin:/sbin:/bin par défaut)
  • rtk est installé dans ~/.cargo/bin/rtk ou /usr/local/bin/rtk, pas dans secure_path
  • sudo rapporte "command not found"

Fix ​

Bypasser RTK pour les commandes sudo en utilisant des chemins absolus :

bash
/usr/bin/sudo /usr/bin/systemctl status fail2ban
/usr/bin/sudo /usr/bin/docker run --rm hello-world
/usr/bin/sudo /usr/sbin/fail2ban-client status

Le hook RTK ne réécrit pas les commandes appelées par chemin absolu. Plus chiant à taper mais 100 % fiable.

Alternative permanente : ajouter le chemin de rtk au secure_path de sudo (sudo visudo, ligne Defaults secure_path=...). À ne faire que si tu es à l'aise avec les implications sécurité (tout binaire dans ce path peut être appelé en root).


6. gh auth login : Claude qui refuse de fournir le device code ​

Symptôme ​

Tu demandes à Claude de te fournir le code de validation pour gh auth login. Claude répond quelque chose du genre "le code est généré côté client par ton navigateur, je ne peux pas te le donner".

Cause ​

Claude confond avec un flow OAuth purement côté navigateur. En réalité, gh auth login --web lance un device flow :

  1. gh (le CLI, qui tourne sur le VPS) appelle l'API GitHub et reçoit en réponse un device code (XXXX-YYYY) et une URL d'activation
  2. gh affiche ces deux infos sur stdout dans le terminal du VPS
  3. Claude est dans le terminal du VPS — donc Claude voit le code dans la sortie de la commande
  4. L'humain ouvre l'URL côté laptop, tape le code, GitHub valide
  5. Côté VPS, gh poll l'API et finit par recevoir un token

Le code est dans la stdout de la commande sur le VPS, donc Claude doit la lancer et lire le résultat — pas s'imaginer qu'il est généré ailleurs.

Fix (procédure pour Claude) ​

Lancer gh auth login en background, lire la sortie, extraire le code :

bash
# Background, stdout dans un fichier
gh auth login --hostname github.com --git-protocol https --web > /tmp/gh-auth.log 2>&1 &
GH_PID=$!

# Attendre que le code apparaisse (max 30s)
for i in {1..30}; do
  if grep -q "First copy your one-time code" /tmp/gh-auth.log 2>/dev/null; then
    break
  fi
  sleep 1
done

# Extraire et afficher le code
grep "one-time code" /tmp/gh-auth.log
# → "! First copy your one-time code: XXXX-YYYY"

Présente le code à l'humain (qui ouvre https://github.com/login/device dans son navigateur, tape le code). Une fois validé, le processus background se termine et gh écrit son token. Vérifie ensuite avec :

bash
wait $GH_PID
gh auth status

Fallback si le device flow ne marche pas ​

Personal Access Token classique :

bash
gh auth login --with-token < /path/to/token.txt

Le token doit avoir au minimum les scopes repo (et read:org si tu clones depuis une org privée).


Pattern général : exécuter du shell non-interactif ​

La majorité des problèmes ci-dessus partagent une cause : un outil suppose un humain devant un TTY. Quand tu pilotes le VPS depuis Claude/CI/cron, applique systématiquement :

OutilFlag / variableEffet
aptDEBIAN_FRONTEND=noninteractivesupprime les prompts whiptail
apt-o Dpkg::Options::="--force-confold"garde les configs locales sans demander
needrestartNEEDRESTART_MODE=aredémarre les services automatiquement
gpg--batch --yespas de prompt, pas d'écriture sur TTY
ssh-o BatchMode=yeséchoue tôt si une auth interactive est demandée
systemctl--no-pagerpas de pipe vers less
gitGIT_TERMINAL_PROMPT=0échoue plutôt que de demander un mot de passe

À garder en tête au moment d'écrire un script de provisioning ou de demander à Claude d'enchaîner des étapes : si une commande est interactive en local, elle se transforme en hang silencieux ailleurs.