1. Introduction: Choosing a Deployment Strategy
You've built your Laravel application — now you need to deploy it to production. This is where many developers get stuck. There are dozens of hosting options, and the wrong choice can cost you hours of debugging or hundreds of dollars in unnecessary infrastructure.
In this Laravel deployment guide, we'll cover the four most popular ways to deploy a Laravel 12 app in 2026, along with CI/CD automation, SSL setup, queue workers, and a production checklist you can follow for every launch.
Here's a quick comparison to help you decide:
- Laravel Forge — Best for most teams. Manages your own server on DigitalOcean, AWS, or Hetzner. $12/mo. Full control, easy setup.
- Laravel Cloud — Serverless, auto-scaling Laravel hosting by the Laravel team. Best for apps with variable traffic.
- VPS (manual) — Cheapest option. Full control, but you manage everything yourself. Good for learning.
- Docker — Best for teams with DevOps experience. Portable, reproducible environments. Works with any cloud provider.
2. Preparing Your Laravel App for Production
Before deploying anywhere, you need to optimize your Laravel app for production. These steps apply regardless of which hosting option you choose.
Environment Configuration
Your production .env should differ significantly from development:
APP_NAME="Your SaaS"
APP_ENV=production
APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
APP_DEBUG=false
APP_URL=https://yourdomain.com
LOG_CHANNEL=stack
LOG_LEVEL=warning
DB_CONNECTION=mysql
DB_HOST=your-db-host
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_user
DB_PASSWORD=your_secure_password
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
MAIL_MAILER=ses
MAIL_FROM_ADDRESS=hello@yourdomain.com
MAIL_FROM_NAME="${APP_NAME}"
Critical: Never set APP_DEBUG=true in production. It exposes your entire configuration, database credentials, and stack traces to anyone.
Performance Optimizations
Run these commands as part of every deployment:
# Cache configuration (merges all config files into one)
php artisan config:cache
# Cache routes (skips route registration on every request)
php artisan route:cache
# Cache views (pre-compiles all Blade templates)
php artisan view:cache
# Cache events (auto-discovers event listeners)
php artisan event:cache
# Install production-only Composer dependencies
composer install --no-dev --optimize-autoloader
These commands can reduce your response time by 50-100ms on every request by eliminating file reads and parsing on each boot.
3. Option A: Deploy with Laravel Forge (Recommended)
Laravel Forge is a server management tool built by the Laravel team. It provisions and configures Ubuntu servers on DigitalOcean, AWS, Hetzner, or any custom VPS provider. It's the most popular way to deploy Laravel apps, and for good reason.
Step 1: Create a Server
Sign up at forge.laravel.com, connect your cloud provider (DigitalOcean is the cheapest at $6/mo for a server), and create a new server. Forge automatically installs:
- PHP 8.3 with all required extensions
- Nginx as the web server
- MySQL 8 or PostgreSQL
- Redis for caching and queues
- Supervisor for queue workers
- Let's Encrypt SSL (free)
Step 2: Create a Site & Connect Your Repository
In Forge, create a new site with your domain name. Connect your GitHub repository and Forge will clone it to the server. Set your deploy script:
cd /home/forge/yourdomain.com
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
npm ci
npm run build
php artisan queue:restart
Step 3: Enable Auto-Deploy
Toggle "Auto Deploy" in Forge — every push to main triggers an automatic deployment. Forge also supports deploy hooks for Slack notifications, health checks, and custom scripts.
Total cost: Forge ($12/mo) + DigitalOcean Droplet ($6-24/mo) = $18-36/mo for a production-ready Laravel server.
4. Option B: Deploy with Laravel Cloud (Serverless)
Laravel Cloud is Laravel's official serverless hosting platform. It auto-scales your app based on traffic, handles infrastructure automatically, and you only pay for what you use.
# Install the Cloud CLI
composer global require laravel/cloud-cli
# Login to your Cloud account
cloud login
# Initialize your project
cloud init
# Deploy to production
cloud deploy production
Laravel Cloud reads your cloud.yaml configuration:
# cloud.yaml
name: my-saas
environments:
production:
build:
- composer install --no-dev --optimize-autoloader
- npm ci && npm run build
- php artisan config:cache
- php artisan route:cache
- php artisan view:cache
workers:
web:
command: php-fpm
scaling:
min: 1
max: 10
queue:
command: php artisan queue:work --tries=3
scaling:
min: 1
max: 5
schedule: true
database: mysql
cache: redis
Best for: Apps with unpredictable traffic (product launches, marketing campaigns) or teams that don't want to manage servers. Pricing is usage-based.
5. Option C: Deploy to a VPS Manually (Ubuntu + Nginx)
If you want full control and the lowest cost, you can deploy Laravel to a VPS manually. This is what Forge automates, but doing it yourself teaches you exactly how the stack works.
Step 1: Provision the Server
Get an Ubuntu 24.04 VPS from DigitalOcean, Hetzner, or Vultr. SSH in and install the stack:
# Update system
sudo apt update && sudo apt upgrade -y
# Install PHP 8.3 and extensions
sudo add-apt-repository ppa:ondrej/php -y
sudo apt install -y php8.3-fpm php8.3-cli php8.3-mysql \
php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip \
php8.3-gd php8.3-redis php8.3-bcmath php8.3-intl
# Install Nginx
sudo apt install -y nginx
# Install MySQL
sudo apt install -y mysql-server
sudo mysql_secure_installation
# Install Redis
sudo apt install -y redis-server
# Install Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
# Install Node.js 22
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
Step 2: Configure Nginx
Create an Nginx config for your Laravel app:
# /etc/nginx/sites-available/yourdomain.com
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
root /var/www/yourdomain.com/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.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;
}
}
Enable the site and restart Nginx:
sudo ln -s /etc/nginx/sites-available/yourdomain.com \
/etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
Step 3: Clone and Configure Your App
# Create the web directory
sudo mkdir -p /var/www/yourdomain.com
sudo chown -R $USER:www-data /var/www/yourdomain.com
# Clone your repo
git clone git@github.com:you/your-app.git /var/www/yourdomain.com
cd /var/www/yourdomain.com
# Install dependencies
composer install --no-dev --optimize-autoloader
npm ci && npm run build
# Set permissions
sudo chown -R $USER:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
# Configure environment
cp .env.example .env
php artisan key:generate
# Edit .env with your production settings
# Run migrations
php artisan migrate --force
# Cache everything
php artisan config:cache
php artisan route:cache
php artisan view:cache
6. Option D: Deploy with Docker & Docker Compose
Docker gives you reproducible, portable deployments. The same container runs identically on your laptop, CI, and production.
# Dockerfile
FROM php:8.3-fpm-alpine
# Install system dependencies
RUN apk add --no-cache \
nginx supervisor curl zip unzip git \
libpng-dev libjpeg-turbo-dev libwebp-dev \
icu-dev oniguruma-dev libzip-dev
# Install PHP extensions
RUN docker-php-ext-configure gd \
--with-jpeg --with-webp && \
docker-php-ext-install \
pdo_mysql mbstring exif pcntl bcmath \
gd intl zip opcache
# Install Redis extension
RUN pecl install redis && docker-php-ext-enable redis
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy application files
COPY . .
# Install dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Set permissions
RUN chown -R www-data:www-data storage bootstrap/cache
# Cache Laravel config
RUN php artisan config:cache && \
php artisan route:cache && \
php artisan view:cache
EXPOSE 9000
CMD ["php-fpm"]
Use Docker Compose to orchestrate all services:
# docker-compose.production.yml
services:
app:
build: .
restart: unless-stopped
volumes:
- storage:/var/www/html/storage
depends_on:
- mysql
- redis
environment:
- APP_ENV=production
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf
- ./public:/var/www/html/public
- certs:/etc/letsencrypt
depends_on:
- app
mysql:
image: mysql:8.0
restart: unless-stopped
volumes:
- mysql_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
redis:
image: redis:alpine
restart: unless-stopped
queue:
build: .
restart: unless-stopped
command: php artisan queue:work --tries=3 --timeout=90
depends_on:
- mysql
- redis
scheduler:
build: .
restart: unless-stopped
command: sh -c "while true; do php artisan schedule:run; sleep 60; done"
depends_on:
- mysql
- redis
volumes:
mysql_data:
storage:
certs:
Deploy with a single command:
docker compose -f docker-compose.production.yml up -d --build
7. CI/CD with GitHub Actions
Automate your testing and deployment with GitHub Actions. This workflow runs your tests on every push and deploys to production when you merge to main:
# .github/workflows/deploy.yml
name: Test & Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
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
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, mysql, redis
coverage: none
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: Run tests
run: php artisan test
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Deploy to Forge
uses: jbrooksuk/laravel-forge-action@v1
with:
trigger_url: ${{ secrets.FORGE_DEPLOY_URL }}
How it works: Every push to any branch runs the test suite. When tests pass and you push to main, it triggers a deployment via Forge's deploy webhook. No manual SSH needed.
8. SSL Certificates & Custom Domains
Every production app needs HTTPS. Here's how to set it up depending on your deployment method:
Laravel Forge
One click. Go to your site in Forge, click "SSL", and choose "Let's Encrypt". Forge installs the certificate and configures auto-renewal. Done.
Manual VPS with Certbot
# Install Certbot
sudo apt install -y certbot python3-certbot-nginx
# Get certificate (automatically configures Nginx)
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Verify auto-renewal
sudo certbot renew --dry-run
Certbot adds a cron job that automatically renews your certificate before it expires.
Docker with Traefik
If you're using Docker, consider adding Traefik as a reverse proxy. It handles SSL certificate issuance and renewal automatically with Let's Encrypt:
# Add to docker-compose.production.yml
traefik:
image: traefik:v3.0
command:
- "--providers.docker=true"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.le.acme.email=you@yourdomain.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- letsencrypt:/letsencrypt
9. Running Queues, Cron & Scheduler in Production
Most SaaS apps use background jobs for sending emails, processing webhooks, generating reports, and more. Here's how to run them reliably in production.
Queue Worker with Supervisor
Supervisor keeps your queue worker running and restarts it if it crashes:
# /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/yourdomain.com/artisan queue:work redis
--sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/yourdomain.com/storage/logs/worker.log
stopwaitsecs=3600
# Apply the configuration
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
Laravel Scheduler (Cron)
Add a single cron entry that runs every minute:
# Open crontab
crontab -e
# Add this line
* * * * * cd /var/www/yourdomain.com && php artisan schedule:run >> /dev/null 2>&1
All scheduled tasks defined in your routes/console.php or app/Console/Kernel.php will run at their configured intervals. No need for multiple cron entries.
Important: After every deployment, restart queue workers so they pick up the new code:
php artisan queue:restart
10. Zero-Downtime Deployments
Nobody wants their SaaS to show errors during deployment. Zero-downtime deployment means your users never see an interruption.
With Laravel Forge
Forge supports zero-downtime deployments natively. Enable it in your site settings — Forge creates a new release directory, builds everything there, then swaps the symlink atomically.
With Deployer (Manual VPS)
Deployer is a PHP deployment tool that implements the release-directory pattern:
# Install Deployer
composer global require deployer/deployer
# deploy.php
namespace Deployer;
require 'recipe/laravel.php';
host('yourdomain.com')
->set('remote_user', 'forge')
->set('deploy_path', '/var/www/yourdomain.com');
set('repository', 'git@github.com:you/your-app.git');
after('deploy:failed', 'deploy:unlock');
// Deploy
// dep deploy production
Deployer creates versioned release directories, runs your build process in the new directory, and only switches the current symlink when everything succeeds. If the deployment fails, the old version keeps running.
11. Monitoring, Logging & Error Tracking
You can't fix what you can't see. Set up monitoring from day one:
- Error tracking: Use Sentry, Flare, or Bugsnag to catch exceptions in real-time. Install with
composer require sentry/sentry-laraveland add your DSN to.env. - Uptime monitoring: Use Oh Dear, UptimeRobot, or Better Uptime to get alerted when your site goes down.
- Application performance: Laravel Telescope for development, Laravel Pulse for production. Pulse gives you a real-time dashboard of slow queries, cache hits, queue throughput, and more.
- Server monitoring: Forge includes basic server metrics. For deeper monitoring, use Netdata or the DigitalOcean/AWS monitoring dashboards.
- Log aggregation: Ship logs to Papertrail, Logtail, or Datadog for centralized search and alerting.
At minimum, configure structured logging so you can search and filter your logs:
// config/logging.php — use the 'stack' channel
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'stderr'],
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'warning',
'days' => 14,
],
12. Production Deployment Checklist
Run through this checklist before every launch:
- APP_DEBUG=false — Never expose debug info in production.
- APP_KEY is set — Run
php artisan key:generateif not. - HTTPS is enabled — SSL certificate installed and force-HTTPS configured.
- Database migrations run —
php artisan migrate --force. - Caches are warm — Config, routes, views, and events are cached.
- Queue worker is running — Supervisor is configured and active.
- Scheduler cron is set —
schedule:runruns every minute. - Error tracking is live — Sentry/Flare is configured and tested.
- Uptime monitoring is active — You'll get alerts if the site goes down.
- Backups are scheduled — Database backups run daily with
spatie/laravel-backup. - Stripe webhooks point to production — Update the endpoint URL and webhook secret.
- Email sending works — Test transactional emails (welcome, password reset, invoices).
- DNS is configured — A record or CNAME points to your server's IP.
- Storage symlink exists —
php artisan storage:link.
13. Conclusion
You now know how to deploy a Laravel application to production using four different methods: Laravel Forge, Laravel Cloud, a manual VPS, and Docker. You also have CI/CD automation with GitHub Actions, zero-downtime deployments, and a production checklist.
For most SaaS applications, we recommend Laravel Forge + DigitalOcean for simplicity and cost-effectiveness. If you need auto-scaling, go with Laravel Cloud. If you have DevOps expertise, Docker gives you maximum portability.
LaraSpeed is deployment-ready out of the box. It ships with a production Dockerfile, Nginx config, CI/CD workflow, queue worker configuration, and all the optimizations covered in this guide. You just connect your server and deploy.
Ship your SaaS today, not next month
LaraSpeed gives you a production-ready Laravel SaaS with authentication, billing, teams, admin panel, deployment configs — everything wired together and ready to launch.
Get LaraSpeed — Starting at $49