Laravel on CloudPloy - Complete Guide
CloudPloy is purpose-built for PHP frameworks, and Laravel gets first-class treatment. Every Laravel app runs in an isolated Docker container with automatic detection, zero-downtime deployments, queue workers, scheduler support, and Redis available with one click. This guide covers how CloudPloy works with Laravel from first push to production-ready configuration.
How CloudPloy Detects and Deploys Laravel
Connect a GitHub repository and CloudPloy inspects its contents. If it finds artisan and composer.json with a laravel/framework dependency, it classifies the app as Laravel and applies framework-specific defaults: PHP-FPM as the runtime, storage/ and bootstrap/cache/ mounted as persistent volumes, and post-deploy commands pre-configured.
On every push to the connected branch, CloudPloy runs this sequence:
- Pull the repository
- Build the Docker image (from your Dockerfile or the default base image)
- Run
composer install --no-dev --optimize-autoloader - Start the new container and verify the health check passes
- Atomically switch traffic to the new container
- Stop the old container after a 30-second drain
Zero-downtime. No manual SSH. No deployment scripts to maintain.
Environment Variables
Never commit .env to your repository. Configure all environment variables in App > Settings > Environment Variables. They are injected into the container at startup and available via Laravel's env() helper, $_ENV, and getenv().
Required Laravel variables to configure in CloudPloy:
| Variable | Production Value | Notes |
|---|---|---|
APP_ENV | production | Disables debug mode, enables production error handling |
APP_DEBUG | false | Never true in production - exposes stack traces |
APP_KEY | 32-byte random key | Generate with php artisan key:generate --show |
APP_URL | https://yourapp.com | Used for asset URLs and email links |
DB_CONNECTION | mysql | Or pgsql for PostgreSQL |
DB_HOST | From CloudPloy database panel | Internal hostname within the server |
DB_DATABASE | Your database name | Created in App > Databases |
DB_USERNAME | Your database user | Least-privilege user, not root |
DB_PASSWORD | Strong random password | Store only in CloudPloy, never in code |
CACHE_DRIVER | redis | Add Redis service in App > Add-ons first |
SESSION_DRIVER | redis | Shared session state across multiple workers |
QUEUE_CONNECTION | redis | Required if using queue workers |
REDIS_HOST | From CloudPloy Redis panel | Internal hostname, no external exposure |
LOG_CHANNEL | stderr | Writes to container stdout - visible in App > Logs |
LOG_LEVEL | error | Reduces log noise in production |
Dockerfile for Laravel
If you do not include a Dockerfile, CloudPloy uses its default cloudploy/php:8.3-fpm base image and runs composer install automatically. For most Laravel apps this is sufficient. Add a Dockerfile when you need custom PHP extensions, build-time npm compilation, or specific post-install commands.
Minimal production Dockerfile:
FROM cloudploy/php:8.3-fpm
WORKDIR /var/www/html
# Install Composer dependencies (cached layer)
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts
# Copy application
COPY . .
# Run post-install scripts
RUN composer run-script post-autoload-dump
# Cache Laravel configuration for performance
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
# Correct permissions
RUN chown -R www-data:www-data storage bootstrap/cache For apps with frontend assets (Vite or Mix):
# Stage 1: Build assets
FROM node:20-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY resources ./resources
COPY vite.config.js ./
RUN npm run build
# Stage 2: Production image
FROM cloudploy/php:8.3-fpm
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts
COPY --from=assets /app/public/build ./public/build
COPY . .
RUN composer run-script post-autoload-dump \
&& php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache \
&& chown -R www-data:www-data storage bootstrap/cache Database Migrations
Running php artisan migrate during deployment needs care. You do not want migrations to run mid-deployment while old containers are still serving traffic. CloudPloy provides a post-deploy command hook that runs after traffic switches to the new container.
Configure it in App > Settings > Deploy Commands:
php artisan migrate --force The --force flag is required in production (Laravel asks for confirmation interactively otherwise). For zero-downtime migrations that both old and new containers can run safely, follow the expand-contract migration pattern: add nullable columns first, deploy new code, then make columns non-nullable in a follow-up migration.
To see migration status from the terminal:
# In App > Terminal
php artisan migrate:status
php artisan migrate:rollback --step=1 # Undo last migration Queue Workers
Laravel queue workers run as separate processes. In CloudPloy, configure them in App > Workers. Each worker entry becomes a separate container running the specified command.
Recommended worker configuration for most apps:
| Setting | Value | Reason |
|---|---|---|
| Command | php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 | Restarts after 1 hour to reclaim memory |
| Replicas | 2 | Redundancy - one worker can fail without queue stalling |
| Restart policy | Always | Auto-restarts if the process exits |
For high-throughput queues, use Laravel Horizon instead of bare queue:work. Horizon provides dynamic scaling, metrics, and a monitoring dashboard:
# composer.json dependency
composer require laravel/horizon
# Publish config
php artisan horizon:install
# Worker command in CloudPloy
php artisan horizon Configure Horizon's worker pools in config/horizon.php to match CloudPloy's Redis hostname.
Task Scheduler
Laravel's task scheduler requires a cron trigger every minute. Add this as a worker in CloudPloy:
# Worker command for scheduler
php artisan schedule:run Set the worker's restart policy to On Finish with a 60-second delay to simulate a cron. Alternatively, use CloudPloy's native Cron Jobs feature in Server > Cron Jobs and add:
* * * * * cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1 Redis Setup
Add Redis from App > Add-ons > Redis. CloudPloy creates a Redis instance on the same server as your app, accessible via the internal hostname shown in the add-ons panel.
Configure Laravel to use Redis for cache, sessions, and queues simultaneously by setting these environment variables:
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=your-redis-host # From App > Add-ons panel
REDIS_PORT=6379
REDIS_PASSWORD= # Leave empty if no auth configured With Redis handling sessions, your app scales horizontally - multiple PHP-FPM workers share session state. File-based sessions break when requests hit different workers.
For cache key separation between multiple apps on the same Redis instance, set a unique prefix in config/database.php:
'redis' => [
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'prefix' => env('REDIS_PREFIX', 'myapp_'),
],
] Storage and File Uploads
CloudPloy automatically mounts /var/www/html/storage as a persistent volume. Files written here survive deployments and container restarts. This is where Laravel stores uploaded files when using local disk, framework logs, and compiled views.
For production file uploads accessible via URL, use S3-compatible storage:
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=your-bucket S3-compatible providers like DigitalOcean Spaces, Cloudflare R2, and MinIO all work with Laravel's S3 driver. Cloudflare R2 has no egress fees, which makes it cost-effective for user-generated content.
Performance Configuration
OPcache
OPcache is pre-enabled in CloudPloy's PHP images. In production, disable timestamp validation since Docker images are immutable - the PHP files never change without a new deployment:
; Add via Dockerfile: COPY docker/php.ini /usr/local/etc/php/conf.d/99-custom.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0 Configuration Cache
Laravel's configuration, route, and view caches dramatically reduce per-request overhead. Run these in your Dockerfile build step:
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache Important: after caching config, the env() helper no longer reads from environment variables at runtime - it reads the cached config. This is correct for production. If you need to change an environment variable, update it in CloudPloy and trigger a new deployment (which rebuilds the cache with the new values).
Database Query Optimization
Common Laravel N+1 query problems become visible under load. Use Laravel Debugbar in development and set up query logging in production:
// In AppServiceProvider::boot() - only logs slow queries
DB::listen(function ($query) {
if ($query->time > 1000) { // queries over 1 second
Log::warning('Slow query', [
'sql' => $query->sql,
'time' => $query->time,
]);
}
}); Use Eager Loading (with()) to eliminate N+1 problems and add database indexes for columns used in WHERE clauses and orderBy calls.
Logging
Set LOG_CHANNEL=stderr in production. This writes log output to container stdout, which CloudPloy captures and displays in App > Logs. Avoid writing to files inside the container - log files don't rotate automatically and will fill up the container's overlay filesystem.
For structured logging (easier to search and filter), configure the stderr channel with JSON format:
// config/logging.php
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'formatter' => JsonFormatter::class,
'with' => [
'stream' => 'php://stderr',
],
], Common Issues
| Problem | Cause | Fix |
|---|---|---|
| 500 error on first deploy | Missing APP_KEY | Generate with php artisan key:generate --show and add to env vars |
| env() returns null after config:cache | Expected behavior - cached config reads from cache, not .env | Update env var in CloudPloy and redeploy to refresh cache |
| Storage permission denied | Files created by root, PHP-FPM runs as www-data | Add RUN chown -R www-data:www-data storage bootstrap/cache to Dockerfile |
| Queue jobs not processing | Worker not configured or QUEUE_CONNECTION not set | Add worker in App > Workers and set QUEUE_CONNECTION=redis |
| Scheduler not running | No cron trigger configured | Add cron job or worker running schedule:run every minute |
| Session not persisting | SESSION_DRIVER=file with multiple workers | Set SESSION_DRIVER=redis and configure Redis |
| Assets 404 after deploy | Vite manifest missing - npm build not running | Add npm install + npm run build steps to Dockerfile |
| Slow first request | Config/route/view not cached | Add artisan cache commands to Dockerfile build step |
Deployment Checklist
Before going live with a Laravel app on CloudPloy:
- APP_ENV=production and APP_DEBUG=false set in environment variables
- APP_KEY generated and stored in CloudPloy (not in repository)
- Database credentials configured and connection tested
- CACHE_DRIVER and SESSION_DRIVER set to redis (not file or array)
- LOG_CHANNEL=stderr so logs appear in the CloudPloy dashboard
- Storage volume mounted (automatic for /storage, verify in App > Settings)
- Queue workers configured if the app dispatches jobs
- Scheduler configured if the app uses Console/Kernel.php schedule()
- Migration command configured in App > Settings > Deploy Commands
- Health check endpoint verified (GET / returns 200)
Laravel Guides
Detailed step-by-step guides for specific Laravel tasks on CloudPloy:
- Deploy Laravel with GitHub Actions - Set up automated deployments triggered on push to main, with environment-specific builds and rollback on failure.
- Database Optimization for Laravel - Query optimization, index strategies, connection pooling, and using read replicas with Laravel's database configuration.
- Laravel Performance Optimization - OPcache tuning, Redis caching strategies, PHP-FPM pool sizing, and measuring performance improvements.
- Laravel Queue Configuration - Setting up Redis queues, configuring worker concurrency, handling failed jobs, and monitoring with Horizon.
- Laravel Security Hardening - Securing headers, disabling dangerous PHP functions, CSRF configuration, rate limiting, and secrets management.
Need help with a specific Laravel issue? Use App > Terminal to run artisan commands directly in your running container, or contact CloudPloy support with your app URL and error details.