CloudPloy

Symfony Hosting on CloudPloy

CloudPloy is built for PHP frameworks and has first-class support for Symfony. Your Symfony application runs on a managed server with PHP-FPM, Nginx, MySQL or MariaDB, Redis, and Supervisor for queue workers - all provisioned automatically. This guide covers the Symfony-specific configuration you need to get a production deployment running correctly.

What CloudPloy Configures for Symfony

When you connect a Symfony repository and deploy, CloudPloy sets up:

  • PHP 8.2 or 8.3 with all required extensions (intl, mbstring, pdo_mysql, redis, opcache, zip)
  • PHP-FPM tuned for your server's memory and CPU
  • Nginx configured to serve the Symfony public/ directory
  • OPcache enabled and pre-warmed on deploy
  • Composer dependency installation on every deployment
  • Automated SSL certificate provisioning via Let's Encrypt

Environment Configuration

Symfony reads configuration from the .env file and environment variables. On CloudPloy, set all environment-specific values in the dashboard - never commit secrets to your repository. The variables set in the dashboard override values in .env.

Required environment variables for a Symfony production deployment:

Variable Value Notes
APP_ENV prod Enables production optimizations, disables debug
APP_SECRET 32-char random string Used for CSRF tokens and signed URLs - rotate after breaches
DATABASE_URL mysql://user:pass@host:3306/dbname?serverVersion=8.0 Doctrine connection string
REDIS_URL redis://localhost:6379 Used for cache, sessions, and message queue
MAILER_DSN smtp://user:pass@smtp.mailgun.org:587 Symfony Mailer transport

Generate a secure APP_SECRET with:

php -r "echo bin2hex(random_bytes(16));"

Deploying with Git

CloudPloy deploys your application automatically when you push to your configured branch. The deployment sequence for Symfony runs:

  1. Pull latest code from the connected Git repository
  2. Run composer install --no-dev --optimize-autoloader
  3. Clear and warm the Symfony cache: php bin/console cache:clear --env=prod
  4. Run pending database migrations: php bin/console doctrine:migrations:migrate --no-interaction
  5. Restart PHP-FPM to reload updated code
  6. Reload Nginx configuration

You can customize the deployment hooks in the CloudPloy dashboard to add steps like asset compilation or cache warming for specific bundles.

Database Migrations

Symfony uses Doctrine Migrations for schema management. The safest production migration strategy is to run migrations as a deployment step before restarting PHP-FPM:

# Check what migrations are pending before deploying
php bin/console doctrine:migrations:status

# Run migrations - this runs in the deployment hook
php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration

# If something goes wrong, roll back
php bin/console doctrine:migrations:execute --down 'App\Migrations\Version20260101000000'

For large tables, run migrations that add columns as nullable first, deploy the code that handles both the old and new schema, then run a separate migration to add the NOT NULL constraint. This eliminates downtime from long-running ALTER TABLE operations.

Caching with Redis

Configure Symfony to use Redis for all cache pools. This is essential on multi-server setups and significantly faster than filesystem caching even on single servers:

# config/packages/cache.yaml
framework:
  cache:
    app: cache.adapter.redis
    default_redis_provider: '%env(REDIS_URL)%'
    pools:
      cache.app:
        adapter: cache.adapter.redis
        provider: '%env(REDIS_URL)%'
        default_lifetime: 3600

For HTTP cache responses (ESI fragments, Varnish-style caching), use the Symfony HTTP Cache component or configure Nginx to cache responses from PHP-FPM.

Session Storage

Store sessions in Redis instead of the filesystem. Filesystem sessions break across multiple PHP-FPM workers and disappear when the server restarts:

# config/packages/framework.yaml
framework:
  session:
    handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler
    cookie_secure: auto
    cookie_samesite: lax
    gc_maxlifetime: 3600

# config/services.yaml
services:
  Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler:
    arguments:
      - '@snc_redis.default'
      - { prefix: 'session:', ttl: 3600 }

Queue Workers with Supervisor

Symfony Messenger uses Supervisor to keep queue workers running in the background. CloudPloy configures Supervisor automatically when you enable queue workers in your application settings. The standard configuration for a Symfony Messenger worker:

[program:symfony-worker]
command=/usr/bin/php /var/www/html/bin/console messenger:consume async --time-limit=3600 --memory-limit=256M
directory=/var/www/html
autostart=true
autorestart=true
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d
redirect_stderr=true
stdout_logfile=/var/log/supervisor/symfony-worker.log

The --time-limit flag restarts the worker after one hour, preventing memory leaks from accumulating. The --memory-limit flag restarts the worker if it exceeds 256MB.

In your Symfony Messenger configuration:

# config/packages/messenger.yaml
framework:
  messenger:
    transports:
      async:
        dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
        options:
          auto_setup: false
    routing:
      App\Message\SendEmailMessage: async
      App\Message\ProcessImageMessage: async

Set MESSENGER_TRANSPORT_DSN to redis://localhost:6379/messages in your environment variables.

OPcache Configuration

OPcache dramatically improves PHP performance by caching compiled bytecode. CloudPloy enables OPcache by default with settings appropriate for Symfony production deployments. The recommended configuration for high-traffic Symfony apps:

