Aller au contenu

SSH & SCP — Commandes distantes : guide pratique

Résumé

Guide complet des commandes SSH et SCP utilisées pour administrer un serveur distant (VPS Hetzner). Couvre l'exécution de commandes, le transfert de fichiers, les pièges à éviter, et les patterns avancés (python3, docker, heredoc) découverts lors du déploiement de [[PsychoEnLigneV2]].


1. Anatomie d'une commande SSH

ssh -i ~/.ssh/kanmaber_hetzner kanmaber@178.105.46.57 "commande 2>&1"
Partie Rôle
ssh Client SSH (Secure Shell) — ouvre un tunnel chiffré
-i ~/.ssh/kanmaber_hetzner Clé privée à utiliser (sans -i, SSH cherche ~/.ssh/id_rsa ou id_ed25519)
kanmaber@178.105.46.57 utilisateur@adresse-ip-ou-domaine
"commande" Commande à exécuter sur le serveur. Sans guillemets = session interactive
2>&1 Redirige stderr vers stdout — capture TOUTES les sorties (erreurs incluses)

Avec vs sans commande

# Session interactive (terminal ouvert sur le serveur)
ssh -i ~/.ssh/kanmaber_hetzner kanmaber@178.105.46.57

# Exécution non-interactive (une commande, résultat retourné)
ssh -i ~/.ssh/kanmaber_hetzner kanmaber@178.105.46.57 "ls /home/kanmaber"

Chaîner plusieurs commandes

# && : s'arrête si une commande échoue
ssh ... "cd /mon/dossier && npm install && npm run build 2>&1"

# ; : exécute toutes les commandes même si une échoue
ssh ... "cmd1; cmd2; cmd3"

2. Anatomie d'une commande SCP

scp -i ~/.ssh/kanmaber_hetzner /local/fichier.ts \
  kanmaber@178.105.46.57:/remote/chemin/fichier.ts
Partie Rôle
scp Secure Copy — transfert de fichiers via SSH
-i ~/.ssh/kanmaber_hetzner Clé privée (même logique que SSH)
/local/fichier.ts Source locale
kanmaber@ip:/chemin Destination distante

Sens du transfert

# LOCAL → DISTANT (upload)
scp -i clé fichier_local.ts utilisateur@ip:/chemin/distant/

# DISTANT → LOCAL (download)
scp -i clé utilisateur@ip:/chemin/distant/fichier.ts ./local/

# Dossier entier (-r pour récursif)
scp -r -i clé ./dossier/ utilisateur@ip:/chemin/distant/

Plusieurs fichiers en une fois

scp -i clé \
  fichier1.ts \
  fichier2.ts \
  fichier3.ts \
  utilisateur@ip:/même/dossier/distant/

3. Utilisateurs et permissions

Le problème des fichiers Docker (UID 10000)

Quand Docker crée des fichiers via un bind mount, ils appartiennent à l'UID interne du container (souvent 10000 pour des images comme Hermes). L'utilisateur kanmaber ne peut pas les écrire.

-rw-r--r-- 1 10000 hermes-docker  fichier.ts   ← kanmaber ne peut PAS écrire
-rw-r--r-- 1 kanmaber kanmaber    fichier.ts   ← kanmaber PEUT écrire

Solutions par ordre de préférence

1. SCP en root (si root a une clé SSH configurée ou accepte la connexion)

scp fichier.ts root@ip:/chemin/fichier.ts

2. SSH + sudo cp (interactif, nécessite le mot de passe sudo)

# Copier d'abord dans /tmp (toujours accessible)
scp -i clé fichier.ts kanmaber@ip:/tmp/fichier.ts
# Puis en SSH interactif :
ssh -i clé kanmaber@ip
sudo cp /tmp/fichier.ts /chemin/protégé/fichier.ts

3. docker run pour écrire dans le volume (sans sudo)

ssh kanmaber@ip "
docker run --rm \
  -v /home/kanmaber/projets:/project \
  -v /tmp/fichier.ts:/src/fichier.ts:ro \
  busybox cp /src/fichier.ts /project/chemin/fichier.ts
"

4. Fix permanent — docker-compose avec l'UID hôte

services:
  monservice:
    user: "${HOST_UID:-1000}:${HOST_GID:-1000}"
HOST_UID=$(id -u) HOST_GID=$(id -g) docker compose up --build


4. Édition de fichiers distants

python3 pour les fichiers UTF-8 (avec accents)

sed échoue sur les caractères accentués. Toujours utiliser python3 :

ssh kanmaber@ip "python3 -c \"
path = '/chemin/fichier.ts'
with open(path, 'r') as f:
    content = f.read()
content = content.replace('ancien texte', 'nouveau texte')
with open(path, 'w') as f:
    f.write(content)
print('OK')
\""

