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:
- Pull latest code from the connected Git repository
- Run
composer install --no-dev --optimize-autoloader - Clear and warm the Symfony cache:
php bin/console cache:clear --env=prod - Run pending database migrations:
php bin/console doctrine:migrations:migrate --no-interaction - Restart PHP-FPM to reload updated code
- 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=prodandAPP_DEBUG=0in environment variables - Set a strong, unique
APP_SECRET(never share between environments) - Configure
DATABASE_URLwith the server's internal IP - Set up Redis for cache and sessions
- Add
REDIS_URLand configure cache adapter incache.yaml - Enable Supervisor workers for Symfony Messenger queues
- Configure trusted proxies in
public/index.php - Run
composer install --no-dev --optimize-autoloaderin deploy hook - Run migrations in deploy hook with
--allow-no-migrationflag - 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 |