Skip to content

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:

deploy/docker-compose.sqlite.yml
# 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

deploy/docker-compose.mariadb.yml
# 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

deploy/docker-compose.postgres.yml
# 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_VERSION and pins a Postgres image tag. Keep these aligned when you customize.


Configuration

You can configure the app either:

  1. With a .env living next to your docker-compose.yml, or
  2. 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 .env or 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 /healthcheck working: requests to /healthcheck are redirected to %BASE_PATH%/healthcheck when BASE_PATH is set.
  • Inside the container, Nginx rewrites ^$BASE_PATH/(.*)$ to /$1 so the app sees clean paths. The rewrite is generated automatically from the container configuration when BASE_PATH is set.

Verification:

  • Visit https://your-host%BASE_PATH%/healthcheck and expect { "status": "ok" }.
  • If you hit /healthcheck without the prefix while BASE_PATH is 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.

Example: Nginx reverse proxy with TLS
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=1 and ensure X-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 by FILES_DIR.
  • DB tools: mysqldump / pg_dump for 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

  1. Log in as an administrator
  2. Navigate to Admin Panel → Extensions → Templates
  3. Select the target language from the dropdown
  4. Click Upload Template and select an .elpx file
  5. Provide a display name and optional description
  6. 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

  1. Design your project in eXeLearning
  2. Export it as an .elpx file (File → Download as... → eXeLearning content)
  3. Place it in the appropriate language folder in your templates directory
  4. Templates will automatically appear in the File → New from Template menu

Troubleshooting

  • Port already in use: Change APP_PORT in your .env or 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:


See also


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 OK with a JSON summary; 207 Multi-Status when some deletions fail.

  • Security and configuration:

  • Set TMP_CLEANUP_KEY in your environment (also present in .env.dist). The endpoint validates ?key=... against this value.
  • If TMP_CLEANUP_KEY is empty or unset, the endpoint is inert and returns 204 No Content without performing any action (silent exit).
  • Expose the endpoint only over HTTPS and/or restrict by IP as needed.