Skillversitas

Laravel on a MicroVM

Run a Laravel application in a MicroVM with Nginx, PHP-FPM, and MySQL/PostgreSQL. A production-ready setup for deployment.

Architecture

Client
  |
[MicroVM]
  |
[Nginx :80] -> [PHP-FPM :9000] -> [Laravel App]
                     |
              [MySQL/PostgreSQL]
              [Redis (optional)]

Preparing the Image

sudo mount -o loop rootfs.img /mnt/microvm

sudo chroot /mnt/microvm bash -c '
  apt update && apt install -y \
    nginx php8.2-fpm php8.2-cli php8.2-mbstring php8.2-xml \
    php8.2-curl php8.2-zip php8.2-gd php8.2-bcmath \
    php8.2-readline php8.2-redis php8.2-mysql php8.2-pgsql \
    php8.2-intl php8.2-dom \
    mysql-server \
    redis-server \
    composer \
    git

  # Create the Laravel project
  cd /var/www
  composer create-project laravel/laravel html
  chown -R www-data:www-data html
  chmod -R 775 html/storage
  chmod -R 775 html/bootstrap/cache
'

sudo umount /mnt/microvm

Configuring Nginx

cat > /etc/nginx/sites-available/laravel << 'LARAVELCONF'
server {
    listen 80;
    server_name yourdomain.com;
    root /var/www/html/public;
    index index.php;

    client_max_body_size 64M;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

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

    # PHP-FPM
    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 300;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
    }

    # Block access to sensitive files
    location ~ /\.(?!well-known).* {
        deny all;
    }

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Access to .env
    location ~ /\.env {
        deny all;
    }
}
LARAVELCONF

ln -s /etc/nginx/sites-available/laravel /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default

Configuring Laravel

.env

APP_NAME="Laravel MicroVM"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=http://yourdomain.com

LOG_CHANNEL=stack
LOG_LEVEL=error

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secure_password

CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=587

Database Config

// config/database.php (override via .env)
'mysql' => [
    'driver' => 'mysql',
    'host' => env('DB_HOST', '127.0.0.1'),
    'port' => env('DB_PORT', '3306'),
    'database' => env('DB_DATABASE', 'laravel'),
    'username' => env('DB_USERNAME', 'laravel'),
    'password' => env('DB_PASSWORD', ''),
    'charset' => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix' => '',
    'strict' => true,
    'engine' => null,
],

Running the MicroVM

qemu-system-x86_64 \
  -M microvm -enable-kvm -cpu host -m 1g -smp 4 \
  -kernel vmlinuz \
  -append "console=ttyS0 root=/dev/vda rw" \
  -drive id=rootfs,file=laravel-rootfs.img,format=raw,if=none,cache=none,aio=linux \
  -device virtio-blk-device,drive=rootfs \
  -netdev user,id=net0,hostfwd=tcp::8080-:80 \
  -device virtio-net-device,netdev=net0 \
  -nographic -serial stdio

Setting Up the Guest

# Start services
service mysql start
service redis-server start
service php8.2-fpm start
service nginx start

# Create the database
mysql -u root << 'SQL'
CREATE DATABASE laravel;
CREATE USER 'laravel'@'localhost' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON laravel.* TO 'laravel'@'localhost';
FLUSH PRIVILEGES;
SQL

# Setup Laravel
cd /var/www/html
cp .env.example .env
php artisan key:generate

# Edit .env (set DB credentials, APP_URL, etc.)
sed -i 's/DB_DATABASE=laravel/DB_DATABASE=laravel/' .env
sed -i 's/DB_USERNAME=laravel/DB_USERNAME=laravel/' .env
sed -i 's/DB_PASSWORD=/DB_PASSWORD=secure_password/' .env

# Migrate database
php artisan migrate --force

# Seed the database (optional)
php artisan db:seed

# Cache config
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

# Set permissions
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache

Queue Worker

# Run the queue worker
php artisan queue:work --sleep=3 --tries=3 --max-time=3600

# or with supervisor
cat > /etc/supervisor/conf.d/laravel-worker.conf << 'SUPERVISOR'
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/laravel-worker.log
SUPERVISOR

supervisorctl reread
supervisorctl update
supervisorctl start "laravel-worker:*"

Scheduler

# Add a cron entry for the Laravel scheduler
echo "* * * * * www-data php /var/www/html/artisan schedule:run >> /dev/null 2>&1" > /etc/cron.d/laravel

OPcache for Laravel

; /etc/php/8.2/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.revalidate_freq=0
opcache.save_comments=1
opcache.fast_shutdown=1
opcache.enable_cli=0
opcache.validate_timestamps=0

Redis Cache

# Cache configuration in .env
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

# or with the database driver
CACHE_DRIVER=database
SESSION_DRIVER=database

Deployment Script

#!/bin/bash
# Script: deploy-laravel.sh

set -e

APP_DIR="/var/www/html"
cd $APP_DIR

echo "Pulling latest code..."
git pull origin main

echo "Installing dependencies..."
composer install --no-dev --optimize-autoloader

echo "Running migrations..."
php artisan migrate --force

echo "Caching configuration..."
php artisan config:cache
php artisan route:cache
php artisan view:cache

echo "Setting permissions..."
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache

echo "Restarting services..."
service php8.2-fpm restart
service nginx restart

echo "Deployment complete!"

Monitoring

# Check the Laravel log
tail -f /var/www/html/storage/logs/laravel.log

# Check the queue status
php artisan queue:work --once

# Check the scheduler
php artisan schedule:list

# Check the cache status
php artisan cache:table

Troubleshooting

Permission denied on storage

chown -R www-data:www-data /var/www/html/storage
chown -R www-data:www-data /var/www/html/bootstrap/cache
chmod -R 775 /var/www/html/storage
chmod -R 775 /var/www/html/bootstrap/cache

500 Internal Server Error

# Check the error logs
tail /var/www/html/storage/logs/laravel.log
tail /var/log/nginx/error.log

# Check the PHP errors
php artisan tinker --execute="echo 'PHP OK';"

# Enable debug mode temporarily
# APP_DEBUG=true in .env

Database connection refused

# Check that MySQL is running
service mysql status

# Check the credentials
php artisan tinker
>>> config('database.connections.mysql')

OPcache stale

# Clear OPcache
php artisan opcache:clear

# or restart PHP-FPM
service php8.2-fpm restart