Skip to content
Back to Insights
Engineering 10 min read

Moodle to Hostinger VPS: Practical Live Migration Guide

R
Roomi Kh

Published March 8, 2026 · Reviewed March 8, 2026

Moodle to Hostinger VPS: Practical Live Migration Guide

This was a live Moodle migration from TMD shared hosting / Softaculous to a raw Hostinger Ubuntu VPS. No control panel, no Docker layer, no one-click Moodle app. Just a direct move with the least amount of extra moving parts.

The goal was simple: rebuild the environment cleanly, move the full Moodle stack, and get the site working on the new VPS before final DNS cutover.

The biggest lesson: with Moodle, migrations break when you treat them like a basic website copy. Moodle only works when database, codebase, moodledata, permissions, and runtime versions all line up.

TL;DR Checklist

  1. Keep the new server on plain Ubuntu.
  2. Install a Moodle-compatible PHP version before doing anything else.
  3. Create the new MariaDB database and DB user.
  4. Pull over the database dump, Moodle code, and moodledata.
  5. Extract large archives directly into their final destination to avoid duplicate disk usage.
  6. Fix ownership and permissions.
  7. Clean up config.php so old paths do not override new ones.
  8. Configure Nginx and cron.
  9. Purge caches through CLI.
  10. Test on the VPS before final DNS and SSL cutover.

Why We Avoided the One-Click Route

The new VPS was just Ubuntu 24.04 LTS. Hostinger offered:

  • Docker Moodle
  • Application Moodle
  • control panels
  • plain OS

We stayed with plain Ubuntu because it causes the least trouble for a migration like this.

Why that mattered:

  • Moodle 4.0.6 is older and sensitive to PHP version compatibility
  • one-click stacks can hide paths, versions, and service config
  • Docker adds extra layers for volumes, permissions, and backups
  • a control panel adds complexity we did not need for a single-site migration

For this move, plain Ubuntu gave the cleanest path.

Source Environment

The old Moodle installation details were:

  • Moodle version: 4.0.6
  • URL: https://yourdomain.com
  • Moodle code path: /home/youruser/public_html/yourdomain.com
  • moodledata path: /home/youruser/moodledata
  • DB type: mariadb
  • DB host: localhost
  • DB name: your_db_name
  • DB user: your_db_user
  • DB password: existing production DB password from config.php
  • Table prefix: mdl_

Target Environment

The new destination stack was:

  • Hostinger VPS
  • Ubuntu 24.04 LTS
  • Nginx
  • MariaDB
  • PHP 8.0
  • Moodle code path: /var/www/yourdomain.com
  • moodledata path: /var/www/moodledata

The Golden Rule: Moodle Is a Multi-Layer System

A Moodle migration works only when these layers stay aligned:

  1. Codebase
  2. Database
  3. moodledata
  4. Server runtime

If one of those is off, you get things like:

  • HTTP 500 / 503 errors
  • broken redirects
  • missing assets
  • blank pages
  • bad login/session behavior
  • CLI failures
  • file/media issues

Step 1: Build a Clean Compatible Stack First

Before moving anything, install the right stack on the VPS.

In this case, the critical decision was using PHP 8.0, not the Ubuntu default PHP version, because the source Moodle was 4.0.6.

Ubuntu Server Terminal configuration

BASH
apt update && apt upgrade -y
apt install -y software-properties-common ca-certificates lsb-release apt-transport-https curl wget unzip git nano
add-apt-repository -y ppa:ondrej/php
apt update
apt install -y nginx mariadb-server php8.0-fpm php8.0-cli php8.0-common php8.0-mysql php8.0-xml php8.0-gd php8.0-curl php8.0-zip php8.0-mbstring php8.0-intl php8.0-soap php8.0-bcmath php8.0-ldap php8.0-opcache php8.0-redis imagemagick graphviz aspell ghostscript
systemctl enable nginx mariadb php8.0-fpm
systemctl start nginx mariadb php8.0-fpm

Then verify the actual stack instead of assuming it worked:

BASH
php -v
nginx -v
mysql --version
systemctl is-active nginx
systemctl is-active mariadb
systemctl is-active php8.0-fpm

Step 2: Create the Final Paths Early

BASH
mkdir -p /var/www/yourdomain.com
mkdir -p /var/www/moodledata
chown -R www-data:www-data /var/www/moodledata
chmod -R 770 /var/www/moodledata

This avoids confusion later when you start restoring files.

Step 3: Create the New Database

Secure MariaDB, then create a clean database and DB user for the new server.

SQL
CREATE DATABASE moodle_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'moodle_user'@'localhost' IDENTIFIED BY 'STRONG_PASSWORD_HERE';
GRANT ALL PRIVILEGES ON moodle_db.* TO 'moodle_user'@'localhost';
FLUSH PRIVILEGES;

Test the DB login immediately:

BASH
mysql -u moodle_user -p -e "SHOW DATABASES;"

Do this before importing anything.

Step 4: Export the Three Things That Actually Matter

From the source server, pull over:

  1. database dump
  2. Moodle code
  3. moodledata

On the TMD side, this was the direct export approach:

BASH
mysqldump -u OLD_DB_USER -p OLD_DB_NAME > ~/moodle-db.sql

cd /home/youruser/public_html
zip -r ~/moodle-files.zip yourdomain.com

cd /home/youruser
zip -r ~/moodledata.zip moodledata

Then transfer directly to the new VPS:

BASH
scp ~/moodle-db.sql ~/moodle-files.zip ~/moodledata.zip root@NEW_VPS_IP:/root/

For large moodledata, direct server-to-server transfer saves time and avoids downloading to your local machine.

Step 5: Restore Smarter, Not Just Faster

This migration hit a very real problem:

No space left on device

The VPS had a 100 GB disk, and moodledata.zip alone was 34 GB.

The mistake was:

  • keep the zip in /root
  • unzip a full copy into /root/moodledata
  • then copy another full copy into /var/www/moodledata

That temporarily created multiple copies of the same data and filled the disk.

This is exactly how you burn through space during a migration.

The wrong pattern

BASH
unzip moodledata.zip
cp -a /root/moodledata/. /var/www/moodledata/

The better pattern

Extract directly into the final destination:

BASH
cd /var/www
unzip -q /root/moodledata.zip -d /var/www/
chown -R www-data:www-data /var/www/moodledata
find /var/www/moodledata -type d -exec chmod 770 {} \;
find /var/www/moodledata -type f -exec chmod 660 {} \;

Then free space immediately:

BASH
rm -f /root/moodledata.zip
df -h

Same idea for code:

BASH
cd /var/www
unzip -q /root/moodle-files.zip -d /var/www/
chown -R www-data:www-data /var/www/yourdomain.com
find /var/www/yourdomain.com -type f -exec chmod 644 {} \;
find /var/www/yourdomain.com -type d -exec chmod 755 {} \;

Step 6: Import the Database

Database and Files Secure Transfer

BASH
mysql -u moodle_user -p moodle_db < /root/moodle-db.sql

Do not move on until the DB import completes cleanly.

Step 7: Fix config.php Properly

This is where a lot of Moodle migrations quietly break.

The config file needs to reflect the new server paths and DB values:

PHP
$CFG->wwwroot   = 'https://yourdomain.com';
$CFG->dataroot  = '/var/www/moodledata';
$CFG->admin     = 'admin';

$CFG->dbtype    = 'mariadb';
$CFG->dblibrary = 'native';
$CFG->dbhost    = 'localhost';
$CFG->dbname    = 'moodle_db';
$CFG->dbuser    = 'moodle_user';
$CFG->dbpass    = 'STRONG_PASSWORD_HERE';
$CFG->prefix    = 'mdl_';

$CFG->dboptions = array (
  'dbpersist' => 0,
  'dbport' => '',
  'dbsocket' => '',
  'dbcollation' => 'utf8mb4_general_ci',
);

$CFG->directorypermissions = 0777;

require_once(__DIR__ . '/lib/setup.php');

Critical real-world lesson

In this migration, config.php contained duplicate lines, including an old dataroot later in the file:

PHP
$CFG->dataroot  = '/home/youruser/moodledata';

That old line was overriding the correct new path and causing Moodle CLI to fail with:

  • dataroot not configured properly
  • directory does not exist or is not accessible

So do not just edit the top of config.php. Read the whole file and remove stale duplicate values.

Step 8: Validate moodledata as the Web User

Do not trust that the folder looks right from root only. Test it as the web server user.

BASH
ls -ld /var/www/moodledata
ls -la /var/www/moodledata | head
sudo -u www-data ls /var/www/moodledata | head

If www-data cannot read it, Moodle will break even if the folder exists.

Step 9: Configure Nginx

NGINX
server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;

    root /var/www/yourdomain.com;
    index index.php index.html index.htm;

    client_max_body_size 512M;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ [^/]\.php(/|$) {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.0-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        try_files $uri /index.php?$query_string;
        expires 7d;
        access_log off;
    }

    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    location ~* \.(htaccess|git|env|ini|log|sh|sql)$ {
        deny all;
    }
}

Enable the site and reload:

BASH
rm -f /etc/nginx/sites-enabled/default
ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/yourdomain.com
nginx -t
systemctl reload nginx

