Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,17 @@ jobs:
sed -i 's/CHANGE_ME_SECURE_ROOT_PASSWORD/test_root_456/g' .env
sed -i 's/CHANGE_ME_SECURE_VALKEY_PASSWORD/test_valkey_789/g' .env

# The runner image ships an older compose than deployments run, and it
# rejects keys current compose accepts (pre_start among them). Validate
# against current compose, not against the runner's.
- name: Set up Docker Compose
uses: docker/setup-compose-action@4eb059ff7f16592f9c84d5ca339c53cb7c5064e2 # v2.3.0
with:
version: latest

- name: Validate docker-compose.yml syntax
run: |
docker compose version
docker compose config > /dev/null
echo "Docker Compose syntax is valid"

Expand Down Expand Up @@ -291,8 +300,16 @@ jobs:
-out docker/nginx/ssl/cert.pem \
-subj "/CN=localhost/O=CI Test/C=US"

# Same reason as in validate-compose: compose.yml uses keys the runner's
# preinstalled version does not know.
- name: Set up Docker Compose
uses: docker/setup-compose-action@4eb059ff7f16592f9c84d5ca339c53cb7c5064e2 # v2.3.0
with:
version: latest

- name: Build and start services
run: |
docker compose version
docker compose build --no-cache
docker compose up -d
echo "Services started"
Expand Down
2 changes: 1 addition & 1 deletion QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ To upgrade to a new Moodle version:
2. Rebuild the image and restart the stack. The sources ship inside the image,
so a rebuild is required — without it the container refuses to start:
```bash
docker compose build moodle moodle-cron
docker compose build moodle
docker compose up -d
```

Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ nginx 1.31, MariaDB 12.3 (Docker Hardened Image), and Valkey 9.
- **Modern Web Stack**: PHP-FPM + nginx architecture (no mod_php)
- **HTTP/2 and HTTP/3 (QUIC)**: Modern protocol support out of the box
- **Brotli Compression**: Better compression than gzip for modern browsers
- **Dedicated Cron Container**: Isolated cron execution via Ofelia scheduler
- **Scheduled Cron**: Ofelia runs Moodle cron in the application container
on a one-minute schedule
- **Redis Sessions**: Valkey-backed session storage for scalability

## Architecture
Expand Down Expand Up @@ -50,8 +51,8 @@ nginx 1.31, MariaDB 12.3 (Docker Hardened Image), and Valkey 9.
│ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Moodle Cron │◄────────│ Ofelia │ │
│ │ (PHP-FPM) │ exec │ Scheduler │ │
│ │ Moodle App │◄────────│ Ofelia │ │
│ │ (cron job) │ exec │ Scheduler │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ (optional, dev profile) │
Expand Down Expand Up @@ -218,7 +219,7 @@ docker compose exec moodle php /var/www/html/admin/cli/maintenance.php --enable
docker compose exec database mysqldump -uroot -p"$DB_ROOT_PASSWORD" moodle | gzip > backup-$(date +%Y%m%d).sql.gz

# Rebuild the image with the new version, then restart
docker compose build moodle moodle-cron
docker compose build moodle
docker compose up -d moodle

# Watch the upgrade
Expand Down Expand Up @@ -362,8 +363,8 @@ docker compose run --rm --no-deps --entrypoint cat moodle /var/www/html/.moodle-
# On a mismatch, set MOODLE_VERSION in .env, then rebuild and restart.
# Compose passes that value as the build argument, so do not pass it again
# here - a divergence between the two is what caused the mismatch.
docker compose build moodle moodle-cron
docker compose up -d moodle moodle-cron
docker compose build moodle
docker compose up -d moodle
```

### PHP-FPM Health Check Fails
Expand Down Expand Up @@ -404,10 +405,10 @@ docker compose exec moodle chmod -R 0775 /var/moodledata
docker compose logs ofelia

# Check cron container
docker compose logs moodle-cron
docker compose logs ofelia

# Run cron manually
docker compose exec moodle-cron php /var/www/html/admin/cli/cron.php
docker compose exec -u www-data moodle php /var/www/html/admin/cli/cron.php
```

