Skip to content

Troubleshooting

The most common problems users run into, mined from GitHub Issues. Each entry links to the original report so you can dig deeper.

CSS is missing / site looks broken

Symptoms: Moodle renders with plain HTML, no styling, images fail to load. Browser console shows mixed-content blocks or 404s on /theme/yui_combo.php etc.

Cause: $CFG->wwwroot does not match the URL the browser uses. This is almost always a reverse-proxy / SITE_URL mismatch. Related: #21, #101.

Fix:

environment:
  SITE_URL: https://moodle.example.com   # the PUBLIC URL
  SSLPROXY: "true"                       # proxy terminates TLS
  REVERSEPROXY: "false"

Then restart the container and purge caches:

docker compose restart moodle
docker compose exec moodle php admin/cli/purge_caches.php

ERR_TOO_MANY_REDIRECTS

Cause: Moodle thinks the connection is HTTP while the browser uses HTTPS, so it keeps redirecting to its canonical wwwroot. Related: #15.

Fix:

  • Set SSLPROXY=true.
  • Make sure your proxy forwards X-Forwarded-Proto: https.
  • Set SITE_URL to the HTTPS URL.

"Reverse proxy enabled so the server cannot be accessed directly"

Cause: REVERSEPROXY=true on a single-URL deployment. Moodle refuses direct access when it expects multiple base URLs. Related: #137.

Fix: set REVERSEPROXY=false. Keep SSLPROXY=true.

502 Bad Gateway behind Traefik

Cause: Traefik is pointing at the wrong port. The container listens on 8080, not 80. Related: #61.

Fix:

labels:
  - "traefik.http.services.moodle.loadbalancer.server.port=8080"

"Real client IPs" are all the Docker gateway

Cause: The proxy is not forwarding X-Forwarded-For, or a middle hop is stripping it. Related: #11, #137.

Fix: Make the outermost proxy set X-Forwarded-For to the real client IP. Each downstream hop must append (not replace) it. Confirm by running docker compose exec moodle tail -f /var/log/nginx/access.log and watching the IP in the request line.

Cloudflare users can use CF-Connecting-IP instead.

Allowed memory size exhausted when installing a plugin with Moosh

Cause: Default memory_limit (256M) is too low for some plugins. Related: #119.

Fix:

environment:
  memory_limit: 512M

Restart the container. Then re-run the Moosh command — don't forget the --delete flag to work around the known Moosh install bug:

docker compose exec moodle moosh plugin-install --delete theme_almondb

pluglist.php ... HTTP/1.1 403 Forbidden

Cause: Transient issue with download.moodle.org hitting Moosh's plugin list endpoint. Related: #95.

Fix:

docker compose exec moodle rm -f /tmp/.moosh/plugins.json
docker compose exec moodle moosh plugin-list

Retry after a few minutes if it keeps 403'ing.

PostgreSQL connection refused on a custom port

Cause: DB_PORT is not being passed through. Make sure it is a string (quoted) in YAML. Related: #78.

Fix:

environment:
  DB_TYPE: pgsql
  DB_HOST: postgres.example.com
  DB_PORT: "5060"
  DB_NAME: moodle
  DB_USER: moodle
  DB_PASS: moodle

Data loss after docker compose down && up with PostgreSQL 18+

Cause: Recent postgres:alpine images (18+) expect the named volume at /var/lib/postgresql, not /var/lib/postgresql/data. Mounting the wrong path makes Postgres create an anonymous volume and Moodle starts from scratch. Related: #133.

Fix:

volumes:
  - postgres:/var/lib/postgresql

Follow the PGDATA migration notes if you already have an old volume.

"Data directory (/var/www/moodledata/) cannot be created by the installer"

Cause: The mounted moodledata is not writable by UID 65534 (nobody), or you are reusing a populated moodledata from another image with mismatched permissions. Related: #114, #2.

Fix:

sudo chown -R 65534:65534 ./moodledata

For bind mounts only. Named Docker volumes get the right ownership automatically.

Plugins disappear after upgrading

Cause: Either the moodlehtml volume was wiped, or a code sync (SYNC_MOODLE_CODE=auto) replaced core and a custom plugin path was not listed in EXTRA_PLUGIN_PATHS / reinstalled via PLUGINS or Moosh. Related: #9, #103.

Fix — prefer declarative reinstall so plugins survive every boot:

environment:
  POST_CONFIGURE_COMMANDS: |
    moosh plugin-list
    moosh plugin-install --delete mod_attendance
  # Or list volume-only paths that must survive rsync:
  # EXTRA_PLUGIN_PATHS: "mod/attendance theme/space"

Upgrade seems to do nothing — "No upgrade needed"

Cause: AUTO_UPDATE_MOODLE only runs the database upgrade. If the PHP tree on a moodlehtml volume is still the old release, Moodle correctly reports that no DB upgrade is needed. Related: #102, #103.

Current images ship SYNC_MOODLE_CODE=auto (default): on start they compare Moodle $version in the volume with the image and rsync core from /usr/src/moodle when they differ, then run upgrade.php.

Check:

docker compose logs moodle | grep -E 'Moodle code sync|Upgrading moodle'
# Expect: Moodle code sync: 2025041401.00 → 2025041402.00

If you are on an older image without code sync, either upgrade the image or wipe the code volume once:

docker compose down
docker volume rm <project>_moodlehtml
docker compose pull
docker compose up -d

If sync is disabled (SYNC_MOODLE_CODE=never), re-enable auto or wipe the volume as above.

See Upgrading for the full procedure.

LDAP says the PHP module is missing

Cause: Old image tag. php-ldap is bundled in current releases. Related: #122.

Fix: docker compose pull to update to a current tag. If you use a custom LDAP CA, pass it via MY_CERTIFICATES (base64-encoded PEM).

/var/www/html/vendor/composer does not exist

Cause: Moodle 5.1+ requires composer install to run inside the container on first start. If you stopped the container during this step or mounted an incomplete /var/www/html, the vendor directory is missing. Related: #117.

Fix: Let the container start fully and watch the logs. If it persists, enter the container and run:

docker compose exec moodle composer install --no-dev --classmap-authoritative \
  --working-dir=/var/www/html

Cause: Historically php was missing from the cron service environment. Related: #18.

Fix: Update to a current image. If you want to disable the internal cron entirely, set RUN_CRON_TASKS=false and schedule admin/cli/cron.php externally.

config.php writable warning

Cause: config.php is intentionally made read-only after install for security. Moodle surfaces this as a warning in the admin dashboard. Related: #12.

Fix: This is by design and can be safely ignored. It is not an error.

Where to find Moodle debug output

Enable developer debug mode in the container:

environment:
  DEBUG: "true"

Then read logs with docker compose logs -f moodle and browse the site — errors appear both in the logs and rendered in the page. Related: #25.