CloudPloy

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:

  1. Pull the repository
  2. Build the Docker image (from your Dockerfile or the default base image)
  3. Run composer install --no-dev --optimize-autoloader
  4. Start the new container and verify the health check passes
  5. Atomically switch traffic to the new container
  6. 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:


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.