How to Deploy Laravel: Zero Downtime, Build Pipelines, and Best Practices

Devops & Infrastructure, PHP, and Tutorials

How to Deploy Laravel: Zero Downtime, Build Pipelines, and Best Practices

Laravel is the most widely used PHP framework, but deploying it involves more than copying files to a server. A typical Laravel deployment requires installing Composer dependencies, compiling frontend assets with Vite, running database migrations, clearing caches, restarting queue workers, and managing environment variables — all without interrupting users.

Get any of those steps wrong and you end up with a white screen, missing assets, or a queue processing stale code.

This guide covers the full Laravel deployment lifecycle: what the build process actually involves, how to choose between deployment tools, how to achieve zero-downtime releases, and the most common deployment failures and how to fix them.

What Laravel Deployment Actually Involves

Laravel is not a static site you can deploy by uploading files. A production-ready deployment typically runs through these steps:

The Build Process

# 1. Install PHP dependencies (without dev packages)
composer install --no-dev --optimize-autoloader --no-interaction

# 2. Install and build frontend assets
npm ci
npm run build

# 3. Cache configuration, routes, views, and events
php artisan optimize

# 4. Run database migrations
php artisan migrate --force

# 5. Create the storage symlink
php artisan storage:link

# 6. Restart queue workers (so they pick up new code)
php artisan queue:restart

Each step matters:

  • --no-dev excludes testing packages from production, reducing the vendor directory size
  • npm run build compiles your Vite or Mix assets into the public/build/ directory
  • php artisan optimize is the unified caching command introduced in Laravel 11 — it replaces the separate config:cache, route:cache, view:cache, and event:cache commands
  • --force is required for migrations in production (Laravel refuses to run migrations without it outside local environments)
  • queue:restart signals long-running queue workers to finish their current job and restart, picking up the new code

Server Requirements for Laravel 12

  • PHP 8.2 or higher
  • Required extensions: Ctype, cURL, DOM, Fileinfo, Filter, Hash, Mbstring, OpenSSL, PCRE, PDO, Session, Tokenizer, XML
  • Web server (Nginx or Apache) configured to serve from the public/ directory
  • Database: MySQL 8.0+, PostgreSQL 10+, SQLite 3.35+, or SQL Server 2017+
  • Write permissions on storage/ and bootstrap/cache/

Environment Configuration

Laravel uses a .env file for environment-specific settings. This file must never be committed to version control — it contains your application key, database credentials, queue and cache driver configuration, and third-party API keys.

Key production settings:

APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:your-generated-key
APP_URL=https://yourdomain.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=your_database
DB_USERNAME=your_user
DB_PASSWORD=your_password

CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis

Setting APP_DEBUG=false is critical — with debug mode enabled, Laravel exposes full stack traces, environment variables, and database credentials to anyone who triggers an error.

Choosing Your Deployment Strategy

There are several ways to deploy Laravel, from manual SSH commands to fully managed platforms. The right choice depends on whether you already have servers, how much infrastructure you want to manage, and what other tools you use alongside Laravel.

Comparison Table

Feature Manual SSH Laravel Forge Laravel Cloud DeployHQ Deployer
Server provisioning No Yes Yes (managed) No No
Build pipeline No Basic scripts Yes Yes (remote servers) No
Zero-downtime releases No Yes (Envoyer add-on) Yes Yes (atomic symlinks) Yes
One-click rollback No No Yes Yes Yes
Multi-server deploy No No No Yes Yes
Non-PHP project support N/A Limited (Node, etc.) No Yes (any stack) No
Web dashboard No Yes Yes Yes No (CLI only)
Open source N/A No No No Yes
Cost Free $12-39/mo Usage-based €9-99/mo Free

Manual Deployment (SSH + Git Pull)

The simplest approach: SSH into your server, pull the latest code, and run the build commands manually.

ssh user@yourserver.com
cd /var/www/myapp
git pull origin main
composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan migrate --force
php artisan optimize
php artisan queue:restart

When it works: Solo developer, single server, deploying infrequently.

When it breaks: Teams (who ran the commands last?), multiple servers (repeat everything N times), urgent rollbacks (which files changed?), and Friday afternoon deployments (one wrong command and you are debugging into the evening).

Laravel Forge