; php.ini OPcache settings for Symfony production
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.interned_strings_buffer=16
opcache.fast_shutdown=1
opcache.revalidate_freq=0

Setting validate_timestamps=0 means OPcache will not check for file changes at runtime - the cache is cleared explicitly during deployment. This eliminates stat() calls on every request and significantly improves performance.

Logging

Symfony uses Monolog for logging. Configure different handlers for production - write errors to syslog or a file, and send critical errors to your alerting system:

# config/packages/prod/monolog.yaml
monolog:
  handlers:
    main:
      type: fingers_crossed
      action_level: error
      handler: grouped
      excluded_http_codes: [404, 405]
      buffer_size: 50
    grouped:
      type: group
      members: [streamed, syslog]
    streamed:
      type: stream
      path: "%kernel.logs_dir%/%kernel.environment%.log"
      level: debug
    syslog:
      type: syslog
      ident: symfony
      level: error
    deprecation:
      type: stream
      channels: [deprecation]
      path: php://stderr

Performance Optimization

Composer Autoloader

Always use the optimized autoloader in production. CloudPloy runs this automatically during deployment:

composer install --no-dev --optimize-autoloader --classmap-authoritative

The --classmap-authoritative flag tells Composer that every class is in the classmap - this eliminates filesystem lookups for unknown classes and is safe for production as long as you do not use dynamic class loading.

Cache Warm-up

Warm the Symfony cache immediately after deployment so the first request does not incur the cache generation cost:

php bin/console cache:clear --env=prod --no-debug
php bin/console cache:warmup --env=prod

Route and Template Caching

In production, all routes and Twig templates are compiled and cached automatically when APP_ENV=prod. You do not need to do anything extra - Symfony handles this during the cache warmup step.

Scheduled Tasks with Cron

Use Symfony's built-in scheduler or configure cron jobs in the CloudPloy dashboard for periodic tasks:

# Symfony Scheduler (recommended for Symfony 6.3+)
# src/Scheduler/AppScheduler.php
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;

#[AsSchedule]
class AppScheduler implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        return (new Schedule())->add(
            RecurringMessage::every('1 hour', new CleanupExpiredTokensMessage()),
            RecurringMessage::cron('0 0 * * *', new DailyReportMessage()),
        );
    }
}

For older Symfony apps, add cron entries via the CloudPloy dashboard that run console commands:

# Run every 5 minutes
*/5 * * * * php /var/www/html/bin/console app:process-queue
# Run daily at midnight
0 0 * * * php /var/www/html/bin/console app:cleanup-old-data

File Storage

Do not store uploaded files on the server's local filesystem if you plan to use multiple servers or need persistence across deployments. Use an external storage service instead:

# config/packages/flysystem.yaml
flysystem:
  storages:
    default.storage:
      adapter: 'asyncaws'
      options:
        client: 's3_client'
        bucket: '%env(AWS_S3_BUCKET)%'
        prefix: uploads/

services:
  s3_client:
    class: AsyncAws\S3\S3Client
    arguments:
      -
        region: '%env(AWS_REGION)%'
        accessKeyId: '%env(AWS_ACCESS_KEY_ID)%'
        accessKeySecret: '%env(AWS_SECRET_ACCESS_KEY)%'

Security Configuration

HTTPS Enforcement

CloudPloy provisions SSL certificates and configures Nginx to redirect HTTP to HTTPS. Add Symfony's security configuration to enforce HTTPS at the application level as a second layer:

# config/packages/prod/security.yaml
security:
  access_control:
    - { path: ^/, requires_channel: https }

Trusted Proxies

Symfony needs to know it is behind a proxy (Nginx) to correctly resolve the client's IP address and protocol. Configure trusted proxies in public/index.php:

// public/index.php
Request::setTrustedProxies(
    ['127.0.0.1', '10.0.0.0/8'],
    Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO
);

Without trusted proxy configuration, $request->getClientIp() will return the Nginx IP instead of the real client IP, and $request->isSecure() will return false even for HTTPS connections.

Deployment Checklist

  • Set APP_ENV=prod and APP_DEBUG=0 in environment variables
  • Set a strong, unique APP_SECRET (never share between environments)
  • Configure DATABASE_URL with the server's internal IP
  • Set up Redis for cache and sessions
  • Add REDIS_URL and configure cache adapter in cache.yaml
  • Enable Supervisor workers for Symfony Messenger queues
  • Configure trusted proxies in public/index.php
  • Run composer install --no-dev --optimize-autoloader in deploy hook
  • Run migrations in deploy hook with --allow-no-migration flag
  • Warm the cache in deploy hook

Common Issues

Problem Cause Fix
500 errors after deploy Cache not cleared Add php bin/console cache:clear to deploy hook
Wrong client IP in logs Trusted proxies not configured Set trusted proxies in public/index.php
Sessions lost on restart File-based sessions Switch session handler to Redis
Queue jobs not processing Supervisor worker not running Enable worker in CloudPloy application settings
Slow first request after deploy Cold cache after clear Add php bin/console cache:warmup to deploy hook
File permission errors Wrong ownership on var/ or public/uploads/ Set www-data ownership on writable directories