## Security Notes
Expand Down
130 changes: 45 additions & 85 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,55 +68,59 @@ services:
- frontend # Outbound access for Moodle itself (plugin updates, external services)
- backend
depends_on:
db-init:
condition: service_completed_successfully
database:
condition: service_healthy
valkey:
condition: service_healthy
# Init container: the hardened database image acts on MARIADB_ROOT_PASSWORD
# only and creates neither database nor user, and it offers no
# /docker-entrypoint-initdb.d. This runs once before the service starts,
# in the database image because the PHP image carries no client.
# Idempotent, so repeated starts are harmless.
pre_start:
- image: dhi.io/mariadb:12.3
environment:
- MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD is required}
- DB_NAME=${DB_NAME:-moodle}
- DB_USER=${DB_USER:-moodle}
- DB_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required}
command:
- /bin/sh
- -c
- |
set -eu
# Identifiers cannot be parameterised, so reject anything that would
# break out of the quoting instead of interpolating it blindly.
case "$$DB_NAME$$DB_USER" in
*[!A-Za-z0-9_-]*) echo "DB_NAME and DB_USER may only contain A-Z a-z 0-9 _ -" >&2; exit 1 ;;
esac
# Single quotes are the only metacharacter inside a SQL string literal.
esc_pw=$$(printf '%s' "$$DB_PASSWORD" | sed "s/'/''/g")
mariadb -h database -u root -p"$$MARIADB_ROOT_PASSWORD" \
-e "CREATE DATABASE IF NOT EXISTS \`$$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '$$DB_USER'@'%' IDENTIFIED BY '$$esc_pw';
ALTER USER '$$DB_USER'@'%' IDENTIFIED BY '$$esc_pw';
GRANT ALL PRIVILEGES ON \`$$DB_NAME\`.* TO '$$DB_USER'@'%';
FLUSH PRIVILEGES;"
echo "database and user ready"
# Cron runs in this container rather than a dedicated one. Ofelia's
# job-exec needs a running container, and this one runs anyway; a separate
# service would have to sit in `sleep infinity` doing nothing. job-run
# would avoid the idle container too, but it needs every volume, network
# and environment variable repeated in labels - a second copy of the
# service definition that drifts.
labels:
ofelia.enabled: "true"
ofelia.job-exec.moodle-cron.schedule: "@every 1m"
ofelia.job-exec.moodle-cron.command: "php /var/www/html/admin/cli/cron.php"
ofelia.job-exec.moodle-cron.user: "www-data"
healthcheck:
test: ["CMD", "php-fpm-healthcheck"]
interval: 30s
timeout: 5s
retries: 3
start_period: 120s # Allow time for the first copy into the code volume

# ==========================================================================
# Moodle Cron - Dedicated Container for Scheduled Tasks
# ==========================================================================
moodle-cron:
image: netresearch/moodle:${MOODLE_VERSION:-5.2.3}
build:
context: ./docker/moodle
dockerfile: Dockerfile
args:
MOODLE_VERSION: ${MOODLE_VERSION:-5.2.3}
container_name: ${COMPOSE_PROJECT_NAME:-moodle}_cron
restart: unless-stopped
command: ["sleep", "infinity"]
volumes:
- moodle_code:/var/www/html:ro
- moodledata:/var/moodledata
environment:
- MOODLE_SKIP_BOOTSTRAP=true
- DB_TYPE=${DB_TYPE:-mariadb}
- DB_HOST=${DB_HOST:-database}
- DB_NAME=${DB_NAME:-moodle}
- DB_USER=${DB_USER:-moodle}
- DB_PASSWORD=${DB_PASSWORD}
- DB_PREFIX=${DB_PREFIX:-mdl_}
- VALKEY_HOST=${VALKEY_HOST:-valkey}
- VALKEY_PORT=${VALKEY_PORT:-6379}
- VALKEY_PASSWORD=${VALKEY_PASSWORD}
networks:
- backend
depends_on:
moodle:
condition: service_healthy
labels:
ofelia.enabled: "true"
ofelia.job-exec.moodle-cron.schedule: "@every 1m"
ofelia.job-exec.moodle-cron.command: "php /var/www/html/admin/cli/cron.php"
ofelia.job-exec.moodle-cron.user: "www-data"

# ==========================================================================
# MariaDB 12.3 - Database (Docker Hardened Image)
#
Expand All @@ -143,50 +147,6 @@ services:
retries: 5
start_period: 30s

# ==========================================================================
# Database bootstrap - creates the Moodle database and user
#
# The hardened image acts on MARIADB_ROOT_PASSWORD only: it creates neither
# database nor user, and offers no /docker-entrypoint-initdb.d. This runs
# once and exits. Idempotent, so repeated starts are harmless.
#
# Compose models init containers as pre_start hooks, which would be the
# tidier form, but the compose version on the CI runners (v2.38) rejects
# pre_start outright. A one-shot service works on every version.
# ==========================================================================
db-init:
image: dhi.io/mariadb:12.3
container_name: ${COMPOSE_PROJECT_NAME:-moodle}_db_init
restart: "no"
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -eu
# Identifiers cannot be parameterised, so reject anything that would
# break out of the quoting instead of interpolating it blindly.
case "$$DB_NAME$$DB_USER" in
*[!A-Za-z0-9_-]*) echo "DB_NAME and DB_USER may only contain A-Z a-z 0-9 _ -" >&2; exit 1 ;;
esac
# Single quotes are the only metacharacter inside a SQL string literal.
esc_pw=$$(printf '%s' "$$DB_PASSWORD" | sed "s/'/''/g")
mariadb -h database -u root -p"$$MARIADB_ROOT_PASSWORD" \
-e "CREATE DATABASE IF NOT EXISTS \`$$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '$$DB_USER'@'%' IDENTIFIED BY '$$esc_pw';
ALTER USER '$$DB_USER'@'%' IDENTIFIED BY '$$esc_pw';
GRANT ALL PRIVILEGES ON \`$$DB_NAME\`.* TO '$$DB_USER'@'%';
FLUSH PRIVILEGES;"
echo "database and user ready"
environment:
- MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD is required}
- DB_NAME=${DB_NAME:-moodle}
- DB_USER=${DB_USER:-moodle}
- DB_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required}
networks:
- backend
depends_on:
database:
condition: service_healthy

# ==========================================================================
# Valkey 9 - Redis-Compatible Cache & Sessions
# ==========================================================================
Expand Down Expand Up @@ -223,7 +183,7 @@ services:
networks:
- backend
depends_on:
- moodle-cron
- moodle

# ==========================================================================
# Mailpit - Development Mail Catcher (Optional)
Expand Down
Loading