Step 10: Configure Cron

BASH
crontab -l > /tmp/currentcron 2>/dev/null || true
echo "*/1 * * * * /usr/bin/php8.0 /var/www/yourdomain.com/admin/cli/cron.php >/dev/null 2>&1" >> /tmp/currentcron
crontab /tmp/currentcron
rm -f /tmp/currentcron
crontab -l

Step 11: Use CLI to Validate the Install

This is one of the safest sanity checks after restoring the site.

BASH
php /var/www/yourdomain.com/admin/cli/purge_caches.php

If this fails, stop and fix the environment before browsing around the site.

In this migration, that command helped catch the bad overridden dataroot path.

The Redirect Gotcha: Why We Saw a 303 Then a Broken Site

Server logs showed the new VPS was being hit and returning:

TXT
303

That pointed to a redirect issue.

The reason was simple:

  • config.php had https://yourdomain.com
  • Nginx was only set up on port 80
  • SSL was not fully configured yet

So Moodle redirected HTTP to HTTPS, but HTTPS was not ready.

For testing, there are two clean options:

  1. temporarily use http:// in $CFG->wwwroot
  2. finish SSL first, then keep https://

If you are mid-migration and just trying to validate the build privately, temporary HTTP is often faster.

Biggest Migration Problems We Hit

1) Disk exhaustion during restore

Root cause:

  • storing zip
  • storing extracted source
  • storing copied destination

Fix:

  • extract directly into final destination
  • delete archives as soon as safe

2) config.php had duplicate settings

Root cause:

  • old values from the source server remained lower in the file
  • PHP used the last values, not the first edited ones

Fix:

  • read the whole file
  • remove duplicate wwwroot, dataroot, and related settings

3) Moodle CLI reported bad dataroot

Root cause:

  • old dataroot path still overriding the new one

Fix:

  • clean config.php
  • confirm www-data can actually read /var/www/moodledata

4) Browser tests were misleading

At one point, the browser showed a 503 page that looked like the old host. The fix was to confirm the request path using Nginx logs instead of guessing.

Use this:

BASH
tail -n 50 /var/log/nginx/error.log
tail -n 50 /var/log/nginx/access.log

That tells you whether your browser is hitting the new VPS or not.

Zero-Drama Cutover Advice

For a live Moodle, this is the safer move:

  1. build the new server privately
  2. validate DB, code, moodledata, permissions, and cron
  3. test with hosts-file override before public DNS cutover
  4. issue SSL
  5. only then point public DNS

That keeps the risky debugging away from the live traffic path.

Short Command Reference

Database dump

BASH
mysqldump -u USER -p DBNAME > moodle-db.sql

Direct transfer

BASH
scp ~/moodle-db.sql ~/moodle-files.zip ~/moodledata.zip root@SERVER_IP:/root/

Restore code

BASH
cd /var/www
unzip -q /root/moodle-files.zip -d /var/www/

Restore moodledata

BASH
cd /var/www
unzip -q /root/moodledata.zip -d /var/www/

Fix ownership

BASH
chown -R www-data:www-data /var/www/yourdomain.com
chown -R www-data:www-data /var/www/moodledata

Fix permissions

BASH
find /var/www/yourdomain.com -type f -exec chmod 644 {} \;
find /var/www/yourdomain.com -type d -exec chmod 755 {} \;
find /var/www/moodledata -type d -exec chmod 770 {} \;
find /var/www/moodledata -type f -exec chmod 660 {} \;

Purge caches

BASH
php /var/www/yourdomain.com/admin/cli/purge_caches.php

What breaks most Moodle VPS migrations?

The most common failure points are wrong PHP version, stale config.php paths, bad moodledata permissions, and trying to restore large moodledata archives in a way that duplicates disk usage.

What is the safest way to restore moodledata on a smaller VPS disk?

Extract it directly into the final destination instead of unzipping to one location and copying it again.

Why can Moodle still fail after config.php looks correct?

Because duplicate values lower in the file can override the values you just edited. Read the entire file, not just the top.

Final Take

This move was not difficult because Moodle is complex. It was difficult because infrastructure details matter, and Moodle exposes every mismatch fast.

That is exactly why the best migration strategy is not speed. It is sequencing.

Correct PHP version first.
Correct paths second.
Correct data restore method third.
Correct config cleanup fourth.
Then verify through CLI before browser testing.

That order saves hours.

If you want, I can also turn this into a second short checklist version for your /tutorials/ section.

Keep the Thread Going

Continue Reading

Keep moving from insight to action

Use the next article, service, or case study to keep building the thread instead of bouncing back to the index.

Need a deeper implementation guide?