Forge provisions and manages servers on DigitalOcean, Hetzner, AWS, or other providers. It handles Nginx configuration, SSL, database setup, and push-to-deploy with customisable deploy scripts.

Best for: Teams that want server management bundled with deployment. Forge handles everything from provisioning to SSL renewal.

Limitations: Deploy scripts run on the production server (no separate build pipeline). Zero-downtime requires the separate Envoyer product ($12/mo extra). Limited to PHP/Node stacks.

Laravel Cloud

Laravel Cloud is fully managed hosting — zero server management, automatic scaling, push-to-deploy from GitHub, and SOC 2 compliance. Launched February 2025.

Best for: Teams that want zero infrastructure management and are building exclusively with Laravel.

Limitations: Vendor lock-in to Laravel's infrastructure. Usage-based pricing that can be hard to predict. Less control over server configuration. Laravel-only.

DeployHQ

DeployHQ is a deployment tool that connects your Git repository to your servers. Build commands run on DeployHQ's infrastructure (not on your production server), and files are deployed via SSH, SFTP, FTP, or S3.

Best for: Teams that already have servers (from any provider) and want automated deployment with build pipelines, zero-downtime releases, and multi-server support. Also ideal when you deploy Laravel alongside other stacks (Node.js frontend, WordPress marketing site, Python microservice).

Key advantage: Build pipeline runs remotely. Your production server does not need Node.js or npm installed — DeployHQ compiles your Vite assets and installs Composer dependencies on its build servers, then deploys only the production-ready files.

For a step-by-step setup walkthrough, see How to Deploy Laravel with DeployHQ.

Deployer

Deployer is a free, open-source PHP deployment tool with a dedicated Laravel recipe that includes 40+ artisan tasks, zero-downtime via symlinks, parallel deployment, and rollbacks.

Best for: Developers who want full control, are comfortable with CLI tools, and prefer open-source solutions.

Limitations: No web dashboard, no build pipeline (builds run on your machine or CI server), steeper learning curve.

For a detailed comparison, see DeployHQ vs Deployer.

Forge + DeployHQ Together

This combination is worth highlighting because it addresses a common misconception: Forge and DeployHQ are not competitors. They solve different problems.

  • Forge manages your server: provisioning, Nginx config, SSL, database, PHP version, firewall rules
  • DeployHQ manages your deployment pipeline: build steps, file transfer, zero-downtime release, rollback, notifications

Using both means Forge handles the infrastructure while DeployHQ handles the deployment workflow — with remote build pipelines, one-click rollback, multi-server deployment, and team notifications that Forge's built-in deploy scripts do not offer.

For a detailed comparison, see DeployHQ vs Laravel Forge: How They Differ and How to Use Them Together.

Zero-Downtime Laravel Deployments

Standard deployments cause a brief period of downtime while files are being uploaded and commands are running. For high-traffic applications, this is unacceptable. Zero-downtime deployment eliminates this entirely.

How Atomic Releases Work

Instead of overwriting files in place, atomic releases use a directory structure with a symlink swap:

/var/www/myapp/
├── releases/
│   ├── 20260301120000/    ← previous release
│   ├── 20260302150000/    ← current release (live)
│   └── 20260303090000/    ← new release (being prepared)
├── current → releases/20260302150000/    ← symlink
├── shared/
│   ├── storage/           ← persists across releases
│   └── .env               ← persists across releases

The deployment process:

flowchart TD
    A[Clone new release] --> B[Install dependencies]
    B --> C[Build assets]
    C --> D[Run migrations]
    D --> E[Swap symlink]
    E --> F[Live traffic served]
    style E fill:#1abc9c,color:#fff
  1. A new release directory is created
  2. Code is deployed into this directory
  3. Dependencies are installed and assets are built
  4. Shared directories (storage, .env) are symlinked into the new release
  5. Database migrations run against the new release
  6. The current symlink is atomically swapped to point to the new release
  7. If anything fails before the swap, current still points to the previous working release

The symlink swap is an atomic filesystem operation — there is no moment where the application is in a broken state.

Shared Files and Directories

Some files and directories must persist across releases:

  • .env — environment configuration (database credentials, API keys)
  • storage/ — logs, uploaded files, cache, sessions
  • bootstrap/cache/ — sometimes shared depending on your setup

