Deploying a Laravel application to production doesn’t have to be a nightmare. Whether you’re launching your first Laravel project or scaling to millions of users, this comprehensive guide will walk you through everything you need to know about Laravel deployment in 2025.
We’ll cover manual deployment, automated CI/CD pipelines, zero-downtime strategies, and production optimization techniques that have helped companies reduce deployment time from hours to minutes while maintaining 99.99% uptime.
Why Deployment Strategy Matters
Poor deployment practices can turn a successful development project into a production disaster. We’ve seen companies lose thousands of dollars per minute due to botched deployments, while others seamlessly push updates multiple times per day without users noticing.
The difference? A solid deployment strategy that prioritizes automation, testing, and rollback capabilities. This guide will help you build that strategy from the ground up using Ubuntu servers and industry best practices.
Looking for managed Laravel hosting? CloudPloy’s Laravel deployment automation handles all the complexity below automatically, including zero-downtime deployments, security hardening, and performance optimization. For performance tips after deployment, check out our Laravel Performance Optimization Guide.
Prerequisites and Preparation
Before diving into deployment, ensure your Laravel application is production-ready:
Environment Configuration
Your production environment variables should be properly configured:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
LOG_CHANNEL=daily
LOG_LEVEL=error
DB_CONNECTION=mysql
DB_HOST=your-db-host
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_username
DB_PASSWORD=your_secure_password
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
Security Preparations
Never deploy without these security essentials:
# Generate a fresh application key
php artisan key:generate
# Ensure .env is in .gitignore
echo ".env" >> .gitignore
# Set proper file permissions
find . -type f -exec chmod 644 {} \;
find . -type d -exec chmod 755 {} \;
chmod -R 775 storage bootstrap/cache
Optimization Commands
Run these optimization commands before deployment to significantly improve performance:
# Cache configuration
php artisan config:cache
# Cache routes (only if not using closures)
php artisan route:cache
# Cache views
php artisan view:cache
# Optimize autoloader
composer install --optimize-autoloader --no-dev
# Combined optimization
php artisan optimize
Server Requirements and Setup
PHP Requirements for Laravel 11/12
Laravel 12 requires PHP 8.2 or higher with these extensions:
# Required PHP extensions
php8.2-bcmath
php8.2-ctype
php8.2-curl
php8.2-dom
php8.2-fileinfo
php8.2-json
php8.2-mbstring
php8.2-openssl
php8.2-pcre
php8.2-pdo
php8.2-tokenizer
php8.2-xml
php8.2-mysql # or your database driver
php8.2-redis # if using Redis
Recommended Server Specifications
Based on real-world deployments, here are recommended specifications:
Small Applications (< 1,000 daily users):
- 2 vCPUs
- 2GB RAM
- 20GB SSD storage
- 1TB bandwidth
Medium Applications (1,000 - 10,000 daily users):
- 4 vCPUs
- 8GB RAM
- 50GB SSD storage
- 5TB bandwidth
Large Applications (10,000+ daily users):
- 8+ vCPUs
- 16GB+ RAM
- 100GB+ SSD storage
- Load balancer with multiple instances
Deployment Methods Comparison
Manual Deployment via SSH
The traditional approach - simple but error-prone:
# SSH into your server
ssh user@yourserver.com
# Navigate to project directory
cd /var/www/yourapp
# Pull latest changes
git pull origin main
# Install dependencies
composer install --no-dev --optimize-autoloader
# Run migrations
php artisan migrate --force
# Clear and rebuild caches
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Restart queue workers
php artisan queue:restart
Pros: Simple, direct control Cons: Manual process, no rollback, downtime during deployment
Automated CI/CD Deployment
Modern deployment with GitHub Actions:
name: Deploy Laravel Application
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install Dependencies
run: composer install
- name: Run Tests
run: php artisan test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Deploy to Server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/yourapp
git pull origin main
composer install --no-dev
php artisan migrate --force
php artisan config:cache
php artisan queue:restart
supervisorctl restart all
Step-by-Step AWS EC2 Deployment
Let’s deploy Laravel to AWS EC2 from scratch:
1. Launch EC2 Instance
Choose Ubuntu 22.04 LTS with at least t3.small instance type for production.
2. Initial Server Setup
# Connect to your instance
ssh -i your-key.pem ubuntu@your-ec2-ip
# Update system packages
sudo apt update && sudo apt upgrade -y
# Install required packages
sudo apt install -y nginx php8.2-fpm php8.2-mysql php8.2-mbstring \
php8.2-xml php8.2-bcmath php8.2-curl php8.2-zip php8.2-redis \
mysql-client composer git supervisor redis-server
3. Configure Nginx
Create your Nginx configuration:
# /etc/nginx/sites-available/yourapp
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
root /var/www/yourapp/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
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.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
4. Deploy Your Application
# Create web directory
sudo mkdir -p /var/www
cd /var/www
# Clone your repository
sudo git clone https://github.com/yourusername/yourapp.git
cd yourapp
# Set permissions
sudo chown -R www-data:www-data /var/www/yourapp
sudo chmod -R 755 /var/www/yourapp
sudo chmod -R 775 storage bootstrap/cache
# Install dependencies
composer install --no-dev --optimize-autoloader
# Copy and configure .env
cp .env.example .env
php artisan key:generate
# Edit .env with your production values
nano .env
# Run migrations
php artisan migrate --force
# Enable site
sudo ln -s /etc/nginx/sites-available/yourapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
5. Configure SSL with Let’s Encrypt
# Install Certbot
sudo apt install certbot python3-certbot-nginx -y
# Obtain SSL certificate
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Auto-renewal
sudo systemctl enable certbot.timer
Amazon Lightsail Deployment
Lightsail offers a simpler alternative to EC2 with predictable pricing:
Why Choose Lightsail for Laravel?
- Fixed monthly pricing ($3.50 - $160/month)
- Simplified networking and firewall management
- Built-in snapshots and backups
- Easy vertical scaling
- Integrated CDN and load balancing
Quick Lightsail Setup
# After creating a Lightsail instance with Ubuntu
# Install LAMP stack
sudo apt update
sudo apt install -y apache2 php8.2 libapache2-mod-php8.2 \
php8.2-mysql php8.2-common php8.2-cli php8.2-json \
php8.2-opcache php8.2-mbstring php8.2-xml php8.2-zip
# Enable Apache modules
sudo a2enmod rewrite
sudo a2enmod headers
# Configure Apache for Laravel
sudo tee /etc/apache2/sites-available/laravel.conf > /dev/null <<EOF
<VirtualHost *:80>
ServerName yourdomain.com
DocumentRoot /var/www/laravel/public
<Directory /var/www/laravel>
AllowOverride All
Require all granted
</Directory>
ErrorLog \${APACHE_LOG_DIR}/laravel-error.log
CustomLog \${APACHE_LOG_DIR}/laravel-access.log combined
</VirtualHost>
EOF
# Enable site and restart Apache
sudo a2ensite laravel.conf
sudo a2dissite 000-default.conf
sudo systemctl restart apache2
Database Deployment Strategies
Safe Migration Practices
Always test migrations before running in production:
# Preview migrations without executing
php artisan migrate --pretend
# Run migrations with force flag for production
php artisan migrate --force
# Rollback if needed (have backups!)
php artisan migrate:rollback --step=1
Database Optimization for Production
-- Add indexes for frequently queried columns
ALTER TABLE users ADD INDEX idx_email (email);
ALTER TABLE posts ADD INDEX idx_user_created (user_id, created_at);
-- Optimize tables periodically
OPTIMIZE TABLE users, posts, comments;
-- Enable query cache
SET GLOBAL query_cache_size = 268435456;
SET GLOBAL query_cache_type = 1;
Automated Backup Strategy
#!/bin/bash
# backup.sh - Run daily via cron
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DB_NAME="your_database"
BACKUP_DIR="/backups/mysql"
# Create backup
mysqldump -u root -p$DB_PASSWORD $DB_NAME | gzip > $BACKUP_DIR/backup_$TIMESTAMP.sql.gz
# Keep only last 30 days of backups
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +30 -delete
# Upload to S3 (optional)
aws s3 cp $BACKUP_DIR/backup_$TIMESTAMP.sql.gz s3://your-backup-bucket/
Queue Worker Management
Supervisor Configuration
Ensure your queue workers stay running in production:
# /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/yourapp/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=8
redirect_stderr=true
stdout_logfile=/var/www/yourapp/storage/logs/worker.log
stopwaitsecs=3600
Apply the configuration:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
Laravel Horizon for Advanced Queue Management
For Redis-based queues, Horizon provides a beautiful dashboard:
composer require laravel/horizon
php artisan horizon:install
php artisan horizon:publish
Configure Horizon supervisor:
[program:horizon]
process_name=%(program_name)s
command=php /var/www/yourapp/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/yourapp/storage/logs/horizon.log
stopwaitsecs=3600
Zero-Downtime Deployment
Atomic Deployment Strategy
Deploy without any downtime using symbolic links:
#!/bin/bash
# deploy.sh - Zero-downtime deployment script
REPO_URL="git@github.com:youruser/yourapp.git"
RELEASES_DIR="/var/www/releases"
CURRENT_DIR="/var/www/current"
SHARED_DIR="/var/www/shared"
NEW_RELEASE_DIR="$RELEASES_DIR/$(date +%Y%m%d%H%M%S)"
# Clone new release
git clone $REPO_URL $NEW_RELEASE_DIR
cd $NEW_RELEASE_DIR
# Install dependencies
composer install --no-dev --optimize-autoloader
# Link shared files and directories
ln -nfs $SHARED_DIR/.env $NEW_RELEASE_DIR/.env
ln -nfs $SHARED_DIR/storage $NEW_RELEASE_DIR/storage
# Run Laravel commands
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Atomic switch
ln -nfs $NEW_RELEASE_DIR $CURRENT_DIR
# Restart services
php artisan queue:restart
sudo supervisorctl restart all
# Clean up old releases (keep last 5)
cd $RELEASES_DIR
ls -t | tail -n +6 | xargs rm -rf
echo "Deployment completed successfully!"
Blue-Green Deployment
For even safer deployments, use blue-green strategy with load balancer:
# nginx load balancer configuration
upstream laravel_app {
# Blue environment
server 10.0.1.10:80 weight=100;
# Green environment (standby)
server 10.0.1.11:80 weight=0;
}
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://laravel_app;
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;
}
}
CI/CD Pipeline with GitHub Actions
Complete Production Pipeline
name: Laravel Production Deployment
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
PHP_VERSION: '8.2'
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
redis:
image: redis:alpine
ports:
- 6379:6379
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
extensions: mbstring, dom, fileinfo, mysql, redis
coverage: xdebug
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install Dependencies
run: composer install --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Generate key
run: php artisan key:generate
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Run Tests
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
run: |
php artisan migrate --force
php artisan test --parallel
- name: Run Static Analysis
run: ./vendor/bin/phpstan analyse
- name: Run Code Style Check
run: ./vendor/bin/pint --test
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to Production
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
script: |
cd /var/www/yourapp
# Backup current version
cp -r . ../backup_$(date +%Y%m%d_%H%M%S)
# Pull latest changes
git pull origin main
# Install dependencies
composer install --no-dev --optimize-autoloader
# Run migrations
php artisan migrate --force
# Clear and optimize caches
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize
# Restart queue workers
php artisan queue:restart
# Restart Horizon if using
php artisan horizon:terminate
# Reload PHP-FPM
sudo service php8.2-fpm reload
- name: Verify Deployment
run: |
response=$(curl -s -o /dev/null -w "%{http_code}" https://yourdomain.com/health)
if [ $response -eq 200 ]; then
echo "Deployment successful!"
else
echo "Deployment verification failed!"
exit 1
fi
- name: Notify Slack
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Production deployment ${{ job.status }}'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
Performance Optimization for Production
PHP-FPM Tuning
Optimize PHP-FPM for your server resources:
; /etc/php/8.2/fpm/pool.d/www.conf
[www]
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500
; Process idle timeout
pm.process_idle_timeout = 10s
; Request timeout
request_terminate_timeout = 30s
; Slow log
slowlog = /var/log/php8.2-fpm-slow.log
request_slowlog_timeout = 5s
OPcache Configuration
Enable and configure OPcache for massive performance gains:
; /etc/php/8.2/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.fast_shutdown=1
Redis Configuration for Caching
// config/database.php
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'persistent' => true,
'persistent_id' => 'laravel',
'read_timeout' => 60,
],
'cache' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
],
Monitoring and Logging
Application Monitoring Setup
Implement comprehensive monitoring:
// app/Http/Middleware/PerformanceMonitoring.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
class PerformanceMonitoring
{
public function handle($request, Closure $next)
{
$start = microtime(true);
$response = $next($request);
$duration = microtime(true) - $start;
if ($duration > 1) { // Log slow requests
Log::warning('Slow request detected', [
'url' => $request->fullUrl(),
'method' => $request->method(),
'duration' => $duration,
'ip' => $request->ip(),
]);
}
return $response;
}
}
Health Check Endpoint
// routes/web.php
Route::get('/health', function () {
try {
// Check database
DB::connection()->getPdo();
// Check Redis
Redis::ping();
// Check storage
Storage::disk('local')->exists('test');
return response()->json([
'status' => 'healthy',
'timestamp' => now(),
'checks' => [
'database' => 'connected',
'cache' => 'connected',
'storage' => 'accessible',
]
]);
} catch (\Exception $e) {
return response()->json([
'status' => 'unhealthy',
'error' => $e->getMessage()
], 503);
}
});
Security Best Practices
Essential Security Headers
# Add to your Nginx server block
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';";
Firewall Configuration
# UFW firewall setup
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable
Troubleshooting Common Deployment Issues
Permission Problems
# Fix storage permissions
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
# Fix all permissions
sudo find . -type f -exec chmod 644 {} \;
sudo find . -type d -exec chmod 755 {} \;
Memory Issues
// Increase memory limit for specific operations
ini_set('memory_limit', '512M');
// Or in PHP-FPM config
memory_limit = 512M
Queue Processing Failures
# Check failed jobs
php artisan queue:failed
# Retry all failed jobs
php artisan queue:retry all
# Clear failed jobs
php artisan queue:flush
The Future of Laravel Deployment
As Laravel continues to evolve, deployment strategies are becoming increasingly sophisticated. Container orchestration with Kubernetes, serverless deployments with Laravel Vapor, and automation tools are changing how we think about deployment.
Modern deployment practices focus on simplifying the entire process by providing automated deployment pipelines with automatic scaling, zero-downtime deployments, and built-in optimization - all while maintaining control over your infrastructure.
Conclusion
Deploying Laravel to production doesn’t have to be complex or risky. By following this guide, you’ve learned:
- How to prepare your application for production
- Multiple deployment strategies from manual to fully automated
- Zero-downtime deployment techniques
- Performance optimization for production environments
- Monitoring and security best practices
Remember, the best deployment strategy is the one that fits your team’s needs and expertise. Start simple, automate gradually, and always prioritize reliability over complexity.
By implementing the Ubuntu server deployment procedures outlined in this guide, you can deploy Laravel applications with confidence, ensuring they’re secure, scalable, and maintainable in production environments.