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-devexcludes testing packages from production, reducing the vendor directory sizenpm run buildcompiles your Vite or Mix assets into thepublic/build/directoryphp artisan optimizeis the unified caching command introduced in Laravel 11 — it replaces the separateconfig:cache,route:cache,view:cache, andevent:cachecommands--forceis required for migrations in production (Laravel refuses to run migrations without it outside local environments)queue:restartsignals 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/andbootstrap/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
- A new release directory is created
- Code is deployed into this directory
- Dependencies are installed and assets are built
- Shared directories (storage, .env) are symlinked into the new release
- Database migrations run against the new release
- The
currentsymlink is atomically swapped to point to the new release - If anything fails before the swap,
currentstill 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, sessionsbootstrap/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=productionandAPP_DEBUG=falseare set - [ ]
APP_KEYis generated and set (never commit this to Git) - [ ]
composer install --no-dev --optimize-autoloaderruns without errors - [ ] Frontend assets compile with
npm run build— checkpublic/build/exists - [ ] Database migrations tested in staging first
- [ ]
php artisan optimizeruns after deployment (caches config, routes, views, events) - [ ] Queue workers restart after deployment (
php artisan queue:restart) - [ ]
storage/andbootstrap/cache/have correct write permissions - [ ] Nginx or Apache serves from the
public/directory only - [ ]
.env,.git/, andstorage/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:
- Does
.envexist in the deployment directory? - Is
APP_KEYset? Runphp artisan key:generate --showto verify. - Clear cached config:
php artisan config:clear && php artisan cache:clear - Check
storage/logs/laravel.logfor the actual error message. - 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:
- Does
public/build/manifest.jsonexist? If not,npm run builddid not complete. - If using DeployHQ, verify the build command (
npm ci && npm run build) is in your build pipeline. - Check that
APP_URLin.envmatches your actual domain — Vite uses this to generate asset URLs. - If assets load on HTTP but not HTTPS, set
ASSET_URL=https://yourdomain.comin.env.
Database Migration Failures
Migrations failing in production while working locally.
Check:
- Did you include
--force? Laravel refuses to migrate in production without it. - Use
php artisan migrate --pretendto preview SQL without executing. - Never run destructive migrations (dropping columns or tables) without a rollback plan.
- 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.
Storage Symlink Not Working After Zero-Downtime Deploy
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:
- Sign up for DeployHQ (free trial, no credit card)
- Connect your Git repository
- Add your server (SSH or SFTP)
- Set up build commands:
composer install --no-dev --optimize-autoloader && npm ci && npm run build - Add post-deploy SSH commands:
php artisan migrate --force && php artisan optimize && php artisan queue:restart - Enable auto-deploy on push to your main branch
- 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.