DeployHQ handles this automatically when you enable zero-downtime deployments — you configure which paths are shared, and the symlinks are managed for you.

Instant Rollback

With atomic releases, rolling back means changing the current symlink to point at the previous release directory. This takes milliseconds. No files are modified, no git reverts, no partial states.

In DeployHQ, rollback is a single click in the deployment history.

Laravel Deployment Checklist

Use this checklist before every production deployment:

  • [ ] APP_ENV=production and APP_DEBUG=false are set
  • [ ] APP_KEY is generated and set (never commit this to Git)
  • [ ] composer install --no-dev --optimize-autoloader runs without errors
  • [ ] Frontend assets compile with npm run build — check public/build/ exists
  • [ ] Database migrations tested in staging first
  • [ ] php artisan optimize runs after deployment (caches config, routes, views, events)
  • [ ] Queue workers restart after deployment (php artisan queue:restart)
  • [ ] storage/ and bootstrap/cache/ have correct write permissions
  • [ ] Nginx or Apache serves from the public/ directory only
  • [ ] .env, .git/, and storage/ are not web-accessible
  • [ ] Health check endpoint (/up) responds correctly
  • [ ] Deployment notifications configured (Slack, Discord, or email)
  • [ ] Zero-downtime deployments enabled for production
  • [ ] At least 3-4 previous releases kept for rollback
  • [ ] Queue workers monitored with Supervisor or Laravel Horizon

Troubleshooting Common Deployment Issues

500 Server Error After Deployment

The most common cause: missing .env file or APP_KEY. Laravel cannot boot without an application key.

Check first:

  1. Does .env exist in the deployment directory?
  2. Is APP_KEY set? Run php artisan key:generate --show to verify.
  3. Clear cached config: php artisan config:clear && php artisan cache:clear
  4. Check storage/logs/laravel.log for the actual error message.
  5. Verify storage/ has write permissions: chmod -R 775 storage bootstrap/cache

Assets Not Loading (CSS/JS 404 Errors)

Frontend build did not run or the Vite manifest is missing.

Check:

  1. Does public/build/manifest.json exist? If not, npm run build did not complete.
  2. If using DeployHQ, verify the build command (npm ci && npm run build) is in your build pipeline.
  3. Check that APP_URL in .env matches your actual domain — Vite uses this to generate asset URLs.
  4. If assets load on HTTP but not HTTPS, set ASSET_URL=https://yourdomain.com in .env.

Database Migration Failures

Migrations failing in production while working locally.

Check:

  1. Did you include --force? Laravel refuses to migrate in production without it.
  2. Use php artisan migrate --pretend to preview SQL without executing.
  3. Never run destructive migrations (dropping columns or tables) without a rollback plan.
  4. For large tables, consider running schema changes during low-traffic periods.

Queue Workers Serving Old Code

After deployment, queue workers continue processing jobs with the old application code because they cache the application state in memory.

Fix: Add php artisan queue:restart to your post-deployment commands. This signals workers to finish their current job, then restart with the new code.

If using Laravel Horizon, restart Horizon instead:

php artisan horizon:terminate

Supervisor (or your process manager) will automatically restart it.

The public/storage symlink points to storage/app/public, but after a zero-downtime deployment, the path has changed because the release directory is new.

Fix: Configure storage as a shared directory in your zero-downtime settings. The symlink should point to the shared storage/ path, not the release-specific one. Then run php artisan storage:link in your post-deployment commands.

Getting Started

If you already have a server, the fastest path to automated Laravel deployment is:

  1. Sign up for DeployHQ (free trial, no credit card)
  2. Connect your Git repository
  3. Add your server (SSH or SFTP)
  4. Set up build commands: composer install --no-dev --optimize-autoloader && npm ci && npm run build
  5. Add post-deploy SSH commands: php artisan migrate --force && php artisan optimize && php artisan queue:restart
  6. Enable auto-deploy on push to your main branch
  7. Deploy

For a detailed walkthrough with screenshots, see How to Deploy Laravel with DeployHQ. For help choosing between Forge and DeployHQ, see DeployHQ vs Laravel Forge.


Questions about deploying Laravel? Reach out at support@deployhq.com or on Twitter/X.