Aller au contenu

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é.

  1. Récupérez les sources et copiez le fichier d’environnement.

    Fenêtre de terminal
    git clone https://github.com/Metalhearf/badlen.git
    cd badlen
    cp .env.example .env
  2. 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
  3. Construisez l’image et démarrez les deux conteneurs.

    Fenêtre de terminal
    docker compose up -d --build

    La 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.

  4. Ouvrez http://localhost:8080, ou le WEB_PORT que 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
docker-compose.yml
# 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:

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à.

Fenêtre de terminal
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.

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.

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.1
WEB_PORT=8080
PUBLIC_URL=http://localhost:8080

Accessible 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.0
WEB_PORT=9000
PUBLIC_URL=http://192.168.1.20:9000

Derrière un reverse proxy sur le même hôte, en HTTPS sous votre propre domaine :

BIND_ADDRESS=127.0.0.1
WEB_PORT=8080
PUBLIC_URL=https://money.example.org
COOKIE_SECURE=true
TRUSTED_PROXY_HOPS=1

Dans 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.

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.

Fenêtre de terminal
docker compose ps
docker compose logs app | tail -30

Le 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.

Fenêtre de terminal
docker compose stop # conteneurs arrêtés, rien de supprimé
docker compose down # conteneurs et réseau supprimés ; le volume reste intact
docker compose up -d # de retour, sans reconstruire l'image
docker 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.