This project deploys a Moodle Learning Management System (LMS) on an AWS EC2 instance (t3.medium) using Docker Compose. It was designed to support an online exam setup for 200–500 concurrent users.
The deployment includes a custom Moodle Docker image (built from the Moodle Git repository), MySQL database, phpMyAdmin for database management, Nginx as a reverse proxy, and Let’s Encrypt SSL via Certbot.
- Moodle Service: Custom Docker image (
oluwaseuna/runmoodle:1.7) with anentrypoint.shscript that dynamically generatesconfig.phpusing environment variables and secret files. - Database: MySQL 8.4.5 with persistent storage via Docker named volumes.
- phpMyAdmin: Database management tool to interact with Moodle’s MySQL database.
- Nginx: Reverse proxy with SSL termination.
- Certbot: Automated SSL certificate issuance from Let’s Encrypt.
This architecture ensures a secure, scalable, and production-ready Moodle LMS deployment.
The architecture of the deployment is documented in the architecture folder
- Custom Docker image (
oluwaseuna/runmoodle:1.4). - Runs Apache + PHP with Moodle codebase.
- Uses entrypoint.sh to auto-generate
config.phpat container startup. - Persists uploaded files and cache in
moodleDatavolume.
- MySQL 8.4.5 container.
- Stores Moodle database.
- Persists data in
cloudDBdatavolume. - Reads root and user passwords from secret files in
./secrets/.
- Runs on port 8080.
- Allows DB administrators to log in with MySQL credentials.
- Depends on
moodleDBto be available.
-
Acts as reverse proxy.
-
Forwards traffic to Moodle and phpMyAdmin.
-
Handles ports 80 and 443.
-
Uses bind mounts:
/home/ssm-user/nginx/conf.d/home/ssm-user/nginx/html/home/ssm-user/letsencrypt
- Obtains SSL certificates from Let’s Encrypt.
- Shares the same bind mounts as Nginx for certificate storage.
- Runs once to issue certificates, then exits.
Instead of Docker secrets, this setup uses plain text secret files stored locally inside a secrets/ folder:
mkdir secrets
echo "secure_root_password" > secrets/db_root_password.txt
echo "secure_db_password" > secrets/db_password.txtIn compose.yml, these files are mounted and read by MySQL and Moodle containers.
- AWS EC2 t3.medium instance (Ubuntu 24).
- AmazonSSMManagedInstanceCore IAM role attached to the instance profile (for Systems Manager Session Manager access instead of SSH).
- Domain name pointing to EC2 public IP.
- Docker & Docker Compose installed.
-
Clone Repository
git clone https://github.com/seunayolu/moodle-docker-compose.git cd moodle-docker-compose -
Prepare Environment Variables Create a
.envfile:MYSQL_DATABASE=moodle MYSQL_USER=moodleuser
-
Create Secret Files
mkdir secrets echo "secure_root_password" > secrets/db_root_password.txt echo "secure_db_password" > secrets/db_password.txt
-
Prepare Bind Mount Directories
mkdir -p /home/ssm-user/nginx/conf.d /home/ssm-user/nginx/html /home/ssm-user/letsencrypt
-
Start Services
docker compose up -d
-
Access Services
- Moodle LMS →
https://moodle.teachdev.online - phpMyAdmin →
http://<EC2-IP>:8080
- Moodle LMS →
The entrypoint.sh script is responsible for configuring Moodle automatically when the container starts. Let’s break it down step by step:
#!/bin/bash
set -e- Uses bash shell.
set -e→ stop immediately if any command fails.
MOODLE_DIR=/var/www/html
MOODLE_CONFIG=$MOODLE_DIR/config.php
MOODLE_DATA=/var/www/moodledata- Defines paths for Moodle code, config file, and data directory.
if [ ! -f "$MOODLE_CONFIG" ]; then
echo "Generating Moodle config.php..."
DB_PASS="$(cat "$MOODLE_DATABASE_PASSWORD_FILE")"- Checks if
config.phpalready exists. - If not, it creates one.
- Reads DB password securely from a file (
/run/secrets/...or mounted file).
cat > "$MOODLE_CONFIG" <<EOF
<?php
unset(\$CFG);
global \$CFG;
\$CFG = new stdClass();
...
EOF- Writes a fresh Moodle config.php with PHP settings.
- Sets database connection, site URL (
wwwroot), and data directory.
\$CFG->sslproxy = true;- Tells Moodle it’s running behind an SSL-terminating proxy (Nginx).
else
echo "Moodle config.php already exists — skipping generation."
fi- If config already exists → skip regeneration (avoids overwriting).
exec apache2-foreground- Finally, starts the Apache web server in the foreground → keeps the container alive.
👉 In simple terms: “On first run, generate Moodle’s config file from environment variables and secrets, then start Apache. On later runs, skip config generation and just start Apache.”
- Secrets are stored in local files (
./secrets) and not hardcoded. - SSL certificates auto-renew with Certbot.
- Restrict phpMyAdmin access (firewall or security groups).
- Use AWS SSM Session Manager for secure access instead of SSH.
Oluwaseun Alausa DevOps Engineer | Enabling Secure, Scalable, and Observable Infrastructure 🚀 LinkedIn | YouTube
