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:
# Application settings
APP_ENV: prod
APP_DEBUG: 0
APP_SECRET: "${APP_SECRET:-ChangeThisToASecretForSQLiteDeployment}"
APP_ONLINE_MODE: 1
ONLINE_THEMES_INSTALL: ${ONLINE_THEMES_INSTALL:-0}
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 (guest enabled for E2E tests)
APP_AUTH_METHODS: password,guest
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}"
API_JWT_SECRET: "${API_JWT_SECRET:-ChangeThisToASecretForSQLiteDeployment}"
# Post-configuration commands
POST_CONFIGURE_COMMANDS: |
echo "Setting up eXeLearning with SQLite..."
bun cli create-user --email ${TEST_USER_EMAIL:-user@exelearning.net} --password ${TEST_USER_PASSWORD:-1234} --username ${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:
# Application settings
APP_ENV: prod
APP_DEBUG: 0
APP_SECRET: "${APP_SECRET:-ChangeThisToASecretForMariaDBDeployment}"
APP_ONLINE_MODE: 1
ONLINE_THEMES_INSTALL: ${ONLINE_THEMES_INSTALL:-0}
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 (guest enabled for E2E tests)
APP_AUTH_METHODS: password,guest
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}"
API_JWT_SECRET: "${API_JWT_SECRET:-ChangeThisToASecretForMariaDBDeployment}"
# Post-configuration commands
POST_CONFIGURE_COMMANDS: |
echo "Setting up eXeLearning with MariaDB..."
bun cli create-user --email ${TEST_USER_EMAIL:-user@exelearning.net} --password ${TEST_USER_PASSWORD:-1234} --username ${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:
# Application settings
APP_ENV: prod
APP_DEBUG: 0
APP_SECRET: "${APP_SECRET:-ChangeThisToASecretForPostgresDeployment}"
APP_ONLINE_MODE: 1
ONLINE_THEMES_INSTALL: ${ONLINE_THEMES_INSTALL:-0}
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 (guest enabled for E2E tests)
APP_AUTH_METHODS: password,guest
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}"
API_JWT_SECRET: "${API_JWT_SECRET:-ChangeThisToASecretForPostgresDeployment}"
# Post-configuration commands
POST_CONFIGURE_COMMANDS: |
echo "Setting up eXeLearning with PostgreSQL..."
bun cli create-user --email ${TEST_USER_EMAIL:-user@exelearning.net} --password ${TEST_USER_PASSWORD:-1234} --username ${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 (Yjs WebSocket): Uses the main server port, no additional configuration needed
- 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, 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 from the container configuration whenBASE_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;
}
# WebSocket for Yjs collaboration
location /yjs/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
proxy_read_timeout 86400; # Keep WebSocket alive
}
}
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 managed through the Admin Panel under the Extensions tab.
Adding templates via Admin Panel¶
- Log in as an administrator
- Navigate to Admin Panel → Extensions → Templates
- Select the target language from the dropdown
- Click Upload Template and select an
.elpxfile - Provide a display name and optional description
- The template will be available to users with that language setting
Template storage¶
Templates uploaded through the admin panel are stored in FILES_DIR/admin/templates/<locale>/ and their metadata is stored in the database.
Enabling/Disabling templates¶
Administrators can enable or disable templates through the admin panel. Disabled templates won't appear in the "New from Template" menu for users.
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/WebSocket issues: Ensure your reverse proxy supports WebSocket upgrade headers. See Reverse proxy & TLS above.
High Availability¶
For deployments requiring horizontal scaling and high availability with multiple server instances, see:
- High Availability Guide - Multi-instance deployment with Redis synchronization
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):
bun cli tmp-cleanup [--max-age=SECONDS]-
Example cron (daily at 03:00, keeping 24h):
0 3 * * * cd /opt/exelearning && bun cli tmp-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.