Aller au contenu

Deployment

Version française
Back to index


Primary source: docs/DEPLOIEMENT.md (complete reference, kept in place).


Architecture

Internet
    │
    ▼
┌─────────────────────────────────────────┐
│              Traefik                    │
│  (TLS Let's Encrypt, CORS MinIO)        │
└──────────────────┬──────────────────────┘
                   │
      ┌────────────┴────────────┐
      ▼                         ▼
┌─────────────┐        ┌─────────────────────┐
│  Next.js    │◄──────►│  MinIO (S3)         │
│  port 3000  │ upload │  port 9000 (API)    │
│             │        │  port 9001 (console)│
└──────┬──────┘        └─────────────────────┘
       │                         ▲
       ▼                         │ GET audio (direct)
┌─────────────┐             Browser client
│ PostgreSQL  │
│  port 5432  │
└─────────────┘

Role of each component

Component Role
Traefik Reverse proxy, TLS, CORS middleware for MinIO
Next.js Application + REST API (auth, audio upload, dev fallback proxy)
PostgreSQL Business data (languages, cards, items, speakers, admins)
MinIO Audio storage — audios bucket public for reading

Direct browser → MinIO access for audio: zero load on Next.js during playback, stable URLs compatible with the Service Worker.


Prerequisites

Component Minimum version
Docker ≥ 24
Docker Compose ≥ 2.20
Traefik Deployed on the external Docker network
GitLab Runner With Docker access (for CI/CD)

Environment variables

Build-time variables (NEXT_PUBLIC_*)

Embedded into the bundle during docker build. A value change requires a rebuild.

Variable Description Example
NEXT_PUBLIC_APP_URL Public app URL (confirmation emails) https://racines.kiyara.net
NEXT_PUBLIC_PWA_ENABLED Enable the Service Worker true (prod), false (dev)

AUDIO_CDN_URL is no longer a build-arg — it is a runtime variable injected at container startup.

Runtime variables (container)

Passed via env_file in docker-compose.yml. No rebuild required.

Audio CDN (critical):

Variable Description Example value
AUDIO_CDN_URL MinIO public URL (includes the bucket) — mandatory https://racines-s3.id2real.net/audios
AUDIO_PROXY_DISABLED Disable the fallback proxy true (prod)
AUDIO_CDN_DOMAIN Hostname for CSP — auto-derived if absent racines-s3.id2real.net

⚠️ The Docker entrypoint (docker/entrypoint.sh) refuses to start if AUDIO_CDN_URL is empty.

PostgreSQL:

Variable Example value
PG_HOST postgres
PG_PORT 5432
PG_DATABASE racines_db
PG_USER racines
PG_PASSWORD [secret]
PG_SSL false

MinIO (server access for upload):

Variable Description
MINIO_ENDPOINT Internal URL (Node.js → MinIO): http://minio:9000
MINIO_ACCESS_KEY Application access key
MINIO_SECRET_KEY Application secret key
MINIO_BUCKET Bucket name: audios
MINIO_PUBLIC_URL Server-side public URL (building audio_url values)

Application:

Variable Description
JWT_SECRET JWT secret (admin/speaker sessions)
NODE_ENV production

SMTP (contact form):

Variable Description
SMTP_HOST SMTP server
SMTP_PORT SMTP port (587 STARTTLS, 465 SSL)
SMTP_SECURE true for port 465, otherwise false
SMTP_USER SMTP user
SMTP_PASS SMTP password
SMTP_FROM_EMAIL Sender address
ADMIN_EMAIL_1/2/3 Recipients for contact messages

.app_env file format

# All values in double quotes (mandatory for values with #, =, spaces)
AUDIO_CDN_URL="https://racines-s3.id2real.net/audios"
JWT_SECRET="a_long_random_generated_secret"
PG_PASSWORD="postgres_password"
MINIO_SECRET_KEY="minio_secret_key"

GitLab CI/CD pipeline

Stages

push branch
    │
    ├── test
    │     ├── test:quality:sonarqube   (main/develop/qa/test-ci + tags)
    │     └── test:build:docker        (MR only — builder target)
    │
    ├── containerize
    │     ├── package:image:dev        (dev branch)
    │     ├── package:image:staging    (develop/qa/test-ci)
    │     └── package:image:prod       (main + tags)
    │
    └── deploy
          ├── deploy:int:ansible       (develop/test-ci → staging, automatic)
          ├── deploy:dev:ansible       (dev → dev, automatic)
          └── deploy:prod:manual       (main/tags → prod, ⚠️ MANUAL trigger)

A single image per commit — identical for all environments. The MinIO CDN URL is injected at container startup.

Docker images

Job Branch(es) Image tag
package:image:dev dev dev
package:image:staging develop, qa, test-ci develop / qa
package:image:prod main, tags main / v1.2.3

GitLab CI/CD variables

To define in GitLab → Settings → CI/CD → Variables:

Variable Description
DOCKER_REG_URL Docker registry URL
DOCKER_REG_LOGIN Registry login
DOCKER_REG_PASS Registry password
SSHKEY_CI SSH key → staging
SSHKEY_DEV SSH key → dev
DEPLOY_SSHKEY_FILE SSH key → production
SONAR_HOST_URL SonarQube URL (quality analysis)

Production deployment

GitLab → Pipelines → pipeline on main → deploy:prod:manual → ▶ (click Play)

Deployment is manual: no automatic production deployments.


MinIO infrastructure

Bucket and policies

mc alias set minio https://racines-s3.id2real.net ACCESS_KEY SECRET_KEY

# Create bucket with versioning
mc mb minio/audios --ignore-existing
mc version enable minio/audios

# Public read (browser → audio without auth)
mc anonymous set download minio/audios

# Application account (upload/delete, no admin rights)
mc admin user add minio app_racines_rw STRONG_PASSWORD
mc admin policy attach minio readwrite --user app_racines_rw

MinIO CORS via Traefik

MinIO AGPL does not support CORS configuration via the S3 API. CORS is handled by a Traefik middleware:

labels:
  - traefik.http.routers.racines-minio-api.middlewares=minio-cors
  - traefik.http.middlewares.minio-cors.headers.accesscontrolalloworiginlist=https://racines.kiyara.net,https://staging.racinesbykiyara.net
  - traefik.http.middlewares.minio-cors.headers.accesscontrolallowmethods=GET,HEAD,OPTIONS
  - traefik.http.middlewares.minio-cors.headers.accesscontrolallowheaders=*
  - traefik.http.middlewares.minio-cors.headers.accesscontrolmaxage=86400

Add the domain of each new environment to accesscontrolalloworiginlist.

MinIO healthcheck

healthcheck:
  test: ["CMD", "mc", "ready", "local"]
  interval: 30s
  timeout: 20s
  retries: 3

⚠️ Use mc ready local — not mc admin info (requires admin rights) nor curl (absent from the image).


Dockerfile (multi-stage)

# Stage 1: deps — Node.js dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: builder — Next.js build
FROM node:20-alpine AS builder
ARG NEXT_PUBLIC_PWA_ENABLED=true
ARG NEXT_PUBLIC_APP_URL
ENV NEXT_PUBLIC_PWA_ENABLED=$NEXT_PUBLIC_PWA_ENABLED
ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL
WORKDIR /app
COPY . .
RUN npm ci && NODE_ENV=production npm run build

# Remove SW if PWA is disabled
RUN if [ "$NEXT_PUBLIC_PWA_ENABLED" != "true" ]; then \
      rm -f public/sw*.js public/register-sw*; \
    fi

# Stage 3: runner — final image (standalone)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
COPY docker/entrypoint.sh ./entrypoint.sh
RUN chmod +x entrypoint.sh
EXPOSE 3000
ENTRYPOINT ["./entrypoint.sh"]

Production build:

docker build \
  --build-arg NEXT_PUBLIC_PWA_ENABLED=true \
  --build-arg NEXT_PUBLIC_APP_URL=https://racines.kiyara.net \
  -t registry.id2real.net/racines-app:main .

First deployment

1. Prepare the server

mkdir -p /docker-apps/racines
cd /docker-apps/racines

# Copy the environment's docker-compose file (provided by Ansible)
cp ansible/docker-compose.integration.yaml docker-compose.yml

# Create the variables file
cp .env.example .app_env
nano .app_env  # Fill in all values (double quotes mandatory)

2. Configure MinIO

# Start MinIO only
docker compose up -d minio

# Configure the bucket
docker exec -it racines-minio mc alias set local http://localhost:9000 root root_password
docker exec -it racines-minio mc mb local/audios --ignore-existing
docker exec -it racines-minio mc version enable local/audios
docker exec -it racines-minio mc anonymous set download local/audios

3. Initialize the database

Migrations are in the migrations/ folder:

# Start PostgreSQL only
docker compose up -d postgres

# Apply migrations
docker exec -i racines-postgres psql -U racines -d racines_db \
  < migrations/001_initial_schema.sql

docker exec -i racines-postgres psql -U racines -d racines_db \
  < migrations/002_create_admins.sql

docker exec -i racines-postgres psql -U racines -d racines_db \
  < migrations/004_quiz_results.sql

003_idb_card_id_index_note.sql is a documentation note — do not execute it.

4. Create the first admin account

# On the server, once the app container is running
docker exec -it racines-app npm run create-admin
# → Enter email and password

Or from a development machine with database access:

npm run create-admin

5. Launch the application

docker compose pull
docker compose up -d
docker compose logs -f app

6. Verify startup

# Next.js liveness probe
curl https://racines.kiyara.net/api/health/live
# → {"status":"ok"}

# Direct MinIO audio access
curl -I https://racines-s3.id2real.net/audios/pul/item-uuid/audio.mp3
# → HTTP 200, Content-Type: audio/mpeg

# Audio CORS
curl -I -H "Origin: https://racines.kiyara.net" \
  https://racines-s3.id2real.net/audios/pul/item-uuid/audio.mp3
# → Access-Control-Allow-Origin: https://racines.kiyara.net

Updates

Via CI/CD pipeline (normal method)

git push origin develop   # → build + automatic staging deploy
git push origin main      # → prod build, then MANUAL deploy

Manual update on the server

cd /docker-apps/racines
docker compose pull app
docker compose up -d --force-recreate app

After an environment variable change only

# Edit .app_env on the server, then:
docker compose up -d --force-recreate app
# No rebuild needed — AUDIO_CDN_URL is read at container startup

PWA and deployments

Impact of a deployment on offline users

  • Users with a downloaded language continue using it offline (old version in cache).
  • On the next online load, the new SW installs → invalidates old caches (racines-cache-vXXracines-cache-vXX+1).
  • Re-downloading the language updates the offline content.

If AUDIO_CDN_URL changes between deployments

Previously cached audio (keys = absolute URLs) becomes orphaned. Users must re-download the language to have offline audio with the new URLs.


Post-deployment checklist

[ ] GET /api/health/live          → {"status":"ok"}
[ ] Admin interface accessible    → /admin → login page
[ ] Admin login works             → valid credentials accepted
[ ] Language list                 → /api/languages → valid JSON
[ ] Direct MinIO audio            → curl -I [AUDIO_CDN_URL]/pul/uuid/audio.mp3 → 200
[ ] MinIO CORS                    → Access-Control-Allow-Origin present
[ ] No CSP violations             → DevTools → Console, no CSP errors
[ ] Audio playback on a card      → 🔊 button works
[ ] PWA installable               → install icon visible in the browser
[ ] Offline download              → download a language, airplane mode, navigate
[ ] Contact form                  → submission → email received + success toast
[ ] Statistics tracked            → actions in the app → admin dashboard updated

Troubleshooting

Container refuses to start: AUDIO_CDN_URL must be set

Cause: Variable missing from docker-compose.yml or .app_env.
Fix: Add AUDIO_CDN_URL="https://[CDN]/audios" and restart. No rebuild needed.

Audio blocked by CSP

Symptom: Refused to connect to 'https://s3.example.com/...' because it violates CSP
Cause: AUDIO_CDN_DOMAIN is incorrect or missing (auto-derivation failed).
Fix: Explicitly set AUDIO_CDN_DOMAIN="s3.example.com" and restart.

CORS error on audio

Symptom: Access-Control-Allow-Origin missing from MinIO response headers.
Cause: App domain missing from the Traefik minio-cors middleware.
Fix: Add the domain to accesscontrolalloworiginlist in Traefik labels.

MinIO healthcheck unhealthy: Access Denied

Cause: mc admin info command used (requires admin rights).
Fix: Use mc ready local in the healthcheck.

SW not updated after deployment

Symptom: User sees the old version.
Fix: Hard refresh (Ctrl+Shift+R) or DevTools → Application → Service Workers → Unregister.

Offline page → returns to home instead of the card

Possible cause 1: The page was not pre-cached (download interrupted).
Possible cause 2: The SW is not yet the controller (first install).
Fix: Reload online to activate the new SW, then re-download the language.


Next steps