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