Deployment¶
This page gives administrators a fast, copy-pasteable path to run eXeLearning in production with Docker. It embeds the official Docker Compose files, shows required environment variables, and highlights the few things you must secure.
Images & Architectures¶
We build and publish multi-architecture images for amd64 and arm64. Images are pushed to two registries to avoid potential access issues or rate limiting:
- docker.io/exelearning/exelearning
- ghcr.io/exelearning/exelearning
Choose your database¶
| Engine | Best for | File (embedded below) | 
|---|---|---|
| SQLite | Single user / proof-of-concept | deploy/docker-compose.sqlite.yml | 
| MariaDB | Most teams / general workloads | deploy/docker-compose.mariadb.yml | 
| Postgres | Larger teams / high concurrency | deploy/docker-compose.postgres.yml | 
Rule of thumb: SQLite for demos, MariaDB for most deployments, Postgres for heavier load.
1) SQLite (simplest)¶
Here is the exact Compose file used by releases:
# SQLite configuration for eXeLearning
# Use with: docker compose -f docker-compose.sqlite.yml up -d
# This is a minimal configuration as SQLite doesn't require a separate database service
services:
  exelearning:
    image: ghcr.io/exelearning/exelearning:${TAG:-latest}
    build: ../../    
    ports:
      - "${APP_PORT:-8080}:8080"
    restart: unless-stopped
    volumes:
      - exelearning-data:/mnt/data:rw
    environment:
      # Symfony settings
      APP_ENV: prod
      APP_DEBUG: 0
      APP_SECRET: "${APP_SECRET:-ChangeThisToASecretForSQLiteDeployment}"
      APP_PORT: "${APP_PORT:-8080}"
      APP_ONLINE_MODE: 1
      XDEBUG_MODE: off
      # Database settings
      DB_DRIVER: pdo_sqlite
      DB_PATH: /mnt/data/exelearning.db
      DB_SERVER_VERSION: 3.32
      # Files directory
      FILES_DIR: "/mnt/data/"
      # Authentication settings
      APP_AUTH_METHODS: password
      AUTH_CREATE_USERS: true
      # Test users
      TEST_USER_EMAIL: "${TEST_USER_EMAIL:-user@exelearning.net}"
      TEST_USER_USERNAME: "${TEST_USER_USERNAME:-user}"
      TEST_USER_PASSWORD: "${TEST_USER_PASSWORD:-1234}"
      # Mercure settings
      MERCURE_URL: "http://localhost:8080/.well-known/mercure"
      MERCURE_PUBLIC_URL:
      MERCURE_JWT_SECRET_KEY: "${MERCURE_JWT_SECRET_KEY:-!ChangeThisMercureHubJWTSecretKey!}"
      MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_JWT_SECRET_KEY:-!ChangeThisMercureHubJWTSecretKey!}"
      MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_JWT_SECRET_KEY:-!ChangeThisMercureHubJWTSecretKey!}"
      API_JWT_SECRET: "${API_JWT_SECRET:-ChangeThisToASecretForSQLiteDeployment}"
      # Post-configuration commands
      POST_CONFIGURE_COMMANDS: |
        echo "Setting up eXeLearning with SQLite..."
        php bin/console app:create-user ${TEST_USER_EMAIL:-user@exelearning.net} ${TEST_USER_PASSWORD:-1234} ${TEST_USER_USERNAME:-user} --no-fail
volumes:
  exelearning-data:
