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

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.