How to Install Moodle on a VPS

Most learning management platforms charge per learner, cap your storage, and keep your course data locked inside their ecosystem. Scale past a few hundred students and the invoices start looking punishing. Moodle takes a different approach entirely. It’s free, open-source, and completely indifferent to how many users you enroll. Self-hosting it on a VPS puts you in full control of your data, your costs, and your platform’s future.

This guide walks you through a production-ready Moodle deployment on Ubuntu using Docker, Nginx, and Let’s Encrypt SSL. By the end you’ll have a fully operational LMS sitting behind a secured reverse proxy, ready to serve real learners from day one.

What Is Moodle?

Moodle (Modular Object-Oriented Dynamic Learning Environment) is the most widely deployed open-source LMS on the planet, powering more than 43 million courses across 248 countries. That kind of reach doesn’t happen by accident. The platform handles everything a structured learning environment demands: course creation, student enrollment, quiz engines, assignment submission, grade books, competency frameworks, and granular reporting.

The plugin ecosystem is what separates Moodle from lighter alternatives. More than 1,700 plugins extend the core with H5P interactive content, BigBlueButton virtual classrooms, payment gateways, gamification systems, and SCORM compatibility. You can shape it into almost any kind of learning environment you can imagine.

Use cases span everything from Fortune 500 onboarding programs to rural community colleges. A 20-person startup uses it the same way a 50,000-student university does. The architecture is flexible enough to handle both when given appropriate resources. That said, Moodle isn’t lightweight. It earns its resource requirements by delivering an enterprise-grade platform at zero licensing cost.

Prerequisites

Before diving into the installation, confirm your VPS meets the requirements below. Moodle’s database, PHP runtime, and session cache all need headroom, especially as your learner count grows.

Component Minimum Recommended
vCPU 2 cores 4 cores
RAM 4 GB 8 GB
Storage 30 GB SSD 80 GB SSD
Operating System Ubuntu 22.04 LTS Ubuntu 24.04 LTS
Domain Name Required Required

You’ll also need:

  • A registered domain name with an A record pointing to your VPS IP address
  • SSH access with sudo privileges
  • Ports 80 and 443 open at your provider’s firewall level

How to Install Moodle on a VPS

Step 1: Update Your Server

Start with a clean, fully updated system. This prevents dependency conflicts later and ensures your package index reflects the latest available versions.

sudo apt update && sudo apt upgrade -y

Reboot if the kernel was updated:

sudo reboot

Step 2: Install Docker Engine

Install Docker Engine from the official Docker repository, not the Ubuntu package manager. The Ubuntu-packaged version often lags several releases behind and lacks the Compose V2 plugin.

sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

Add the Docker repository to your apt sources:

echo /
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu /
  $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | /
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Install Docker Engine and the Compose plugin:

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Enable Docker to start on boot and add your user to the docker group:

sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker

Verify Docker is working correctly:

docker run --rm hello-world

Step 3: Configure the UFW Firewall

Lock down your server to allow only SSH, HTTP, and HTTPS traffic. UFW handles this cleanly with a few commands.

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

Moodle’s Docker port (8080) stays bound to 127.0.0.1 only. Nginx handles all public traffic. This means no learner ever reaches the application container directly.

Step 4: Create the Project Directory

Create a dedicated directory for your Moodle project files. This keeps your compose file and environment config organized in one place.

mkdir -p ~/moodle && cd ~/moodle

Step 5: Configure Environment Variables

Create an .env file to hold your credentials and configuration values. Docker Compose picks this up automatically, so sensitive values never live directly inside your compose file.

nano .env

Paste the following block and replace every placeholder value with something strong and unique:

# Database credentials
DB_ROOT_PASSWORD=replace_with_strong_root_password
DB_NAME=moodle
DB_USER=moodle
DB_PASSWORD=replace_with_strong_db_password

# Moodle admin account
MOODLE_SITE_NAME=My Moodle Site
MOODLE_ADMIN_USER=admin
MOODLE_ADMIN_PASSWORD=replace_with_strong_admin_password
[email protected]

