Deploying Laravel on Vessl
Vessl detects Laravel automatically (by the presence of artisan in your repository root) and pre-fills sensible defaults for build, start, and post-deploy commands. This guide covers everything from the first deploy to production-level concerns like queues, storage, and scheduled tasks.
Prerequisites
- A server added and provisioned in Vessl (Ubuntu, any size)
- A GitHub repository with your Laravel app
- PHP 8.1+ and Composer in your
composer.json
What Vessl auto-detects
When Vessl sees artisan in your repo root it sets:
| Setting | Auto-filled value |
|---|---|
| Build engine | Nixpacks |
| Port | 8000 |
| Install command | composer install (+ npm ci if package.json is present) |
| Build command | php artisan migrate --force |
| Start command | php artisan serve --host=0.0.0.0 --port=${PORT:-8000} |
| Post-deploy command | php artisan migrate --force |
| Volume | /app/storage (auto-mounted for persistent storage) |
Vessl also auto-injects these environment variables on first deploy (only if you haven't set them yourself):
| Variable | Value |
|---|---|
APP_KEY | Auto-generated base64:… key |
APP_ENV | production |
APP_URL | Your app's public URL (https://…) |
SESSION_DRIVER | cookie (falls back if no DB add-on is attached) |
CACHE_STORE | array (falls back if no DB add-on is attached) |
QUEUE_CONNECTION | sync (falls back if no DB add-on is attached) |
Once you attach a database add-on, Vessl sets the database-backed drivers automatically on the next deploy.
Step 1 — Create the app
- In your project, click New Application.
- Select your Laravel repository and branch.
- The wizard auto-detects Laravel — review the pre-filled settings and click Deploy.
The first deploy runs composer install, then php artisan migrate --force at build time, then starts php artisan serve. Storage is automatically mounted at /app/storage so uploaded files and logs survive restarts and redeploys.
Step 2 — Add a database
For any real app you'll want a persistent database. Open the app → Overview → Connected Services and click Add:
- Click Add add-on → PostgreSQL (or MySQL).
- Choose Deploy new to let Vessl provision a dedicated instance.
- Click Attach — Vessl provisions the database and injects these variables into your app:
DB_CONNECTION=pgsql
DB_HOST=<internal hostname>
DB_PORT=5432
DB_DATABASE=<db name>
DB_USERNAME=<username>
DB_PASSWORD=<password>
DATABASE_URL=postgresql://<user>:<pass>@<host>:5432/<db>
Laravel reads DB_CONNECTION, DB_HOST, etc. directly from .env — no config/database.php changes needed. The post-deploy command (php artisan migrate --force) runs automatically on every deploy.
Step 3 — Add Redis (optional)
For sessions, cache, or queues backed by Redis:
- Open the app → Overview → Connected Services → Add → Redis.
- Vessl injects:
REDIS_URL=redis://:<password>@<host>:6379
REDIS_HOST=<internal hostname>
REDIS_PORT=6379
REDIS_PASSWORD=<password>
Then update your env vars:
SESSION_DRIVER=redis
CACHE_STORE=redis
QUEUE_CONNECTION=redis
Step 4 — Queue workers
Laravel queues need a separate process running queue:work. In Vessl this is a daemon — a background process that runs alongside your web app on the same server.
- Go to the Daemons tab → Add Daemon.
- Set the command to:
php artisan queue:work --sleep=3 --tries=3 --max-time=3600
- Set restart policy to always (so it restarts if it crashes).
The worker runs in its own container and shares environment variables with your web app, including the REDIS_* or DB_* connection details.
Step 5 — Scheduled tasks
Laravel's scheduler (php artisan schedule:run) needs to run every minute. Add a second daemon:
/bin/sh -c "while true; do php artisan schedule:run --no-interaction; sleep 60; done"
This is a simple shell loop that fires the scheduler every 60 seconds without needing cron access on the host.
Step 6 — Storage and file uploads
Vessl mounts /app/storage as a persistent volume automatically for Laravel apps. This means:
- Uploaded files stored under
storage/app/survive redeploys and restarts. - Log files in
storage/logs/persist across container restarts. - The
storage/directory skeleton (framework/views, framework/cache, etc.) is bootstrapped on first mount.
To use storage:link for publicly accessible uploads, add it to your post-deploy command:
php artisan storage:link && php artisan migrate --force
Environment variables reference
Key variables you'll typically set manually:
| Variable | Example | Notes |
|---|---|---|
APP_NAME | My App | Display name |
MAIL_MAILER | smtp | Mail driver |
MAIL_HOST | smtp.mailgun.org | |
MAIL_PORT | 587 | |
MAIL_USERNAME | … | |
MAIL_PASSWORD | … | |
FILESYSTEM_DISK | local | Set to s3 for S3-backed storage |
Common issues
php artisan key:generate is missing
Vessl auto-generates APP_KEY on first deploy — you don't need to run this manually. If you see "No application encryption key has been specified", check that APP_KEY is set in the Variables tab.
php artisan migrate fails at build time
The build step runs inside a container that may not have network access to the database. Move migrations to the post-deploy command instead of the build command, or use the pre-filled default which runs them post-deploy.
Storage permission errors
Vessl bootstraps the storage/ skeleton, but if you see permission errors on storage/logs or storage/framework, add this to your post-deploy command:
chmod -R 775 storage bootstrap/cache && php artisan migrate --force
Sessions / cache not working after attaching a database
Update SESSION_DRIVER, CACHE_STORE, and QUEUE_CONNECTION in the Variables tab to database or redis, then redeploy.
Vite / Mix assets not built
If your package.json has a build script (Vite build or Mix prod), add it to your build command:
npm run build && php artisan migrate --force
Vessl detects package.json and includes npm ci in the install phase automatically.