Run it:
docker compose -f docker-compose.sqlite.yml up -d
Access the app at http://localhost:8080. Change APP_PORT if needed.  
2) MariaDB¶
# MariaDB configuration for eXeLearning
# Use with: docker compose -f docker-compose.mariadb.yml up -d
services:
  exelearning:
    image: ghcr.io/exelearning/exelearning:${TAG:-latest}
    build: ../../    
    ports:
      - "${APP_PORT:-8080}:8080"
    restart: unless-stopped
    volumes:
      - exelearning-data:/mnt/data:rw
    environment:
      # Symfony settings
      APP_ENV: prod
      APP_DEBUG: 0
      APP_SECRET: "${APP_SECRET:-ChangeThisToASecretForMariaDBDeployment}"
      APP_PORT: "${APP_PORT:-8080}"
      APP_ONLINE_MODE: 1
      XDEBUG_MODE: off
      # Database settings
      DB_DRIVER: pdo_mysql
      DB_HOST: mariadb
      DB_PORT: 3306
      DB_NAME: "${DB_NAME:-exelearning}"
      DB_USER: "${DB_USER:-exelearning}"
      DB_PASSWORD: "${DB_PASSWORD:-exelearning}"
      DB_CHARSET: "${DB_CHARSET:-utf8mb4}"
      DB_SERVER_VERSION: 10.6
      # Files directory
      FILES_DIR: "/mnt/data/"
      # Authentication settings
      APP_AUTH_METHODS: password
      AUTH_CREATE_USERS: true
      # Test users
      TEST_USER_EMAIL: "${TEST_USER_EMAIL:-user@exelearning.net}"
      TEST_USER_USERNAME: "${TEST_USER_USERNAME:-user}"
      TEST_USER_PASSWORD: "${TEST_USER_PASSWORD:-1234}"
      # Mercure settings
      MERCURE_URL: "http://localhost:8080/.well-known/mercure"
      MERCURE_PUBLIC_URL:
      MERCURE_JWT_SECRET_KEY: "${MERCURE_JWT_SECRET_KEY:-!ChangeThisMercureHubJWTSecretKey!}"
      MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_JWT_SECRET_KEY:-!ChangeThisMercureHubJWTSecretKey!}"
      MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_JWT_SECRET_KEY:-!ChangeThisMercureHubJWTSecretKey!}"
      API_JWT_SECRET: "${API_JWT_SECRET:-ChangeThisToASecretForMariaDBDeployment}"
      # Post-configuration commands
      POST_CONFIGURE_COMMANDS: |
        echo "Setting up eXeLearning with MariaDB..."
        php bin/console app:create-user ${TEST_USER_EMAIL:-user@exelearning.net} ${TEST_USER_PASSWORD:-1234} ${TEST_USER_USERNAME:-user} --no-fail
    depends_on:
      - mariadb
  mariadb:
    image: mariadb:12.0
    restart: unless-stopped
    environment:
      MARIADB_ROOT_PASSWORD: "${MARIADB_ROOT_PASSWORD:-root}"
      MARIADB_DATABASE: "${DB_NAME:-exelearning}"
      MARIADB_USER: "${DB_USER:-exelearning}"
      MARIADB_PASSWORD: "${DB_PASSWORD:-exelearning}"
    ports:
      - "${DB_PORT:-3306}:3306"
    volumes:
      - mariadb-data:/var/lib/mysql
volumes:
  exelearning-data:
  mariadb-data:
Run it:
docker compose -f docker-compose.mariadb.yml up -d
Note: Default DB credentials in the file are for quick starts. Override them in a
.env(see Configuration).
3) PostgreSQL¶
# PostgreSQL configuration for eXeLearning
# Use with: docker compose -f docker-compose.postgres.yml up -d
services:
  exelearning:
    image: ghcr.io/exelearning/exelearning:${TAG:-latest}
    build: ../../
    ports:
      - "${APP_PORT:-8080}:8080"
    restart: unless-stopped
    volumes:
      - exelearning-data:/mnt/data:rw
    environment:
      # Symfony settings
      APP_ENV: prod
      APP_DEBUG: 0
      APP_SECRET: "${APP_SECRET:-ChangeThisToASecretForPostgresDeployment}"
      APP_PORT: "${APP_PORT:-8080}"
      APP_ONLINE_MODE: 1
      XDEBUG_MODE: off
      # Database settings
      DB_DRIVER: pdo_pgsql
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: "${DB_NAME:-exelearning}"
      DB_USER: "${DB_USER:-postgres}"
      DB_PASSWORD: "${DB_PASSWORD:-postgres}"
      DB_CHARSET: "${DB_CHARSET:-utf8}"
      DB_SERVER_VERSION: 17
      # Files directory
      FILES_DIR: "/mnt/data/"
      # Authentication settings
      APP_AUTH_METHODS: password
      AUTH_CREATE_USERS: true
      # Test user
      TEST_USER_EMAIL: "${TEST_USER_EMAIL:-user@exelearning.net}"
      TEST_USER_USERNAME: "${TEST_USER_USERNAME:-user}"
      TEST_USER_PASSWORD: "${TEST_USER_PASSWORD:-1234}"
      # Mercure settings
      MERCURE_URL: "http://localhost:8080/.well-known/mercure"
      MERCURE_PUBLIC_URL:
      MERCURE_JWT_SECRET_KEY: "${MERCURE_JWT_SECRET_KEY:-!ChangeThisMercureHubJWTSecretKey!}"
      MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_JWT_SECRET_KEY:-!ChangeThisMercureHubJWTSecretKey!}"
      MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_JWT_SECRET_KEY:-!ChangeThisMercureHubJWTSecretKey!}"
      API_JWT_SECRET: "${API_JWT_SECRET:-ChangeThisToASecretForPostgresDeployment}"
      # Post-configuration commands
      POST_CONFIGURE_COMMANDS: |
        echo "Setting up eXeLearning with PostgreSQL..."
        php bin/console app:create-user ${TEST_USER_EMAIL:-user@exelearning.net} ${TEST_USER_PASSWORD:-1234} ${TEST_USER_USERNAME:-user} --no-fail
    depends_on:
      - postgres
  postgres:
    image: postgres:18-alpine
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: "${DB_PASSWORD:-postgres}"
      POSTGRES_USER: "${DB_USER:-postgres}"
      POSTGRES_DB: "${DB_NAME:-exelearning}"
    ports:
      - "${DB_PORT:-5432}:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
volumes:
  exelearning-data:
  postgres-data:
Run it:
docker compose -f docker-compose.postgres.yml up -d
Heads-up: The sample sets
DB_SERVER_VERSIONand pins a Postgres image tag. Keep these aligned when you customize.
Configuration¶
You can configure the app either:
- With a .envliving next to yourdocker-compose.yml, or
- Inline in Compose using ${VARIABLE:-default}.
Common knobs (all supported by the example files):
- Application: APP_ENV,APP_DEBUG,APP_SECRET,APP_PORT,APP_ONLINE_MODE
- Base path (subdirectory installs): BASE_PATH
- Database: DB_DRIVER,DB_HOST,DB_PORT,DB_NAME,DB_USER,DB_PASSWORD,DB_CHARSET, engine-specific version flags
- Files: FILES_DIR(default:/mnt/data/)
- Auth: APP_AUTH_METHODS,AUTH_CREATE_USERS, plus optional test user (TEST_USER_*)
- Real-time (Mercure): MERCURE_URL,MERCURE_*_JWT_*
- Post-configure hooks: POST_CONFIGURE_COMMANDS(e.g., auto-create a user)
(See the embedded Compose files for the full set.)
Important: Always set strong secrets (
APP_SECRET,MERCURE_JWT_SECRET_KEY, DB passwords) via.envor environment overrides—never commit them.
Subdirectory deployment (BASE_PATH)¶
You can deploy eXeLearning under a subdirectory (e.g., https://example.org/exelearning) by setting BASE_PATH.
- Do not include a trailing slash.
- Can be multi-level.
Examples:
# Install at root
BASE_PATH=
# One level
BASE_PATH=/exelearning
# Multi-level
BASE_PATH=/web/exelearning
What it does:
- Prefixes all application routes with BASE_PATH(e.g.,/exelearning/workarea).
- Keeps /healthcheckworking: requests to/healthcheckare redirected to%BASE_PATH%/healthcheckwhenBASE_PATHis set.
- Inside the container, Nginx rewrites ^$BASE_PATH/(.*)$to/$1so the app sees clean paths. The rewrite is generated automatically fromsubdir.conf.templateby02-configure-symfony.shwhenBASE_PATHis set.
Verification:
- Visit https://your-host%BASE_PATH%/healthcheckand expect{ "status": "ok" }.
- If you hit /healthcheckwithout the prefix whileBASE_PATHis set, you will be redirected to%BASE_PATH%/healthcheck.
Reverse proxy & TLS¶
Put eXeLearning behind Nginx or Traefik to terminate TLS and forward to the app.
server {
    listen 80;
    server_name exelearning.example.org;
    return 301 https://$host$request_uri;
}
server {
    listen 443 ssl http2;
    server_name exelearning.example.org;
    ssl_certificate     /etc/letsencrypt/live/exelearning.example.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/exelearning.example.org/privkey.pem;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $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 $scheme;
    }
    # Mercure (SSE) must disable buffering
    location ^~ /.well-known/mercure {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off; # critical for SSE
        proxy_set_header Host $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 $scheme;
    }
}
If TLS is terminated at your proxy: set
USE_FORWARDED_HEADERS=1and ensureX-Forwarded-*headers are sent.
Data & backups¶
- Volumes: Each Compose file declares named volumes for app data and databases.
- Backups: Snapshot volumes regularly (mariadb-data,postgres-data,exelearning-data) and any external storage used byFILES_DIR.
- DB tools: mysqldump/pg_dumpfor live exports; for SQLite, copy the DB file when the service is stopped.
Troubleshooting¶
- Port already in use: Change APP_PORTin your.envor Compose overrides.
- File permissions: Ensure your volumes are writable by the container user.
- Real-time/SSE stalls: In Nginx, set proxy_buffering offfor/.well-known/mercure. See Reverse proxy & TLS above.
See also¶
- Real-time configuration: development/real-time.md
Maintenance¶
Temporary files cleanup¶
eXeLearning stores intermediate/temporary files (exports, conversions, etc.) under the configured temporary directory. You can clean up old entries either via a console command (recommended for cron) or an HTTP endpoint (for environments where only HTTP access is available).
- Command (recommended):
- php bin/console app:tmp-files:cleanup [--max-age=SECONDS]
- Composer shortcut: composer tmp-cleanup
- 
Example cron (daily at 03:00, keeping 24h): - 0 3 * * * cd /opt/exelearning && php bin/console app:tmp-files:cleanup --max-age=86400
 
- 
HTTP endpoint (GET or POST): 
- Path: /maintenance/tmp/cleanup
- Query/body parameter: key
- Example:- curl -fsS "https://exelearning.example.org/maintenance/tmp/cleanup?key=$TMP_CLEANUP_KEY"
 
- 
Response: 200 OKwith a JSON summary;207 Multi-Statuswhen some deletions fail.
- 
Security and configuration: 
- Set TMP_CLEANUP_KEYin your environment (also present in.env.dist). The endpoint validates?key=...against this value.
- If TMP_CLEANUP_KEYis empty or unset, the endpoint is inert and returns204 No Contentwithout performing any action (silent exit).
- Expose the endpoint only over HTTPS and/or restrict by IP as needed.