# Your domain
MOODLE_DOMAIN=yourdomain.com

Save and close the file, then restrict its permissions so only your user can read it:

chmod 600 .env

Step 6: Write the Docker Compose File

Create the Compose file that defines your Moodle stack. It includes two services: a MariaDB 11.4 database and the Bitnami Moodle 4 application container. The database healthcheck prevents a startup race condition where Moodle tries to connect before MariaDB is ready to accept connections.

nano docker-compose.yml
services:
  mariadb:
    image: bitnami/mariadb:11.4
    container_name: moodle_db
    restart: unless-stopped
    environment:
      - MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
      - MARIADB_DATABASE=${DB_NAME}
      - MARIADB_USER=${DB_USER}
      - MARIADB_PASSWORD=${DB_PASSWORD}
      - MARIADB_CHARACTER_SET=utf8mb4
      - MARIADB_COLLATE=utf8mb4_unicode_ci
    volumes:
      - mariadb_data:/bitnami/mariadb
    networks:
      - moodle_net
    healthcheck:
      test: ['CMD', '/opt/bitnami/scripts/mariadb/healthcheck.sh']
      interval: 15s
      timeout: 5s
      retries: 6

  moodle:
    image: bitnami/moodle:4
    container_name: moodle_app
    restart: unless-stopped
    environment:
      - MOODLE_DATABASE_HOST=mariadb
      - MOODLE_DATABASE_PORT_NUMBER=3306
      - MOODLE_DATABASE_USER=${DB_USER}
      - MOODLE_DATABASE_PASSWORD=${DB_PASSWORD}
      - MOODLE_DATABASE_NAME=${DB_NAME}
      - MOODLE_SITE_NAME=${MOODLE_SITE_NAME}
      - MOODLE_USERNAME=${MOODLE_ADMIN_USER}
      - MOODLE_PASSWORD=${MOODLE_ADMIN_PASSWORD}
      - MOODLE_EMAIL=${MOODLE_ADMIN_EMAIL}
      - MOODLE_HOST=${MOODLE_DOMAIN}
      - MOODLE_REVERSEPROXY=yes
    ports:
      - "127.0.0.1:8080:8080"
    volumes:
      - moodle_data:/bitnami/moodle
      - moodledata_data:/bitnami/moodledata
    depends_on:
      mariadb:
        condition: service_healthy
    networks:
      - moodle_net

volumes:
  mariadb_data:
  moodle_data:
  moodledata_data:

networks:
  moodle_net:
    driver: bridge

A few things worth noting here. The MOODLE_REVERSEPROXY=yes variable tells Moodle to set $CFG->sslproxy = true in its configuration, which is essential when Nginx terminates SSL and forwards requests over plain HTTP internally. Without it, Moodle generates broken HTTP links on an HTTPS site. The MOODLE_HOST variable ensures Moodle builds its internal URLs using your actual domain. Two separate volumes persist the Moodle codebase and uploaded course data independently, making upgrades cleaner.

Verify the image tags on Docker Hub before deploying to confirm bitnami/moodle:4 and bitnami/mariadb:11.4 reflect the current stable releases.

Step 7: Launch the Containers

Start the stack in detached mode:

docker compose up -d

First boot takes longer than you might expect. Moodle initializes an entire database schema, installs default data, and configures the PHP environment from scratch. Budget 5 to 15 minutes. This is normal. Don’t restart the containers mid-initialization.

Monitor the startup progress in real time:

docker compose logs -f moodle

Moodle is ready when the log output shifts from installation messages to Apache access log lines. You can also check the overall container state:

docker compose ps

Both containers should show a status of running. The MariaDB container will show healthy once it passes its healthcheck. While you wait on Moodle, move straight to the Nginx setup below.

Step 8: Install and Configure Nginx

Install Nginx from the Ubuntu repository:

sudo apt install -y nginx

Create a new virtual host configuration for Moodle. Replace yourdomain.com with your actual domain in both the filename and the config content.

