chore(ops): start Homarr as an unprivileged user under systemd
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
977bfece6e
commit
b963a26446
3 files changed
+140
No files matched your search
@@ -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://<hôte>:<port>/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.
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+51
@@ -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://<hôte>:<port>/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
|
||||
Reference in new issue
Block a user