Skip to content

Docker Compose et .env ​

TaskFlow ne demande rien d'installé en local à part Node et Docker : PostgreSQL, sa base de test, Adminer et Redis tournent dans Docker Compose. Cette page présente le fichier docker-compose.yml, ses profils, les commandes courantes et les fichiers .env lus par l'application. Le fichier est créé au J1, Redis arrive au J9 et le service app au J11.

Le fichier docker-compose.yml ​

yaml
# docker compose up -d                 -> PostgreSQL + Adminer for `npm run start:dev`
# docker compose --profile queue up -d -> + Redis for the BullMQ queue (J9)
# docker compose --profile app up -d   -> + the API itself, built from the Dockerfile (J11)
services:
  db:
    image: postgres:16-alpine
    container_name: taskflow-db
    environment:
      POSTGRES_USER: taskflow
      POSTGRES_PASSWORD: taskflow
      POSTGRES_DB: taskflow
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U taskflow -d taskflow"]
      interval: 5s
      timeout: 3s
      retries: 10

  # Throwaway database for e2e tests: data lives in memory (tmpfs).
  db-test:
    image: postgres:16-alpine
    container_name: taskflow-db-test
    environment:
      POSTGRES_USER: taskflow
      POSTGRES_PASSWORD: taskflow
      POSTGRES_DB: taskflow_test
    ports:
      - "5433:5432"
    tmpfs:
      - /var/lib/postgresql/data

  adminer:
    image: adminer
    ports:
      - "8081:8080"
    depends_on:
      - db

  redis:
    image: redis:7-alpine
    profiles: [queue]
    ports:
      - "6379:6379"

  app:
    build: .
    profiles: [app]
    environment:
      NODE_ENV: production
      PORT: 3000
      DB_HOST: db
      DB_PORT: 5432
      DB_USER: taskflow
      DB_PASSWORD: taskflow
      DB_NAME: taskflow
      # Empty unless exported: the app then refuses to start (validateEnv), while
      # the other services still work without it.
      JWT_SECRET: ${JWT_SECRET:-}
    ports:
      - "3000:3000"
    depends_on:
      db:
        condition: service_healthy

volumes:
  pgdata:

Les services ​

ServiceImagePort localProfilRôle
dbpostgres:16-alpine5432(aucun)Base de développement, données dans le volume pgdata
db-testpostgres:16-alpine5433(aucun)Base des tests e2e, données en mémoire (tmpfs)
admineradminer8081(aucun)Interface web pour consulter la base
redisredis:7-alpine6379queueFile BullMQ des notifications (optionnelle)
appconstruite depuis le Dockerfile3000appL'API elle-même, conteneurisée

db est la base utilisée par npm run start:dev. Ses données survivent aux redémarrages grâce au volume nommé pgdata. Le healthcheck lance pg_isready toutes les 5 secondes : il indique à Compose quand PostgreSQL accepte vraiment des connexions, ce dont dépend le service app.

db-test est une seconde instance PostgreSQL, sur le port 5433, avec une base taskflow_test. Son répertoire de données est monté en tmpfs, c'est-à-dire en mémoire : la base est rapide et repart vide à chaque arrêt du conteneur. Les tests e2e y appliquent les migrations puis vident les tables avant de s'exécuter (J10). C'est l'option « service Compose » du plan, à la place de Testcontainers.

adminer est une interface web d'administration de base de données, sur http://localhost:8081. Pour vous connecter, choisissez PostgreSQL et indiquez le serveur db (le nom du service sur le réseau Compose, pas localhost), l'utilisateur et le mot de passe taskflow, la base taskflow.

redis sert de stockage à la file BullMQ du J9. Il est optionnel, comme dans le plan : TaskFlow ne charge le module de file que si REDIS_URL est défini.