sudo nano /etc/nginx/sites-available/moodle

Paste the following initial HTTP-only block. Certbot will upgrade it to SSL in the next step.

server {
    listen 80;
    server_name yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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 $scheme;
    }
}

Enable the site, test the configuration, and reload Nginx:

sudo ln -s /etc/nginx/sites-available/moodle /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Step 9: Secure Moodle with SSL

Install Certbot with the Nginx plugin, then issue a certificate for your domain:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com

Certbot handles the domain verification, obtains the certificate, and modifies your Nginx config. Once it finishes, replace the entire Nginx config with the clean production-ready version below. This adds the Moodle-specific tunings that large file uploads and slow PHP operations require.

sudo nano /etc/nginx/sites-available/moodle

Replace the file contents with:

server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Allow large uploads for course files and SCORM packages
    client_max_body_size 512M;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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 $scheme;

        # Extended timeouts for slow Moodle operations
        proxy_read_timeout 300s;
        proxy_connect_timeout 75s;
        proxy_send_timeout 300s;

        # Buffer tuning for large responses
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
    }
}

Test the updated configuration and reload Nginx:

sudo nginx -t && sudo systemctl reload nginx

Let’s Encrypt certificates expire after 90 days. Certbot installs a systemd timer that handles renewals automatically. Confirm the timer is active:

sudo systemctl status certbot.timer

Step 10: Verify Your Installation

Open a browser and navigate to https://yourdomain.com. You should see the Moodle login page with a valid SSL padlock. Log in with the admin credentials you set in your .env file.

After logging in, head to Site administration > Notifications. Moodle checks for any pending upgrade steps here after every fresh install or update. If a prompt appears, run through it to finalize the database setup.

Confirm your site URL is correct at Site administration > General > General settings. The wwwroot value should show https://yourdomain.com with no trailing slash. If it shows HTTP instead of HTTPS, revisit the MOODLE_REVERSEPROXY=yes variable in your .env file and recreate the containers.

How to Update Moodle

Moodle updates ship as new Docker image releases. The upgrade process involves pulling the new image, recreating the containers, and letting Moodle’s built-in upgrade script handle any required database schema changes. Always back up your data before upgrading.

Back up your volumes first:

docker run --rm /
  -v moodle_mariadb_data:/data /
  -v $(pwd)/backups:/backup /
  alpine tar czf /backup/mariadb-$(date +%Y%m%d).tar.gz -C /data .

docker run --rm /
  -v moodle_moodledata_data:/data /
  -v $(pwd)/backups:/backup /
  alpine tar czf /backup/moodledata-$(date +%Y%m%d).tar.gz -C /data .

Pull the latest images and recreate the containers:

cd ~/moodle
docker compose pull
docker compose up -d

After the containers restart, log into the Moodle admin panel and navigate to Site administration > Notifications. If the upgrade introduced database changes, Moodle displays an upgrade prompt. Click through to apply the schema updates. Don’t skip this step. Running a newer Moodle version against an un-upgraded database causes unpredictable behavior.

Check the admin panel for any deprecation notices or plugin compatibility warnings after the upgrade completes. Plugin authors frequently lag one release cycle behind the core, so test thoroughly before cutting over in a production environment.

Troubleshooting

Moodle Shows a Maintenance Screen Right After Startup

This is almost always a first-boot timing issue, not an actual problem. Moodle enters a self-imposed maintenance mode during database initialization to prevent requests from hitting a half-configured system. It lifts the mode automatically once setup is complete.

Check how far along initialization has progressed:

docker compose logs -f moodle

Wait for Apache access log lines to appear in the output. That shift signals that setup is done and Moodle is serving requests normally. If the maintenance screen persists after 20 minutes, check for errors in the log stream. A database connection failure is the most common cause of a stalled install.

Nginx Returns a 502 Bad Gateway Error

A 502 means Nginx reached your server but couldn’t get a valid response from the upstream application. First, confirm both containers are actually running:

