A maintained, open-source container image of the Moodle™ LMS software: the official Moodle code, unmodified, with Apache and PHP set up the way Moodle recommends.
php2shell is fixed as of 21 October 2026. The PHP vulnerabilities CVE-2026-109618 and CVE-2026-109619 (remote code execution without logging in) are fixed in
4.5.15-r17, with PHP 8.3.36, and in5.2.4-r17and5.3.0-r17, with PHP 8.4.27. The branch tags (4.5,5.2,5.3,latest…) already point to them. Pull the tag and recreate the container:docker compose pull moodle && docker compose up -d moodleIf you pinned an immutable tag (
5.2.4-r16or older), change it to the-r17of your branch.
- Follows Moodle and PHP releases. Every six hours a bot looks for new Moodle releases and for new PHP releases in the official PHP image. When it finds one, the images are rebuilt, tested and published.
- Rebuilt every week, to pick up Debian's fixes.
- Installs and upgrades itself. The first start installs the site. Changing the tag upgrades Moodle, after backing up the database and the code.
- Complete for Moodle 5.x: the URL router is configured, and Moodle's Composer dependencies are installed when the image is built. The official package leaves them out; Moodle's environment check asks for them, and in 5.3 OAuth 2 clients need them.
- Verifiable:
- Every image is scanned with Trivy before it is published.
- Images are signed with cosign, without long-lived keys.
- Each image ships an SBOM and build provenance.
- Multi-arch: amd64 and arm64.
curl -O https://raw.githubusercontent.com/OksigeniaSL/docker-moodle/main/compose.yaml
# edit MOODLE_URL and the passwords, then:
docker compose up -d
docker compose logs -f moodle # first start: a few minutesOpen MOODLE_URL and log in as admin with MOODLE_ADMIN_PASSWORD. If you leave the admin password out, a random one is generated and printed once in the logs.
Bitnami's free Moodle images stopped receiving updates in August 2025. This image accepts their environment variables and volume layout, so changing one line of your Compose file is enough:
moodle:
- image: bitnamilegacy/moodle:5.0.2
+ image: oksigenia/moodle:5.2The image recognises the /bitnami/moodle and /bitnami/moodledata volumes, reads your existing config.php, keeps your add-on plugins, upgrades Moodle and keeps ports 8080 and 8443. Bitnami's variable names keep working, and the logs suggest the new names. The migration is tested in CI against real bitnamilegacy/moodle volumes. Read the migration guide before doing it in production: it covers backups, the database container, which tag to choose and rollback.
| Moodle | Tags | PHP | Security fixes until |
|---|---|---|---|
| 5.3 LTS | 5.3, 5, latest, lts |
8.4 | October 2029 |
| 5.2 | 5.2 |
8.4 | October 2027 |
| 4.5 LTS | 4.5, 4 |
8.3 | October 2027 |
latest and 5 follow the newest branch: since 5 October 2026 they point to 5.3. To stay on a branch, use its own tag, such as 5.2 or 4.5.
- Exact version: each branch also has a tag with the exact Moodle version (e.g.
5.2.3), which moves with every rebuild. - Immutable build:
5.2.3-r1,5.2.3-r2… never move. Pin one of these in production if you want to decide when to update. - Support dates: they are Moodle HQ's. A branch is dropped from the image after Moodle stops releasing security fixes for it.
Every variable also accepts a _FILE variant (e.g. MOODLE_DB_PASSWORD_FILE=/run/secrets/db) for Docker secrets.
| Variable | Default | Notes |
|---|---|---|
MOODLE_URL |
http://localhost:8080 |
The address users open (Moodle's wwwroot). |
MOODLE_DB_TYPE |
mariadb |
mariadb, mysqli (MySQL), pgsql (PostgreSQL) or auroramysql. |
MOODLE_DB_HOST |
db |
|
MOODLE_DB_PORT |
3306 / 5432 |
Depends on the type. |
MOODLE_DB_NAME |
moodle |
|
MOODLE_DB_USER |
moodle |
|
MOODLE_DB_PASSWORD |
||
MOODLE_DB_PREFIX |
mdl_ |
|
MOODLE_REVERSE_PROXY |
no |
Moodle's reverseproxy; rarely needed. |
MOODLE_SSL_PROXY |
auto |
auto turns on Moodle's sslproxy when MOODLE_URL is https and TLS ends at a proxy in front of the container. |
These are applied once, when the site is created, and never again. Change them later in Moodle's administration.
| Variable | Default | Notes |
|---|---|---|
MOODLE_ADMIN_USER |
admin |
|
MOODLE_ADMIN_PASSWORD |
random, printed in the logs | |
MOODLE_ADMIN_EMAIL |
admin@<host of MOODLE_URL> |
|
MOODLE_SITE_NAME |
Moodle |
|
MOODLE_SITE_SHORTNAME |
the site name | |
MOODLE_LANG |
en |
Default language; its pack is downloaded during the install. |
MOODLE_TIMEZONE |
e.g. Europe/Madrid. Also sets PHP's timezone. |
|
MOODLE_NOREPLY_ADDRESS |
Moodle's default | |
MOODLE_SMTP_HOST |
SMTP server; without it Moodle uses PHP mail. | |
MOODLE_SMTP_PORT |
||
MOODLE_SMTP_USER |
||
MOODLE_SMTP_PASSWORD |
||
MOODLE_SMTP_SECURITY |
tls, ssl or empty. |
| Variable | Default | Notes |
|---|---|---|
MOODLE_EXTRA_LANGS |
Extra language packs, e.g. es,ca,eu,gl. Installed when the list changes. |
|
MOODLE_REDIS_HOST / _PORT / _PASSWORD |
Sessions and the application cache in Redis or Valkey. | |
MOODLE_CRON |
on |
off if cron runs in another container. |
MOODLE_CRON_INTERVAL |
60 |
Seconds between cron runs. |
MOODLE_CRON_KEEPALIVE |
interval − 10 | Seconds each run stays alive picking up ad hoc tasks (Moodle's --keep-alive). Runs never overlap. |
MOODLE_CRON_LOG |
errors |
errors prints cron output only when it fails; full prints every run. The last run is always in moodledata/oksigenia/cron-last.log. |
MOODLE_AUTO_UPGRADE |
on |
off refuses to start when an upgrade is needed, so you can run it yourself. |
MOODLE_BACKUP_BEFORE_UPGRADE |
on |
Database dump (and code archive) before every upgrade. |
MOODLE_BACKUP_KEEP |
2 |
Backups kept in moodledata/oksigenia/backups. The newest one taken before a version change is always kept. |
MOODLE_ALLOW_REMOVED_PLUGINS |
no |
yes lets an upgrade go ahead when it would uninstall plugins Moodle removed from core that the site still uses (Chat or Survey activities, the Classic theme). |
MOODLE_RETRY_UPGRADE |
no |
After a failed upgrade the container stops at every start, without new backups, until you fix the cause and start it once with yes. |
OKSIGENIA_ACCESS |
off |
See Oksigenia Access. |
| Variable | Default |
|---|---|
PHP_MEMORY_LIMIT |
256M |
PHP_UPLOAD_MAX_FILESIZE |
100M |
PHP_POST_MAX_SIZE |
100M |
PHP_MAX_EXECUTION_TIME |
300 |
PHP_MAX_INPUT_VARS |
5000 |
PHP_DATE_TIMEZONE |
UTC |
APACHE_MAX_REQUEST_WORKERS |
50 |
APACHE_HTTP_PORT |
8080 |
APACHE_HTTPS_PORT |
8443 |
The image writes config.php at every start, from the variables above. For anything else, mount PHP files into /etc/moodle/config.d/; they are loaded in alphabetical order:
<?php // /etc/moodle/config.d/50-local.php
$CFG->debug = 0;
$CFG->forced_plugin_settings = ['theme_boost' => ['brandcolor' => '#0f6cbf']];
// Moodle 5.3+: hide Moodle HQ's promotional cards on the admin notifications page.
$CFG->disablenotificationctas = ['marketplace', 'moodlecloud', 'partners', 'feedback'];The image ships the tools most sites need: Ghostscript and Poppler for assignment PDF annotation, and the database clients for backups. Optional ones are left out to keep it small. Extend the image to add them; at every start it sets Moodle's paths for any of latex, dvips, dvisvgm, convert, dot, python3 and unoconv it finds:
FROM oksigenia/moodle:5.2
RUN apt-get update && apt-get install -y --no-install-recommends \
texlive-latex-base texlive-latex-recommended dvisvgm graphviz \
&& rm -rf /var/lib/apt/lists/*Build it from your Compose file with build: . instead of image:. The TeX filter's SVG output uses dvisvgm; its PNG and GIF outputs also need ImageMagick (add imagemagick).
If the build cannot reach the package servers (apt, or GitHub when building this image itself) while the host can, Docker's IPv6 is often broken on that host. Build with docker build --network host, or network: host under build: in Compose.
Two things about the TeX notation filter:
- LaTeX is required from Moodle 5.3. The filter used to fall back to the bundled mimetex when LaTeX was missing or failed; Moodle 5.3 no longer ships mimetex.
- Accented text needs UTF-8. Moodle's default LaTeX preamble starts with
\usepackage[latin1]{inputenc}, and formulas with accented letters inside\text{}fail with it. Change it to\usepackage[utf8]{inputenc}in Site administration → Plugins → Filters → TeX notation.
| Path | Contents |
|---|---|
/var/www/moodle |
Moodle code, including plugins installed from the web. Keep it in a volume, or plugins installed from the web are lost when the container is recreated. |
/var/www/moodledata |
Moodle's data directory: files, caches, backups made by the image. |
- Ports: HTTP on 8080. HTTPS on 8443 when certificates are mounted at
/certs/tls.crtand/certs/tls.key; most setups terminate TLS in a reverse proxy instead. - User: Apache and cron run as
www-data(uid 33).- The container starts as root only to fix the ownership of the volumes, then drops to
www-data. - It also runs fully unprivileged with
user: www-data. In that case the volumes must already belong to uid 33. - If you fix ownership on the host instead, recreate the container afterwards (
docker compose up -d --force-recreate): a plainup -dleaves the failed container retrying.
- The container starts as root only to fix the ownership of the volumes, then drops to
To upgrade, change the tag (for example from 5.2 to 5.3) and recreate the container. On start, the image compares the Moodle version it ships with the one in the code volume, and when the image is newer it does the following:
- Saves a database dump and an archive of the current code in
moodledata/oksigenia/backups/. - Replaces Moodle's core code and keeps your add-on plugins,
config.phpand anything else that is not core. - Runs Moodle's upgrade in maintenance mode, purges the caches and starts the site. A site that was already in maintenance mode stays in it, and cron waits until it is opened.
If the volume holds a newer Moodle than the image, or one Moodle cannot upgrade from directly, the container refuses to start and says why. It also stops before changing anything when an add-on would make Moodle reject the upgrade (for example modules that still declare FEATURE_GROUPMEMBERSONLY, refused from 5.3), or when the upgrade would uninstall plugins removed from core that the site uses (to keep one, put a separate release of it in its place; see Chat and Survey). If an upgrade fails half way, the site stays in maintenance mode and later starts stop straight away, so a restart loop cannot replace the good backup. On any of these problems the container stays up and waits, unhealthy, instead of exiting: a restart policy would only repeat the same error. Fix the cause and restart it. Several containers sharing the same volumes (web replicas, a cron container) take turns: only one upgrades.
Cron runs inside the container every minute, as www-data. To run it separately, for instance with several web replicas, set MOODLE_CRON=off on the web containers and add one container with the same image, volumes and variables, and the command cron:
moodle-cron:
image: oksigenia/moodle:5.2
command: cron
# same environment and volumes as the web containermoodle-cli runs any script under admin/cli as www-data, from the right directory. From Moodle 5.1, admin/cli stays at the root of the code, not under public/. Avoid docker exec … php admin/cli/… without -u www-data: run as root, Moodle leaves root-owned cache files that Apache cannot replace (a restart of the container hands them back to www-data).
docker compose exec moodle moodle-cli maintenance --enable
docker compose exec moodle moodle-cli cfg --name=theme --set=boost
docker compose exec moodle moodle-cli admin/tool/task/cli/schedule_task.php --listScripts in /docker-entrypoint-init.d/ (.sh and .php) run once, after the first install.
The image includes Oksigenia Access, an accessibility panel for Moodle. It covers 17 controls (text size, contrast, dyslexia-friendly font, reading guide…), 4 one-click profiles and 8 languages, and signed-in users find their settings on any device. It is listed in the Moodle Marketplace with the Privacy friendly and Automated testing support awards. It is not installed unless you ask for it:
environment:
OKSIGENIA_ACCESS: "on"- Turning it on: with
on, the plugin is installed and enabled, and the image keeps it updated. - Turning it off: setting
offlater disables it without removing its settings. - Settings: in Site administration → Plugins → Local plugins → Accessibility panel (Oksigenia Access).
-
Signatures: images are signed with cosign, with the identity of this repository's release workflow. To verify an image:
cosign verify oksigenia/moodle:5.2 \ --certificate-identity-regexp '^https://github.com/OksigeniaSL/docker-moodle/\.github/workflows/release\.yml@' \ --certificate-oidc-issuer https://token.actions.githubusercontent.comThis needs cosign 3, or cosign 2.6 with
--new-bundle-format. Older versions reportno signatures found. -
SBOM and provenance: each image carries an SBOM and SLSA provenance attestations (
docker buildx imagetools inspect oksigenia/moodle:5.2 --format '{{json .SBOM}}'), and a GitHub build attestation (gh attestation verify oci://ghcr.io/oksigeniasl/moodle:5.2 -R OksigeniaSL/docker-moodle, with GitHub CLI 2.49 or later). -
Scanning: a build with a critical vulnerability that has a fix available is not published.
-
Plugins from the web: Moodle lets site administrators install plugins from a ZIP file, and a plugin is PHP code that runs in the container. To turn that off, put
$CFG->disableupdateautodeploy = true;in a file under/etc/moodle/config.d/(see Your own Moodle settings). Plugins are then added to the code volume by whoever runs the server. -
Reporting: report vulnerabilities in the image privately through GitHub security advisories. Vulnerabilities in Moodle itself go to Moodle's security process.
The same images are published on Docker Hub as oksigenia/moodle and on GitHub as ghcr.io/oksigeniasl/moodle.
The team at Oksigenia has worked with Moodle since version 1.7 and maintains this image. Prefer not to maintain it yourself? Oksigenia can host and maintain your Moodle site for you.
Managed hosting for Moodle™ and Bitnami rescue, for schools: oksigenia.com/en/services/moodle (Bitnami rescue).
The image's source (this repository) is licensed under the GNU GPL v3.0. Moodle is distributed under the GNU GPL v3 or later.
The word Moodle and associated Moodle logos are trademarks or registered trademarks of Moodle Pty Ltd or its related affiliates. This project is not affiliated with or endorsed by Moodle HQ.