app construit l'image à partir du Dockerfile (J11) et la lance en NODE_ENV=production, connectée au service db. Trois détails :

  • JWT_SECRET: ${JWT_SECRET:-} : la valeur vient de votre shell (ou d'un fichier .env à côté de docker-compose.yml), et vaut une chaîne vide sinon. Aucun secret n'est écrit dans le fichier, et les autres services n'en ont pas besoin : docker compose up -d ou docker compose up -d db-test fonctionnent sans rien exporter.
  • Sans secret, c'est le conteneur app qui refuse de démarrer : la validation de l'environnement (J7) l'arrête aussitôt sur « property JWT_SECRET has failed the following constraints: minLength, isString ». Exportez une vraie valeur avant le profil app.
  • depends_on avec condition: service_healthy attend que le healthcheck de db réussisse avant de lancer l'API.

L'image n'applique pas les migrations au démarrage : c'est une étape séparée. npm run migration:run:prod exécute les migrations compilées (dist/) dans un conteneur ponctuel de la même image, avant le démarrage de l'API (séquence dans commandes courantes, détails au J11). Le conteneur app ne reçoit pas REDIS_URL : il fonctionne sans file, les notifications sont alors simplement journalisées.

Vérifié sur TaskFlow : l'image se construit, migration:run:prod crée toutes les tables sur une base vide, puis le conteneur démarre sous l'utilisateur node et /health répond ok. Le déploiement sur un serveur n'a pas été vérifié.

Un secret exporté vaut aussi pour npm run start:dev

Une variable exportée l'emporte sur les fichiers .env.* pour toute application lancée depuis ce shell (ordre de lecture) : le JWT_SECRET exporté pour le profil app remplace alors celui de .env.development.

Écarts avec le fichier du plan ​

Le fichier proposé par le plan contient db, db-test, adminer et redis, ce dernier marqué « Optionnel (J9) » mais démarré à chaque docker compose up. TaskFlow place redis derrière le profil queue et ajoute le service app (profil app), prévu par le J11 du plan (« Dockerfile et service app dans le Compose »).

Profils Compose ​

Un service qui déclare profiles ne démarre que si l'un de ses profils est activé (--profile), ou si vous le nommez explicitement sur la ligne de commande. Les services sans profil démarrent à chaque docker compose up qui ne nomme aucun service.

CommandeServices démarrés
docker compose up -ddb, db-test, adminer
docker compose --profile queue up -dles mêmes + redis
docker compose --profile app up -d --buildles mêmes + app (image reconstruite ; JWT_SECRET exporté et migrations appliquées)
docker compose --profile queue --profile app up -dtout (même condition pour app)
docker compose up -d db-testdb-test seul (pour lancer les e2e)

Vérifié avec docker compose config --services : sans profil, la liste est db, adminer, db-test ; --profile queue y ajoute redis, --profile app y ajoute app.

Les profils Compose n'ont rien à voir avec les profils Spring ni avec NODE_ENV : ils choisissent quels conteneurs démarrer, pas comment l'application se configure. La configuration par environnement est traitée au J7.

Commandes courantes ​

bash
docker compose up -d                        # démarre db, db-test et adminer en arrière-plan
docker compose ps                           # état des conteneurs (et du healthcheck de db)
docker compose logs -f db                   # suit les logs de PostgreSQL
docker compose exec db psql -U taskflow -d taskflow   # console SQL sur la base de dev
docker compose stop                         # arrête les conteneurs, garde tout
docker compose down                         # supprime les conteneurs, garde le volume pgdata

Pour les tests e2e, avec un fichier .env.test (voir plus bas) :

bash
docker compose up -d db-test
npm run test:e2e

Pour l'API conteneurisée (profil app), dans cet ordre :

bash
export JWT_SECRET=$(openssl rand -hex 32)    # exigé par le seul conteneur app
docker compose --profile app build
docker compose --profile app run --rm app npm run migration:run:prod
docker compose --profile app up -d

Perte de données

docker compose down -v supprime aussi le volume pgdata, donc toute la base de développement. Après cela, relancez npm run migration:run puis npm run seed.

Fichiers .env ​

L'application lit sa configuration dans des variables d'environnement, validées au démarrage (J7). En développement et en test, elles viennent de fichiers .env.*. Le modèle versionné :

dotenv
# Copy to .env.development (npm run start:dev) and .env.test (npm run test:e2e).
# Only this example file is versioned.
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_USER=taskflow
DB_PASSWORD=taskflow
DB_NAME=taskflow
JWT_SECRET=change-me
JWT_EXPIRES_IN=15m
# typeorm (PostgreSQL) or memory (the J2-J3 in-memory store)
PROJECTS_STORE=typeorm
# Set to enable the BullMQ notifications queue (docker compose --profile queue up -d)
# REDIS_URL=redis://localhost:6379
# Comma-separated origins allowed by CORS; unset = no cross-origin access
# CORS_ORIGIN=http://localhost:5173

Les variables ​

VariableRôleContrôle au démarrage
NODE_ENVEnvironnement : choisit le fichier .env.* et le niveau de logdevelopment, test ou production (défaut development)
PORTPort HTTP de l'APIEntier entre 1 et 65535 (défaut 3000)
DB_HOST, DB_PORTAdresse de PostgreSQLChaîne ; entier (défauts localhost, 5432)
DB_USER, DB_PASSWORD, DB_NAMEIdentifiants et baseChaînes obligatoires
JWT_SECRETClé de signature des tokensObligatoire, 8 caractères au moins
JWT_EXPIRES_INDurée de vie de l'access tokenFormat 15m, 1h, 7d… (défaut 15m)
PROJECTS_STOREImplémentation du store de projetstypeorm ou memory (défaut typeorm)
REDIS_URLActive la file BullMQ des notificationsOptionnelle
CORS_ORIGINOrigines autorisées par CORS, séparées par des virgulesLue directement dans app.setup.ts, hors schéma de validation ; absente = aucun accès cross-origin

Le script de seed lit en plus SEED_ADMIN_PASSWORD (défaut admin-password-dev), absente de .env.example.

Quels fichiers, et lesquels versionner ​

FichierUtilisé parVersionné
.env.examplePersonne : c'est le modèle à copierOui, seul fichier .env* versionné
.env.developmentnpm run start:dev, le CLI TypeORM (migrations) et le seedNon
.env.testnpm run test:e2e (Jest fixe NODE_ENV=test s'il n'est pas défini)Non
.envRepli optionnel, lu après le fichier propre à l'environnementNon

Le .gitignore de TaskFlow (examples/.gitignore) ignore .env* sauf .env.example, et son .dockerignore fait de même : aucun fichier d'environnement local n'entre dans git ni dans l'image.

Pour créer vos fichiers locaux :

bash
cp .env.example .env.development
cp .env.example .env.test

Dans .env.test, pointez vers la base de test : NODE_ENV=test, DB_PORT=5433, DB_NAME=taskflow_test.

Ordre de lecture ​

AppModule charge .env.${NODE_ENV} puis .env (envFilePath dans la configuration) :

  • si une variable figure dans les deux fichiers, le premier l'emporte ;
  • une variable déjà définie dans l'environnement du processus (shell, CI, environment de Compose) l'emporte sur les fichiers, comme une variable d'environnement l'emporte sur application.yml dans Spring Boot.

C'est ce qui permet à la CI de TaskFlow et au service app de se passer de fichier : ils fournissent les variables directement. Le CLI TypeORM (src/database/data-source.ts), qui ne démarre pas Nest, lit le même fichier .env.${NODE_ENV} avec process.loadEnvFile().

Secrets

JWT_SECRET=change-me n'est qu'une valeur de développement. En production, le secret vient du gestionnaire de secrets ou des variables protégées de la CI, jamais d'un fichier versionné.