Pourquoi ce setup plutôt qu'un autre ?Why this setup rather than another?
Un seul mécanisme d'accès, aucune surface publique, et tout est scripté et vérifiable. One access mechanism, no public surface, and everything is scripted and verifiable.
Pas d'exposition publiqueNo public exposure
Aucun port HTTP/HTTPS ouvert sur l'internet. L'accès passe exclusivement par un tunnel SSH, avec une authentification applicative en plus.No HTTP/HTTPS port open to the internet. Access goes exclusively through an SSH tunnel, with app-level authentication on top.
LégerLightweight
Un VPS 1 vCPU / 2 Go de RAM suffit — opencode ne fait que des appels API sortants, il ne fait tourner aucun modèle en local (pas de GPU).A 1 vCPU / 2 GB RAM VPS is enough — opencode only makes outbound API calls; it runs no model locally (no GPU).
SimpleSimple
Un seul mécanisme d'accès (SSH), pas de daemon tiers, pas de compte externe à gérer.A single access mechanism (SSH), no third-party daemon, no external account to manage.
ReproductibleReproducible
Tout est scripté (un Makefile + un script de vérification), sans configuration manuelle non documentée.Everything is scripted (a Makefile + a verification script), with no undocumented manual configuration.
VérifiableVerifiable
Une suite de tests (make test) contrôle l'état réel du serveur, y compris les points de sécurité les plus faciles à mal configurer.A test suite (make test) checks the server's actual state, including the security points easiest to misconfigure.
Architecture
Le VPS n'a aucun port applicatif ouvert sur l'internet — seul SSH (22) l'est. Tout le trafic vers opencode transite par le tunnel, chiffré de bout en bout. The VPS has no application port open to the internet — only SSH (22) is. All traffic to opencode goes through the tunnel, encrypted end to end.
ufw : seulonly :22 ouvertopenopencode web @
127.0.0.1PrérequisRequirements
- Un VPS Ubuntu 24.04 avec accès root/sudo et ta clé SSH déjà installée dessus.An Ubuntu 24.04 VPS with root/sudo access and your SSH key already installed on it.
make,sshetscpdisponibles en local (déjà présents sur macOS/Linux).make,sshandscpavailable locally (already present on macOS/Linux).
Testé chez Gandi, mais rien n'y est spécifique. N'importe quel hébergeur convient : Hetzner Cloud, DigitalOcean, Vultr, Scaleway, OVHcloud, Linode/Akamai… Seule l'étape gandi-firewall porte le nom de Gandi — elle ne fait que rappeler de fermer les ports publics dans le pare-feu cloud de ta console (tous les hébergeurs en proposent un).
Tested on Gandi, but nothing is host-specific. Any provider works: Hetzner Cloud, DigitalOcean, Vultr, Scaleway, OVHcloud, Linode/Akamai… Only the gandi-firewall step is named after Gandi — it merely reminds you to close public ports in your console's cloud firewall (every provider has one).
Installation
Le pipeline s'arrête à chaque étape qui demande une action de ta part et affiche exactement quoi faire. Compte 10–15 minutes. The pipeline pauses at every step that needs an action from you and shows exactly what to do. Budget 10–15 minutes.
-
Cloner le dépôtClone the repository
Récupère le
Makefileet les scripts en local.Grab the
Makefileand scripts locally.bash $ git clone https://github.com/StevenMorlier/open_router_vps.git $ cd open_router_vps -
Tout dérouler avec
make allRun it all withmake allUne seule commande enchaîne durcissement, swap, mises à jour, installation d'opencode et vérifications, avec des pauses de validation.
A single command chains hardening, swap, updates, opencode install and checks, with confirmation pauses.
bash $ make all VPS_HOST=<ip-du-vps>Chaque étape est aussi utilisable seule (
make harden VPS_HOST=…, etc.). Voici ce quemake allenchaîne :Each step can also run on its own (make harden VPS_HOST=…, etc.). Here is whatmake allchains:ÉtapeStep Ce qu'elle faitWhat it does gandi-firewallRappel des réglages à faire dans la console de ton hébergeur (pare-feu cloud).Reminder of the settings to make in your provider's console (cloud firewall). hardenufw: SSH autorisé, tout le reste bloqué + désactivation de l'auth SSH par mot de passe (clé uniquement).ufw: SSH allowed, everything else blocked + SSH password auth disabled (key only).swapAjoute un fichier de swap (filet de sécurité sur un VPS à faible RAM).Adds a swap file (safety net on a low-RAM VPS). security-updatesActive les mises à jour de sécurité auto + demande si le reboot auto doit être activé.Enables automatic security updates + asks whether auto-reboot should be on. opencode-installInstalle opencode via le script officiel.Installs opencode via the official script. opencode-web-serviceDéploie opencode weben service systemd, lié à127.0.0.1uniquement, mot de passe généré.Deploysopencode webas a systemd service, bound to127.0.0.1only, generated password.testSuite de vérifications de bout en bout.End-to-end verification suite. Note le mot de passeWrite down the password
L'étape
opencode-web-servicegénère un mot de passe applicatif affiché une seule fois. Note-le : l'identifiant à saisir dans le navigateur estopencode.Theopencode-web-servicestep generates an app password shown only once. Write it down: the username to enter in the browser isopencode. -
Se connecter via le tunnelConnect through the tunnel
Cette commande reste au premier plan (Ctrl+C pour fermer) et ouvre le tunnel SSH.
This command stays in the foreground (Ctrl+C to close) and opens the SSH tunnel.
bash $ make tunnel VPS_HOST=<ip-du-vps>Une fois connecté, ouvre
http://127.0.0.1:4096dans ton navigateur.Once connected, open
http://127.0.0.1:4096in your browser.
opencode web, accédé en local via le tunnel SSH.opencode web, reached locally through the SSH tunnel.
Usage quotidienDaily use
Au quotidien, une seule commande suffit. Ouvre le tunnel, garde le terminal ouvert pendant la session, puis rends-toi sur http://127.0.0.1:4096.Day to day, one command is enough. Open the tunnel, keep the terminal open during your session, then head to http://127.0.0.1:4096.
$ make tunnel VPS_HOST=<ip-du-vps>
==> http://127.0.0.1:4096 une fois connecté (Ctrl+C pour fermer)
Mettre le Mac en veille coupe la connexion ; il suffit de relancer make tunnel. Ce n'est pas un accès « toujours connecté » en arrière-plan.Putting the Mac to sleep drops the connection; just re-run make tunnel. This is not an always-on background access.
Accès depuis iPhone (Termius)Access from iPhone (Termius)
Le tunnel n'est pas limité au Mac : tout client SSH mobile supportant le « local port forwarding » fonctionne, par exemple Termius. Dans Termius : Vaults → Port Forwarding → +, avec Local Port / Destination port = 4096 et Destination address = 127.0.0.1.The tunnel isn't limited to a Mac: any mobile SSH client supporting local port forwarding works, e.g. Termius. In Termius: Vaults → Port Forwarding → +, with Local Port / Destination port = 4096 and Destination address = 127.0.0.1.
Déconnexions fréquentes sur iPhoneFrequent drops on iPhone
Mise en veille de l'app, bascule Wi-Fi/5G ou passage en arrière-plan coupent la règle. Il faut la relancer manuellement : c'est un dépannage ponctuel, pas un accès « toujours connecté » fiable.App sleep, a Wi-Fi/5G switch or backgrounding cut the rule. You must restart it manually: it's an occasional workaround, not a reliable always-on access.
Synchroniser du code (GitHub)Sync your code (GitHub)
Génère une clé SSH dédiée à un repo sur le VPS et affiche la clé publique à ajouter en Deploy Key sur ce repo précis.Generate an SSH key dedicated to one repo on the VPS and print the public key to add as a Deploy Key on that specific repo.
$ make github-deploy-key VPS_HOST=<ip> REPO=mon-repo
GitHub interdit qu'une même clé serve sur plusieurs repos (key already in use) — c'est volontaire, ça limite le rayon d'action de chaque clé. Pour un autre repo, relance avec un REPO= différent. Le VPS n'a alors accès qu'aux repos où une clé a été explicitement ajoutée, jamais à tout un compte.GitHub forbids reusing one key across repos (key already in use) — intentional, it limits each key's blast radius. For another repo, re-run with a different REPO=. The VPS then only has access to the repos where a key was explicitly added, never to a whole account.
Sécurité — modèle de menaceSecurity — threat model
Les risques envisagés et le choix fait pour chacun.The risks considered and the choice made for each.
| RisqueRisk | MitigationMitigation |
|---|---|
| réseaunetwork Scan / brute-force depuis internetInternet scan / brute-force | Aucun port applicatif public — uniquement SSH (clé, pas de mot de passe).No public app port — only SSH (key, no password). |
| SSH Vol de la clé privée SSHSSH private key theft | harden force la clé uniquement ; mets une passphrase sur ta clé ; fail2ban en option si le VPS reste exposé longtemps.harden enforces key-only; put a passphrase on your key; fail2ban optionally if the VPS stays exposed long. |
| tiersthird party Accès à l'UI opencode par un tiers ayant un accès SSHA third party with SSH access reaching the opencode UI | Mot de passe applicatif dédié, indépendant du tunnel.Dedicated app password, independent of the tunnel. |
| GitHub Serveur compromis → accès au compte GitHubCompromised server → GitHub account access | Deploy keys par repo, jamais un token donnant accès à tout le compte.Deploy keys per repo, never a token granting whole-account access. |
| systèmesystem Paquets système vulnérablesVulnerable system packages | Mises à jour de sécurité automatiques ; reboot programmé au choix.Automatic security updates; scheduled reboot optional. |
Vérifie tout ça d'un coup avec make test VPS_HOST=<ip> : sortie OK / FAIL ligne par ligne, code de sortie non-nul si un problème est détecté.Check it all at once with make test VPS_HOST=<ip>: line-by-line OK / FAIL output, non-zero exit code if a problem is found.
Piège à connaîtreA gotcha to know
Sélecteur de projet cassé dans l'UI web (opencode v1.18.4)Broken project picker in the web UI (opencode v1.18.4)
Dans le dialogue « Open project », chercher un dossier par son nom renvoie toujours « No folders found », même s'il existe. Solution : tape le chemin absolu complet (ex : /home/ubuntu/projects/mon-projet) — ça déclenche une complétion de chemin qui fonctionne. Une fois ouvert, le projet reste enregistré normalement.In the "Open project" dialog, searching a folder by name always returns "No folders found", even when it exists. Fix: type the full absolute path (e.g. /home/ubuntu/projects/my-project) — this triggers a path completion that works. Once opened, the project is remembered normally.
Bugs opencode : #8325, #9795, #7111.opencode issues: #8325, #9795, #7111.
FAQ / DépannageTroubleshooting
Combien ça coûte ? (exemple)How much does it cost? (example)
Ce projet ne couvre que la partie VPS — le prix du modèle IA relève d'opencode et de ton fournisseur d'API, pas de ce setup, et tu peux brancher opencode sur l'offre de ton choix. À titre indicatif seulement, la configuration personnelle de l'auteur : VPS Gandi ~8 €/mois (1 vCPU / 2 Go de RAM) et un abonnement « opencode Go » à ~5 €/mois avec le modèle GLM 5.2. Ce sont des choix personnels donnés en exemple, pas un minimum requis.This project only covers the VPS side — the AI model's price is opencode's and your API provider's domain, not this setup, and you can point opencode at whatever plan you like. As a rough example only, the author's personal setup: a Gandi VPS at ~€8/mo (1 vCPU / 2 GB RAM) and an "opencode Go" subscription at ~€5/mo with the GLM 5.2 model. These are personal choices given as an example, not a required minimum.
Le tunnel se coupe, ou « connection refused » sur 127.0.0.1:4096The tunnel drops, or "connection refused" on 127.0.0.1:4096
make tunnel doit rester ouvert au premier plan pendant toute la session. S'il s'est fermé (Mac en veille, changement de réseau), relance make tunnel VPS_HOST=<ip>. Le tunnel envoie déjà des keepalives ; si ça coupe quand même, c'est presque toujours la veille de la machine ou une bascule réseau, pas le VPS.make tunnel must stay open in the foreground for the whole session. If it closed (Mac asleep, network change), re-run make tunnel VPS_HOST=<ip>. The tunnel already sends keepalives; if it still drops, it's almost always machine sleep or a network switch, not the VPS.
« Address already in use » au lancement de make tunnel"Address already in use" when starting make tunnel
Le port local 4096 est déjà pris (souvent un ancien tunnel resté ouvert). Trouve le process avec lsof -iTCP:4096 -sTCP:LISTEN puis ferme-le, ou ouvre le tunnel sur un autre port local :Local port 4096 is already taken (often an old tunnel left open). Find the process with lsof -iTCP:4096 -sTCP:LISTEN and close it, or open the tunnel on another local port:
$ ssh -N -L 5000:127.0.0.1:4096 ubuntu@<ip> # puis http://127.0.0.1:5000
J'ai perdu (ou je veux changer) le mot de passe opencode-webI lost (or want to change) the opencode-web password
Le mot de passe n'est stocké nulle part en clair côté client : il n'est affiché qu'une fois à l'installation. Pour en régénérer un :The password is stored nowhere in clear text on the client: it is shown only once at install time. To regenerate one:
$ ssh ubuntu@<ip>
$ NEW=$(openssl rand -base64 18); echo "$NEW"
$ printf 'OPENCODE_SERVER_PASSWORD=%s\n' "$NEW" | sudo install -m 600 /dev/stdin /etc/opencode-web.env
$ exit
$ make restart VPS_HOST=<ip> # l'EnvironmentFile n'est relu qu'au redémarrage
L'identifiant à saisir dans le navigateur est opencode, le mot de passe est celui-ci.The username to enter in the browser is opencode, the password is this one.
Le service opencode-web ne démarre pasThe opencode-web service won't start
Regarde la cause : ssh ubuntu@<ip> 'systemctl status opencode-web.service; journalctl -u opencode-web.service -n 50 --no-pager'. Causes fréquentes : opencode-install pas passé (binaire absent), dossier /home/ubuntu/projects inexistant, ou /etc/opencode-web.env manquant. Après correction : make restart puis make test.Check the cause: ssh ubuntu@<ip> 'systemctl status opencode-web.service; journalctl -u opencode-web.service -n 50 --no-pager'. Common causes: opencode-install not run (missing binary), /home/ubuntu/projects folder absent, or /etc/opencode-web.env missing. After fixing: make restart then make test.
SSH : « Permission denied (publickey) »SSH: "Permission denied (publickey)"
harden désactive l'auth par mot de passe : seule ta clé est acceptée ensuite. Assure-toi que ta clé publique est bien dans ~/.ssh/authorized_keys sur le VPS avant de lancer harden, sinon tu risques de te verrouiller dehors (récupération via la console de secours KVM/VNC de l'hébergeur).harden disables password auth: only your key is accepted afterward. Make sure your public key is in ~/.ssh/authorized_keys on the VPS before running harden, or you risk locking yourself out (recover via the provider's KVM/VNC rescue console).