Suppression de lignes spécifiques

ssh kanmaber@ip "python3 -c \"
path = '/chemin/fichier.ts'
with open(path, 'r') as f:
    lines = f.readlines()
# Supprimer les lignes 295 et 296 (index 0-based)
lines = [l for i, l in enumerate(lines) if i not in (295, 296)]
with open(path, 'w') as f:
    f.writelines(lines)
print(f'{len(lines)} lignes restantes')
\""

Modifier un JSON (package.json, etc.)

ssh kanmaber@ip "python3 -c \"
import json
path = '/chemin/package.json'
with open(path) as f:
    pkg = json.load(f)
pkg['scripts']['build'] = 'tsc && tsc-alias'
with open(path, 'w') as f:
    json.dump(pkg, f, indent=2, ensure_ascii=False)
print('scripts.build:', pkg['scripts']['build'])
\""

Heredoc pour écrire un fichier complet

ssh kanmaber@ip "cat > /tmp/mon_fichier.ts << 'ENDOFFILE'
// Contenu du fichier
export const value = 42;
ENDOFFILE
echo 'fichier écrit'"

⚠️ Le 'ENDOFFILE' entre guillemets simples empêche l'interprétation des variables shell dans le contenu. Sans guillemets, $variable serait interprété.


5. Docker via SSH

Lister les containers en cours

ssh kanmaber@ip "docker ps --format '{{.Names}} {{.Status}} {{.Image}}'"

Lire les logs d'un container

# 20 dernières lignes
ssh kanmaber@ip "docker logs psycho-backend --tail 20 2>&1"

# Filtrer avec grep
ssh kanmaber@ip "docker logs psycho-backend 2>&1 | grep -E 'error|warn|info' | tail -15"

Build d'un service docker-compose

# Avec cache (rapide)
ssh kanmaber@ip "cd /home/kanmaber/projets/PsychoEnLigne && docker compose build backend 2>&1"

# Sans cache (force tout reconstruire)
ssh kanmaber@ip "cd /home/kanmaber/projets/PsychoEnLigne && docker compose build --no-cache backend 2>&1"

Démarrer / redémarrer un service

# Démarrer avec ses dépendances (postgres, redis)
ssh kanmaber@ip "cd /projet && docker compose up -d backend"

# Démarrer SANS recréer les dépendances déjà actives
ssh kanmaber@ip "cd /projet && docker compose up -d --no-deps backend"

# Redémarrer proprement
ssh kanmaber@ip "docker rm -f psycho-backend; cd /projet && docker compose up -d --no-deps backend"

Copier un fichier dans un container actif

# Hôte → Container
ssh kanmaber@ip "docker cp /tmp/fichier.ts psycho-backend:/app/src/fichier.ts"

# Container → Hôte
ssh kanmaber@ip "docker cp psycho-backend:/app/dist/server.js /tmp/server.js"

6. Diagnostics courants

Vérifier qu'un fichier existe et ses permissions

ssh kanmaber@ip "ls -la /chemin/fichier.ts"

Compter les lignes d'un fichier

ssh kanmaber@ip "wc -l /chemin/fichier.ts"

Lire les lignes N à M d'un fichier (avec numéros)

# Lignes 270 à 300, avec numéros
ssh kanmaber@ip "cat -n /chemin/fichier.ts | sed -n '270,300p'"

Chercher un pattern dans les sources

ssh kanmaber@ip "grep -rn 'REDIS_HOST\|localhost' /projet/BackEnd/src --include='*.ts' | head -10"

7. Pièges & règles de survie

Piège Solution
sed casse les accents Utiliser python3 open()
Docker cache le COPY . . docker compose build --no-cache
Container utilise localhost au lieu du hostname Docker Passer REDIS_HOST=redis dans les env vars
Fichier appartenant à UID 10000 SCP en root ou docker run busybox cp
scp avec /dev/stdin échoue Écrire d'abord dans /tmp, puis scp depuis /tmp
sudo sans terminal (non-interactif) Passer par une session SSH interactive
npm run build passe mais module introuvable au runtime Les alias @shared ne sont pas résolus → ajouter tsc-alias
Prisma : linux-musl introuvable dans Alpine Ajouter binaryTargets = ["native", "linux-musl-openssl-3.0.x"] + prisma generate dans Dockerfile

Points clés

  • 2>&1 est presque toujours nécessaire pour capturer les erreurs
  • Préférer python3 à sed dès qu'il y a des accents ou des caractères spéciaux
  • Le --no-deps de docker-compose évite de recréer des containers déjà actifs
  • Pour les fichiers protégés : tenter root@ip sans clé (scp fichier root@ip:/chemin)
  • Un container Docker qui redémarre en boucle → lire ses logs AVANT de modifier quoi que ce soit

Liens connexes