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/exelearningghcr.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.1
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.
Custom templates¶
eXeLearning supports project templates that users can select via File → New from Template. Templates are .elpx files organized by language code under public/templates/<lang>/.
Mounting custom templates¶
To provide custom templates in a Docker deployment, mount a volume to the templates directory:
services:
exelearning:
volumes:
# Mount custom templates for all languages
- ./my-templates:/app/public/templates
# Or mount templates for a specific language
- ./my-en-templates:/app/public/templates/en
- ./my-es-templates:/app/public/templates/es
Template structure¶
Templates are organized by language code:
templates/
├── en/ # English templates
│ ├── basic.elpx
│ └── course.elpx
├── es/ # Spanish templates
│ ├── basic.elpx
│ └── course.elpx
└── ...
The filename (without .elpx extension) is displayed as the template name in the UI. You can use spaces and special characters in filenames (e.g., My Course Template.elpx).
Creating templates¶
- Design your project in eXeLearning
- Export it as an
.elpxfile (File → Download as... → eXeLearning content) - Place it in the appropriate language folder in your templates directory
- Templates will automatically appear in the File → New from Template menu
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.