diff --git a/ops/README.md b/ops/README.md index 3a9230a16..e361d5f64 100644 --- a/ops/README.md +++ b/ops/README.md @@ -155,3 +155,51 @@ construit sans elle ne le doit qu'à un `.env` traînant à sa racine. Mémoire mesurée sur ce dépôt : **1,8 Go de PSS au pic**, et non les 5 Go que donne la somme naïve des RSS, qui recompte la mémoire partagée. Sur une machine à 2 Go, c'est jouable service arrêté, mais sans marge. + + +## Faire tourner une instance sous l'utilisateur `homarr` + +L'unité `homarr-dev.service` déclare `User=homarr`. Le `run.sh` de l'image ne +peut pas fonctionner ainsi : il écrit `/etc/nginx/nginx.conf` et écoute le port +80. `ops/run-rootless.sh` le remplace — migrations, secret d'authentification, +serveur Next, et rien d'autre. + +```bash +sudo install -m 755 ops/run-rootless.sh /opt/homarr-dev/run.sh +sudo chown -R homarr:homarr /opt/homarr-dev/apps/nextjs/.next +``` + +Le `chown` n'est pas facultatif : Next écrit son cache d'images sous +`.next/cache` pendant l'exécution. + +Trois réglages à corriger dans `/etc/homarr/homarr-dev.env`, chacun pour une +raison distincte : + +```ini +DB_URL='/var/lib/homarr-dev/db.sqlite' +LOCAL_CERTIFICATE_PATH='/var/lib/homarr-dev/trusted-certificates' +``` + +`DB_URL` doit quitter `/var/lib/homarr`, qui appartient à root : SQLite écrit +aussi ses fichiers `-wal` et `-shm`, donc il lui faut le **dossier** en écriture, +pas seulement le fichier. `/var/lib/homarr-dev` est créé et donné à l'utilisateur +du service par le `StateDirectory=` de l'unité — rien à chowner. + +`LOCAL_CERTIFICATE_PATH` doit être posée, sans quoi l'application retombe sur le +dossier de l'image, tente de le recréer et échoue faute de droits. Le CA qui +valide la console UniFi doit y être recopié : + +```bash +sudo cp /var/lib/homarr/trusted-certificates/*.pem /var/lib/homarr-dev/trusted-certificates/ +sudo chown -R homarr:homarr /var/lib/homarr-dev +``` + +Reste le proxy, qui ne peut pas tourner sous `homarr` : `ops/homarr-proxy.nginx.conf` +est à déposer dans `/etc/nginx/sites-available/` et à activer. Il n'est pas +optionnel — le client appelle `ws://:/websockets` sur la même origine +que la page, alors que le serveur WebSocket écoute sur 3001. Sans lui, +l'application se charge et perd ses mises à jour en direct, sans rien dire. + +Enfin : **les deux instances ne peuvent pas tourner en même temps.** Le port du +serveur WebSocket est écrit en dur dans `apps/websocket/src/main.ts` (3001), donc +même en déplaçant le port HTTP, la seconde instance échouerait à se lier. diff --git a/ops/homarr-proxy.nginx.conf b/ops/homarr-proxy.nginx.conf new file mode 100644 index 000000000..ceb5d7ae2 --- /dev/null +++ b/ops/homarr-proxy.nginx.conf @@ -0,0 +1,41 @@ +# Proxy inverse pour une instance Homarr lancée sous un utilisateur ordinaire. +# +# À déposer dans /etc/nginx/sites-available/ et à lier dans sites-enabled/, puis +# `sudo systemctl reload nginx`. C'est le nginx du système qui le sert — celui de +# l'image est lancé par son run.sh, ce que l'utilisateur `homarr` ne peut pas +# faire. +# +# Les deux emplacements ne sont pas interchangeables : le serveur WebSocket est +# un processus distinct sur le port 3001, embarqué dans le serveur Next mais +# écoutant à part, et le client l'appelle sur la MÊME origine que la page. Sans +# la première règle, l'application se charge et les mises à jour en direct +# meurent silencieusement. +# +# Le port 80 est déjà pris par l'instance de production. Tant qu'elles +# alternent, ça va ; pour les faire cohabiter il faudrait aussi déplacer les +# ports applicatifs, or celui du WebSocket est écrit en dur dans le code (3001), +# donc deux instances ne peuvent pas tourner en même temps. + +server { + listen 8080; + listen [::]:8080; + + server_name _; + + location /websockets { + proxy_pass http://127.0.0.1:3001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $http_host; + } + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + client_max_body_size 256M; + } +} diff --git a/ops/run-rootless.sh b/ops/run-rootless.sh new file mode 100755 index 000000000..74679ce0a --- /dev/null +++ b/ops/run-rootless.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# +# Démarrage d'une instance Homarr sous un utilisateur non privilégié. +# +# À poser à la place du `run.sh` de l'instance (ou à viser directement depuis +# l'`ExecStart` de l'unité). Le `run.sh` du dépôt suppose root et un conteneur ; +# celui-ci suppose systemd et un utilisateur ordinaire. Les différences, et leur +# raison : +# +# - Pas de nginx. Écrire /etc/nginx/nginx.conf et écouter le port 80 exigent +# tous deux root. Le proxy inverse devient un service système distinct — il +# reste NÉCESSAIRE : le client se connecte à ws://:/websockets, +# sur la même origine que la page, alors que le serveur WebSocket écoute sur +# 3001. Sans proxy, l'application répond mais perd ses mises à jour en direct. +# +# - Pas de redis interne. `redis-server` est un service système ici +# (REDIS_IS_EXTERNAL=true), et le lancer depuis ce script le ferait tourner +# sous l'utilisateur du service, sans son répertoire d'état. +# +# - Pas de `mkdir` d'état. `StateDirectory=` dans l'unité crée le dossier ET +# le donne à l'utilisateur du service, ce qu'un `mkdir` lancé sans droits ne +# peut pas faire. +# +# - Pas de boucle de redémarrage ni de `trap`. `Restart=on-failure` le fait +# déjà, et mieux : il compte les échecs, applique un délai, et n'a pas de +# PID à perdre. D'où le `exec` final — le serveur Next devient le processus +# principal de l'unité, celui que systemd surveille et signale directement. +# +set -u + +if [ "${DB_MIGRATIONS_DISABLED:-}" = "true" ]; then + echo "DB migrations are disabled, skipping" +else + echo "Running DB migrations" + if ! DISABLE_REDIS_LOGS=true node "./db/migrations/${DB_DIALECT}/migrate.cjs" "./db/migrations/${DB_DIALECT}"; then + echo "ERROR: DB migrations failed, aborting startup" >&2 + exit 1 + fi +fi + +# Requis par le cadre d'authentification, mais inutilisé : ni JWT ni hachage de +# courriel ici. Régénéré à chaque démarrage, comme dans l'image. +AUTH_SECRET="$(openssl rand -base64 32)" +export AUTH_SECRET + +# L'image écoute sur 0.0.0.0 parce que le nom d'hôte que Docker fabrique n'est +# pas toujours résolvable. Derrière un proxy local, la boucle locale suffit et +# évite d'exposer le serveur applicatif directement. Surchargeable par l'unité. +export HOSTNAME="${HOSTNAME:-127.0.0.1}" + +exec node apps/nextjs/server.js