Configuration
Tout se règle dans un seul fichier, .env, que vous copiez depuis le .env.example fourni par le
dépôt et que lit le docker-compose.yml placé à côté.
-
Récupérez les sources et copiez le fichier d’environnement.
Fenêtre de terminal git clone https://github.com/Metalhearf/badlen.gitcd badlencp .env.example .env -
Renseignez les trois secrets. La pile ne démarre pas tant qu’ils manquent ; le reste du fichier peut rester tel quel.
Fenêtre de terminal $EDITOR .env -
Construisez l’image et démarrez les deux conteneurs.
Fenêtre de terminal docker compose up -d --buildLa première compilation prend quelques minutes. L’application applique ensuite elle-même ses migrations en attente puis commence à répondre ; vous n’avez rien à lancer à la main sur la base de données.
-
Ouvrez
http://localhost:8080, ou leWEB_PORTque vous avez choisi. Le premier écran est la page de connexion ; Premier démarrage explique la suite.
Le docker-compose.yml que cela démarre
# Self-hosted stack: PostgreSQL and one application container serving both the interface# and its API on the published WEB_PORT. Copy .env.example to .env first.## The project name is pinned so the container and volume names are the same on every# machine. docker-compose.dev.yml carries its own, so both stacks can be up at once.name: badlen
services: db: image: postgres:18-alpine restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:-badlen} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?empty in .env; fill it with openssl rand -hex 24} POSTGRES_DB: ${POSTGRES_DB:-badlen} # The whole data home, not `data` inside it: since 18 the image puts the cluster in a # per-major subdirectory (18/docker) and refuses a volume written by an earlier major. volumes: - pgdata:/var/lib/postgresql healthcheck: test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-badlen}'] interval: 10s timeout: 5s retries: 5
# The API plus the compiled SPA it serves from /app/public: one container, one origin. # It applies the pending migrations itself before it starts serving. app: build: context: . dockerfile: Dockerfile.api target: runtime restart: unless-stopped # WEB_PORT is the port you reach the app on, BIND_ADDRESS which host interfaces it is # published on. Set BIND_ADDRESS=127.0.0.1 when only a local reverse proxy should reach it. ports: - '${BIND_ADDRESS:-0.0.0.0}:${WEB_PORT:-8080}:3001' depends_on: db: condition: service_healthy environment: NODE_ENV: production PORT: 3001 DATABASE_URL: postgresql://${POSTGRES_USER:-badlen}:${POSTGRES_PASSWORD:?empty in .env; fill it with openssl rand -hex 24}@db:5432/${POSTGRES_DB:-badlen}?schema=public JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:?empty in .env; fill it with openssl rand -hex 32} SECRET_ENCRYPTION_KEY: ${SECRET_ENCRYPTION_KEY:?empty in .env; fill it with openssl rand -base64 32} ACCESS_TOKEN_TTL: ${ACCESS_TOKEN_TTL:-15m} REFRESH_TOKEN_TTL_DAYS: ${REFRESH_TOKEN_TTL_DAYS:-7} WEB_ORIGIN: ${PUBLIC_URL:-http://localhost:8080} COOKIE_SECURE: ${COOKIE_SECURE:-false} TRUSTED_PROXY_HOPS: ${TRUSTED_PROXY_HOPS:-0} TRUSTED_PROXY_ADDRESSES: ${TRUSTED_PROXY_ADDRESSES:-} REGISTRATION_ENABLED: ${REGISTRATION_ENABLED:-true} SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-587} SMTP_SECURE: ${SMTP_SECURE:-false} SMTP_USER: ${SMTP_USER:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_FROM: ${SMTP_FROM:-} COINGECKO_DEMO_KEY: ${COINGECKO_DEMO_KEY:-} OPENFIGI_API_KEY: ${OPENFIGI_API_KEY:-} healthcheck: # /api/health answers 503 when the database does not answer, so `healthy` here means # the application is serving and can reach its database. test: - CMD - node - -e - "fetch('http://127.0.0.1:3001/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))" interval: 30s timeout: 5s retries: 5 start_period: 20s
volumes: pgdata:Les trois secrets
Section intitulée « Les trois secrets »Trois valeurs sont vides dans le fichier fourni et doivent être renseignées avant le premier démarrage. Tant que l’une d’elles manque, la pile refuse de démarrer et affiche d’un coup toutes les variables vides, avec pour chacune la commande qui la génère.
Si elles ne sont pas générées d’avance, c’est volontaire. Des clés écrites dans un fichier d’exemple seraient publiques et identiques sur toutes les installations, et quiconque oublierait de les remplacer tournerait avec ces clés-là.
openssl rand -hex 24 # à coller après POSTGRES_PASSWORD=openssl rand -hex 32 # à coller après JWT_ACCESS_SECRET=openssl rand -base64 32 # à coller après SECRET_ENCRYPTION_KEY=| Variable | À quoi elle sert | Contrainte |
|---|---|---|
POSTGRES_PASSWORD |
le mot de passe de la base de données | lettres et chiffres seulement |
JWT_ACCESS_SECRET |
signe le jeton de session | au moins 16 caractères |
SECRET_ENCRYPTION_KEY |
chiffre votre secret 2FA et les clés API de fournisseurs enregistrées | doit se décoder en exactement 32 octets |
Un export JSON scellé par une phrase de passe ne dépend volontairement pas de cette clé : le fichier contient ses propres paramètres de dérivation, et reste donc lisible même si vous avez perdu la clé.
Deux remarques sur le mot de passe de la base de données. Il est inséré tel quel dans l’URL de
connexion : un /, un ? ou un # la rend illisible, et l’application ne démarre jamais. Il n’est
lu qu’une fois, à la création du répertoire de la base : le changer ensuite ne modifie rien, sinon
que l’application perd l’accès à sa propre base.
Le reste du fichier
Section intitulée « Le reste du fichier »| Variable | Défaut | Rôle |
|---|---|---|
WEB_PORT |
8080 |
le port par lequel on accède à toute l’application |
BIND_ADDRESS |
0.0.0.0 |
les interfaces de l’hôte sur lesquelles ce port est publié ; 127.0.0.1 le limite à la machine |
PUBLIC_URL |
http://localhost:8080 |
l’adresse qui sert à construire les liens de réinitialisation, et l’origine du cookie |
COOKIE_SECURE |
false |
réserve le cookie de session à HTTPS ; à activer derrière un proxy qui termine TLS |
TRUSTED_PROXY_HOPS |
0 |
le nombre de vos reverse proxies placés devant (voir Derrière un proxy) |
TRUSTED_PROXY_ADDRESSES |
(vide) | avec deux proxies ou plus, ceux placés derrière le premier, par adresse ou plage CIDR |
REGISTRATION_ENABLED |
true |
ouvre ou non l’inscription publique (valeur par défaut seulement) |
ACCESS_TOKEN_TTL |
15m |
la durée de vie d’un jeton de session avant son renouvellement |
REFRESH_TOKEN_TTL_DAYS |
7 |
la durée de vie d’une session inutilisée |
POSTGRES_USER, POSTGRES_DB |
badlen |
l’utilisateur et le nom de la base, lus une seule fois à sa création, comme le mot de passe |
COINGECKO_DEMO_KEY, OPENFIGI_API_KEY |
vide | clés gratuites et facultatives ; les deux sources fonctionnent sans elles |
Les deux dernières relèvent de l’exploitant et ne se règlent qu’ici : l’écran des réglages n’a de
champ pour aucune des deux. REGISTRATION_ENABLED ne sert que par défaut : l’interrupteur
équivalent de Réglages → Administration est enregistré dans la base et prend le dessus sur
cette valeur dès sa première utilisation.
Dans le conteneur, PUBLIC_URL s’appelle WEB_ORIGIN : le fichier compose transmet la première
sous le nom de la seconde, et une image lancée à la main, comme dans Tester une
sauvegarde, attend le nom WEB_ORIGIN. Le fichier compose
définit aussi lui-même NODE_ENV, PORT et DATABASE_URL à partir des valeurs ci-dessus : aucune
des trois n’a donc sa place dans .env. L’application lit encore d’autres variables que le
fichier compose ne transmet pas, toutes utiles pour travailler sur les sources : WEB_DIST,
l’emplacement de l’interface compilée hors de l’image, et DEMO_SEED_ENABLED, DEMO_SEED_LOCALE
et DEMO_RESEED, qui installent le compte de démonstration qu’ouvrent les suites de tests et la
démo en ligne. Le mode production de l’image ignore ces trois dernières.
Trois réglages remplis
Section intitulée « Trois réglages remplis »Les secrets n’y figurent pas : ce sont les trois mêmes lignes dans tous les cas.
Accessible depuis cette machine seulement, et depuis aucune autre du réseau :
BIND_ADDRESS=127.0.0.1WEB_PORT=8080PUBLIC_URL=http://localhost:8080Accessible sur le réseau de la maison, sur le port de votre choix, depuis un téléphone ou un autre ordinateur :
BIND_ADDRESS=0.0.0.0WEB_PORT=9000PUBLIC_URL=http://192.168.1.20:9000Derrière un reverse proxy sur le même hôte, en HTTPS sous votre propre domaine :
BIND_ADDRESS=127.0.0.1WEB_PORT=8080PUBLIC_URL=https://money.example.orgCOOKIE_SECURE=trueTRUSTED_PROXY_HOPS=1Dans le deuxième exemple, remplacez l’adresse par celle de la machine sur votre réseau, et dans le troisième, le domaine par le vôtre. Derrière un proxy explique comment configurer le proxy lui-même.
Le serveur de messagerie
Section intitulée « Le serveur de messagerie »Badlen envoie des courriels pour un mot de passe oublié, un changement d’adresse et le relais
qui transmet une lettre au proche d’un utilisateur, et seulement si vous lui indiquez un serveur.
Si SMTP_HOST reste vide, les liens sont écrits dans le journal de l’application, comme
l’explique Premier démarrage, et le relais n’est pas proposé.
| Variable | Défaut | Rôle |
|---|---|---|
SMTP_HOST |
vide | le serveur d’envoi ; vide, l’instance n’envoie rien |
SMTP_PORT |
587 |
le port de ce serveur |
SMTP_SECURE |
false |
TLS dès le premier octet, soit le port 465 ; false passe par STARTTLS |
SMTP_USER |
vide | le compte utilisé pour s’authentifier ; vide, l’envoi se fait sans authentification |
SMTP_PASSWORD |
vide | le mot de passe de ce compte |
SMTP_FROM |
vide | l’adresse d’expédition ; à défaut, SMTP_USER |
Si vous renseignez SMTP_HOST sans SMTP_FROM ni SMTP_USER, l’application refuse de démarrer et
nomme la variable manquante. Sans ce garde-fou, le serveur rejetterait les messages sans expéditeur,
et vous croiriez l’instance cassée alors qu’il ne manque qu’une ligne.
Le serveur ne suffit pas : il faut aussi PUBLIC_URL, du tableau précédent, car c’est à partir
d’elle que le lien envoyé est construit. À sa valeur par défaut, elle vaut localhost, une adresse
que seule la machine elle-même peut ouvrir. Dans ce cas, le courriel n’est pas envoyé du tout : le
lien part dans le journal, avec une ligne qui nomme la variable. Donnez-lui l’adresse que vos
utilisateurs saisissent.
Le relais en demande davantage à cette adresse : elle
sera ouverte par quelqu’un hors de chez vous, peut-être des années plus tard. Il refuse une adresse
en localhost. Une adresse du réseau de la maison, 192.168.1.20 par exemple, passe ce contrôle
mais n’ouvre rien d’ailleurs : un relais utile demande une PUBLIC_URL joignable depuis internet.
Comme les deux interrupteurs plus haut, ces variables ne servent que par défaut. Réglages → Administration propose les six mêmes champs, enregistre chiffré ce que vous y sauvegardez et prend le dessus sur l’environnement à partir de ce moment : vous pouvez ainsi corriger un port sans redémarrer le conteneur. L’écran indique d’où viennent les valeurs en vigueur, et permet d’effacer celles qui sont enregistrées pour revenir à l’environnement. Envoyer un essai envoie un message à votre propre adresse et affiche la réponse du serveur en cas de refus.
Le mot de passe se saisit mais ne se relit jamais, ni dans l’écran ni par l’API : il est chiffré
avec SECRET_ENCRYPTION_KEY, comme les clés de fournisseurs et votre secret 2FA. Si vous restaurez
une base sous une autre clé, il faudra donc le saisir de nouveau. Un serveur injoignable, qui refuse
la connexion ou présente un certificat inconnu n’empêche jamais l’application de démarrer et ne fait
jamais échouer une demande de réinitialisation : le lien est alors écrit dans le journal.
Vérifier qu’elle est montée
Section intitulée « Vérifier qu’elle est montée »docker compose psdocker compose logs app | tail -30Le fichier .env n’est lu qu’à la création des conteneurs : après une modification, relancez
docker compose up -d. Un simple redémarrage garde les anciennes valeurs.
Arrêter et redémarrer
Section intitulée « Arrêter et redémarrer »docker compose stop # conteneurs arrêtés, rien de supprimédocker compose down # conteneurs et réseau supprimés ; le volume reste intactdocker compose up -d # de retour, sans reconstruire l'imagedocker compose down -v # le volume aussi : tout le contenu de la base est perduÉtape suivante : Premier démarrage, où l’on crée le premier compte, qui devient propriétaire de l’instance.