docker compose ps

If the Moodle container shows as exited or restarting, pull the logs to find the root cause:

docker compose logs moodle

Also verify that Nginx is pointing to the right port. The proxy_pass directive must read http://127.0.0.1:8080 and the Moodle container must have 127.0.0.1:8080:8080 in its ports mapping. A mismatch here produces exactly this error.

Database Connection Errors on First Boot

If Moodle logs show repeated “Cannot connect to database” messages during startup, MariaDB likely isn’t ready yet. The healthcheck-based depends_on in the Compose file handles this in most cases but occasionally MariaDB takes longer than expected on underpowered hardware.

Check the MariaDB healthcheck status:

docker inspect moodle_db --format='{{json .State.Health.Status}}'

If the status reads starting, give MariaDB another minute. If it shows unhealthy, check the database logs directly:

docker compose logs mariadb

Mismatched credentials between your .env file and the running containers are another common culprit. If you edited .env after the first boot, stop the stack and recreate the containers rather than just restarting them, since the database volume already has the old credentials baked in.

File Uploads Fail or Report a Size Limit Error

Large SCORM packages and video course files can easily exceed default upload limits. Two settings control this.

First, confirm your Nginx config includes client_max_body_size 512M; inside the SSL server block. If it’s missing, add it and reload Nginx:

sudo nginx -t && sudo systemctl reload nginx

Second, Moodle has its own upload limit in the admin settings. Go to Site administration > Security > Site security settings and increase the Maximum uploaded file size value to match what you set in Nginx. Keep both values in sync. An Nginx limit larger than Moodle’s internal limit wastes nothing. The reverse causes confusing failures.

Moodle Generates HTTP Links on an HTTPS Site

This mixed-content issue appears when Moodle doesn’t know it’s sitting behind an SSL-terminating proxy. The symptom: asset URLs and internal links use http:// even though the browser loaded the page over HTTPS.

The fix lives in your docker-compose.yml. Confirm the Moodle service has both of these environment variables set:

- MOODLE_HOST=${MOODLE_DOMAIN}
- MOODLE_REVERSEPROXY=yes

If they’re present but the issue persists, check the wwwroot value stored in the database. Navigate to Site administration > General > General settings and confirm Moodle URL starts with https://. You can correct it there directly. After saving, clear Moodle’s cache under Site administration > Development > Purge all caches and reload the page.

What to Build Next

Your Moodle installation is running but there’s plenty left to explore. Here are the most impactful next steps for a serious deployment:

  • Add the H5P plugin for interactive content types like drag-and-drop, branching scenarios, and interactive video. H5P is the fastest way to move beyond static PDFs and quizzes.
  • Integrate BigBlueButton for live virtual classrooms directly inside Moodle courses. Learners can join sessions without leaving the platform.
  • Deploy Redis for session caching to dramatically reduce database load as concurrent user counts grow. Moodle’s session handling is one of the first bottlenecks to hit under load.
  • Configure SMTP for outgoing email so Moodle can send enrollment confirmations, assignment notifications, and forum digests. Go to Site administration > Server > Email > Outgoing mail configuration.
  • Automate database backups with a daily cron job that dumps the MariaDB volume and ships it off-server. A self-hosted LMS with no backup strategy is one disk failure away from disaster.
  • Set up the Moodle Mobile App configuration so learners can access your courses on iOS and Android. The mobile service setup lives under Site administration > Mobile app > Mobile authentication.

Wrapping Up

You now have a production-grade Moodle instance running on your own VPS, secured with SSL, and shielded behind an Nginx reverse proxy. The Docker stack makes future upgrades straightforward and keeps your application data isolated in persistent volumes. Whether you’re building a corporate training portal, an online academy, or a hybrid university course, this foundation scales with you. Start with a course or two, test the learner experience end-to-end, then build out from there.

The post How to Install Moodle on a VPS appeared first on GreenGeeks.

版权声明:
作者:dingding
链接:https://www.techfm.club/p/237161.html
来源:TechFM
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>