Deployment Overview
We're deploying to a Ubuntu 22.04 VPS with:
- NGINX as reverse proxy
- PHP 8.3 with PHP-FPM
- MySQL 8.0
- Let's Encrypt SSL
- Supervisor for queue workers
- GitHub Actions for CI/CD
Step 1: Server Initial Setup
SSH into your server:
ssh root@your-server-ip
Update and install essential packages:
apt update && apt upgrade -y
apt install -y nginx mysql-server curl git unzip supervisor ufw
Configure Firewall
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable
Step 2: Install PHP 8.3
apt install -y software-properties-common
add-apt-repository ppa:ondrej/php -y
apt update
apt install -y php8.3 php8.3-fpm php8.3-cli php8.3-common \
php8.3-mysql php8.3-zip php8.3-gd php8.3-mbstring \
php8.3-curl php8.3-xml php8.3-bcmath php8.3-intl \
php8.3-redis
Verify installation:
php -v
# PHP 8.3.x
Step 3: Install Composer
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
chmod +x /usr/local/bin/composer
Step 4: Configure MySQL
Secure MySQL installation:
mysql_secure_installation
Create database and user:
mysql -u root -p
CREATE DATABASE elearning_production;
CREATE USER 'elearning'@'localhost' IDENTIFIED BY 'your_strong_password';
GRANT ALL PRIVILEGES ON elearning_production.* TO 'elearning'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Step 5: Configure NGINX
Create site configuration /etc/nginx/sites-available/elearning:
server {
listen 80;
listen [::]:80;
server_name elearning.yourdomain.com;
root /var/www/elearning/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
}
Enable site:
ln -s /etc/nginx/sites-available/elearning /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx
Step 6: Deploy Application
Create deploy user:
adduser deploy
usermod -aG www-data deploy
Create application directory:
mkdir -p /var/www/elearning
chown -R deploy:www-data /var/www/elearning
Switch to deploy user and clone:
su - deploy
cd /var/www/elearning
git clone https://github.com/yourusername/elearning-app.git .
Install dependencies:
composer install --optimize-autoloader --no-dev
npm install && npm run build
Configure environment:
cp .env.example .env
php artisan key:generate
Edit .env with production values:
APP_NAME="E-Learning"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://elearning.yourdomain.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=elearning_production
DB_USERNAME=elearning
DB_PASSWORD=your_strong_password
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
Run migrations and seeders:
php artisan migrate --force
php artisan db:seed --class=RolePermissionSeeder --force
Set permissions:
sudo chown -R deploy:www-data /var/www/elearning
sudo chmod -R 755 /var/www/elearning
sudo chmod -R 775 /var/www/elearning/storage
sudo chmod -R 775 /var/www/elearning/bootstrap/cache
Step 7: SSL with Certbot
apt install certbot python3-certbot-nginx -y
certbot --nginx -d elearning.yourdomain.com
Test auto-renewal:
certbot renew --dry-run
Step 8: Configure Queue Worker
Create Supervisor config /etc/supervisor/conf.d/elearning-worker.conf:
[program:elearning-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/elearning/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=deploy
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/elearning/storage/logs/worker.log
stopwaitsecs=3600
Start workers:
supervisorctl reread
supervisorctl update
supervisorctl start elearning-worker:*
Step 9: Optimize for Production
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan icons:cache
php artisan filament:cache-components
Step 10: Automated Deployment
Create deployment script deploy.sh:
#!/bin/bash
set -e
cd /var/www/elearning
echo "๐ Pulling latest changes..."
git pull origin main
echo "๐ฆ Installing dependencies..."
composer install --optimize-autoloader --no-dev
echo "๐จ Building assets..."
npm ci && npm run build
echo "๐ Running migrations..."
php artisan migrate --force
echo "๐งน Clearing caches..."
php artisan config:clear
php artisan route:clear
php artisan view:clear
echo "โจ Rebuilding caches..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan filament:cache-components
echo "๐ Restarting queue workers..."
php artisan queue:restart
echo "โ
Deployment complete!"
Make executable:
chmod +x deploy.sh
GitHub Actions Deployment
.github/workflows/deploy.yml:
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /var/www/elearning
./deploy.sh
Step 11: Migrate Data from Old System
Create migration script database/scripts/migrate_legacy.php:
<?php
// Import old students
$oldStudents = json_decode(file_get_contents('legacy_data/students.json'), true);
foreach ($oldStudents as $old) {
$user = User::create([
'name' => $old['nama_siswa'],
'email' => $old['email'] ?: $old['nis'] . '@school.local',
'password' => Hash::make('defaultpassword'),
]);
$user->assignRole('student');
// Map old ID for reference
DB::table('legacy_mappings')->insert([
'old_table' => 'siswa',
'old_id' => $old['id'],
'new_table' => 'users',
'new_id' => $user->id,
]);
}
// Import old exams
$oldExams = json_decode(file_get_contents('legacy_data/exams.json'), true);
foreach ($oldExams as $old) {
Exam::create([
'title' => $old['nama_ujian'],
'subject_id' => $subjectMapping[$old['mapel_id']],
'duration_minutes' => $old['durasi'],
'start_time' => $old['waktu_mulai'],
'end_time' => $old['waktu_selesai'],
'status' => 'closed', // Old exams are closed
]);
}
Monitoring & Maintenance
Health Check Endpoint
Add to routes/web.php:
Route::get('/health', function () {
return response()->json([
'status' => 'ok',
'timestamp' => now()->toIso8601String(),
'database' => DB::connection()->getPdo() ? 'connected' : 'error',
]);
});
Recommended Monitoring Tools
| Tool | Purpose |
|---|---|
| UptimeRobot | Uptime monitoring |
| Laravel Telescope | Debug & profiling |
| Sentry | Error tracking |
| Laravel Pulse | Real-time dashboard |
Summary
We've successfully deployed our e-learning platform:
- โ Ubuntu VPS with NGINX + PHP-FPM
- โ MySQL database with secure credentials
- โ SSL certificate with auto-renewal
- โ Queue workers with Supervisor
- โ Automated deployment with GitHub Actions
- โ Legacy data migration script
- โ Health monitoring endpoint
Series Conclusion
๐ Congratulations! We've completed the entire Laravel E-Learning rebuild journey:
| Part | Topic | Status |
|---|---|---|
| 1 | Introduction | โ |
| 2 | Setup Laravel 11 + Filament | โ |
| 3 | Database Schema | โ |
| 4 | Authentication & Roles | โ |
| 5 | Exam System | โ |
| 6 | Real-Time Features | โ |
| 7 | Testing | โ |
| 8 | Deployment | โ |
From a legacy Laravel 5.2 application to a modern, tested, production-ready platform!
What's Next?
Future enhancements planned:
- ๐ฑ Mobile app with React Native
- ๐ค AI-powered question generation
- ๐ Advanced analytics dashboard
- ๐ Multi-school tenancy
Have questions about this series? Get in touch or find me on GitHub.
๐ Resources:
