Reverse Proxy¶
Most deployments run alpine-moodle behind a reverse proxy that terminates TLS. This page documents how to wire that up correctly and how to avoid the recurring pitfalls reported in issues like #15, #21, #51, #57, #61, #101, #127 and #137.
How Moodle sees the world¶
Moodle builds every URL from $CFG->wwwroot. That value is derived from SITE_URL at first start. If wwwroot does not exactly match the URL the browser uses, you will see:
- Broken CSS / JS — assets are generated against a different origin, triggering mixed content or 404s.
ERR_TOO_MANY_REDIRECTS— Moodle keeps redirecting to its canonicalwwwrootbecause the incoming request looks different.- Login loops — session cookies are set on a host Moodle does not recognise.
To use a reverse proxy correctly you need to tell Moodle two things:
- The public URL users type in the browser →
SITE_URL - Whether the public connection is HTTPS even though the container speaks HTTP →
SSLPROXY=true
The correct settings¶
| Variable | Value | Purpose |
|---|---|---|
SITE_URL |
Full public URL with scheme | Becomes $CFG->wwwroot. Must match what users type in the browser. |
SSLPROXY |
true |
Trusts X-Forwarded-Proto so Moodle treats the request as HTTPS. |
REVERSEPROXY |
false in most cases |
Only set to true if the same site is intentionally accessed under multiple base URLs (multi-tenant / multi-host). See Moodle docs on reverse proxies. |
REVERSEPROXY=true is rarely what you want
Setting REVERSEPROXY=true on a single-URL deployment triggers "Reverse proxy enabled so the server cannot be accessed directly" errors (issue #137). Leave it at false unless you actually serve the same site from several hostnames.
Required proxy headers¶
Whatever proxy you use, it must forward these headers:
Host— the public hostnameX-Forwarded-Proto—httpsX-Forwarded-For— the real client IP
Without X-Forwarded-Proto: https the SSLPROXY flag has nothing to trust and Moodle will keep redirecting to HTTP.
Traefik (v2 / v3)¶
Label-based configuration, no file changes required on the Traefik side.
services:
moodle:
image: erseco/alpine-moodle
restart: unless-stopped
environment:
SITE_URL: https://moodle.example.com
SSLPROXY: "true"
DB_HOST: postgres
DB_USER: moodle
DB_PASS: moodle
DB_NAME: moodle
MOODLE_USERNAME: admin
MOODLE_PASSWORD: ChangeMe123!
volumes:
- moodledata:/var/www/moodledata
- moodlehtml:/var/www/html
networks:
- proxy
- default
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.moodle.rule=Host(`moodle.example.com`)"
- "traefik.http.routers.moodle.entrypoints=websecure"
- "traefik.http.routers.moodle.tls=true"
- "traefik.http.routers.moodle.tls.certresolver=letsencrypt"
- "traefik.http.services.moodle.loadbalancer.server.port=8080"
networks:
proxy:
external: true
Traefik forwards X-Forwarded-Proto automatically when the entry point terminates TLS, so nothing else is needed.
502 Bad Gateway with Traefik
A 502 from Traefik (issue #61) usually means the service port is wrong. The container listens on 8080, not 80. Set traefik.http.services.moodle.loadbalancer.server.port=8080.
Nginx¶
server {
listen 443 ssl http2;
server_name moodle.example.com;
ssl_certificate /etc/letsencrypt/live/moodle.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/moodle.example.com/privkey.pem;
client_max_body_size 100M;
location / {
proxy_pass http://moodle:8080;
proxy_http_version 1.1;
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 https;
proxy_set_header X-Forwarded-Host $host;
proxy_read_timeout 300s;
}
}
server {
listen 80;
server_name moodle.example.com;
return 301 https://$host$request_uri;
}
Set client_max_body_size high enough to allow large course uploads. Also raise the matching PHP variables (post_max_size, upload_max_filesize) on the Moodle container.
Nginx Proxy Manager¶
NPM is popular for homelab deployments and is the source of several support questions (#51).
- Expose the container on an internal Docker network that NPM can reach (no host port required).
- In NPM, create a Proxy Host:
- Domain Names:
moodle.example.com - Scheme:
http - Forward Hostname / IP: the container name (e.g.
moodle) - Forward Port:
8080 - Toggle Block Common Exploits: off (NPM's WAF can break some Moodle paths)
- Toggle Websockets Support: on
- Domain Names:
- On the SSL tab, request a Let's Encrypt certificate and enable Force SSL and HTTP/2.
-
Under Advanced, add:
-
In your
docker-compose.ymlfor Moodle, setSITE_URL=https://moodle.example.comandSSLPROXY=true.
Apache (mod_proxy)¶
<VirtualHost *:443>
ServerName moodle.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/moodle.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/moodle.example.com/privkey.pem
ProxyPreserveHost On
ProxyRequests Off
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
ProxyPass / http://moodle:8080/
ProxyPassReverse / http://moodle:8080/
<Proxy *>
Require all granted
</Proxy>
</VirtualHost>
<VirtualHost *:80>
ServerName moodle.example.com
Redirect permanent / https://moodle.example.com/
</VirtualHost>
Enable the required modules once:
Caddy¶
moodle.example.com {
reverse_proxy moodle:8080 {
header_up Host {host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-For {remote}
}
}
With SITE_URL=https://moodle.example.com and SSLPROXY=true on the Moodle container this is all you need — Caddy will obtain and renew TLS automatically.
Cloudflare / Cloudflared¶
If you front Moodle with Cloudflare (proxied DNS or cloudflared tunnel), Cloudflare already injects X-Forwarded-Proto: https. You only need:
To log the real visitor IP instead of the Cloudflare edge IP, enable the CF-Connecting-IP header in your front proxy (or use Cloudflare's True-Client-IP) and ensure the next proxy hop copies it into X-Forwarded-For.
Reverse proxy with a URL prefix¶
Moodle does not support being served from a subpath such as https://example.com/mylms/ (issue #127). Upstream Moodle requires its own hostname or subdomain. If you need path-based routing, use a dedicated subdomain (moodle.example.com) instead.
Checklist¶
Before opening a support issue, verify:
-
SITE_URLis the public URL, starts withhttps://, no trailing slash. -
SSLPROXY=trueis set when the proxy terminates TLS. -
REVERSEPROXY=false(unless you really use multiple base URLs). - The proxy forwards
Host,X-Forwarded-ProtoandX-Forwarded-For. - The upstream port is
8080, not80. - You restarted the container after changing
SITE_URLon an existing installation and, if necessary, manually updatedwwwrootinconfig.php.