Your Symfony application works perfectly on localhost. But deploying it to production feels like defusing a bomb while blindfolded. One wrong move and everything explodes.
Note: This guide focuses on deploying Symfony on Ubuntu servers. CloudPloy currently supports Laravel applications, with Symfony support coming soon. Stay tuned for updates!
We’ve deployed over 500 Symfony applications to production, from small APIs to enterprise platforms handling millions of requests. The difference between a smooth deployment and a disaster? Having a bulletproof deployment strategy that eliminates guesswork.
This guide reveals our battle-tested Symfony deployment process that achieves zero-downtime deployments, automatic rollbacks, and 99.99% uptime - the same process used by companies processing billions in transactions.
Why Symfony Deployment Is Different
Symfony isn’t just another PHP framework - it’s an enterprise-grade platform with specific deployment requirements:
- Compiled Container: Symfony’s dependency injection container must be compiled and warmed
- Environment Configuration: Proper handling of
.envfiles and environment variables - Asset Management: Webpack Encore builds and asset versioning
- Database Migrations: Doctrine migrations must run in the correct order
- Cache Warming: OpCode cache and application cache optimization
Ignore these requirements and you’ll face the dreaded “500 Internal Server Error” in production.
Pre-Deployment Checklist: Never Deploy Broken Code
1. Environment Configuration
# .env.prod.local (never commit this!)
APP_ENV=prod
APP_DEBUG=0
APP_SECRET=your-secure-secret-key
DATABASE_URL="mysql://user:pass@localhost:3306/db_name"
MAILER_DSN=smtp://user:pass@smtp.example.com:587
2. Security Audit
# Check for security vulnerabilities
composer audit
# Update dependencies
composer update --no-dev --optimize-autoloader
# Check Symfony security
symfony security:check
3. Code Quality Checks
# Run PHP CS Fixer
php-cs-fixer fix src/
# Static Analysis with PHPStan
vendor/bin/phpstan analyse src/ --level=8
# Run Tests
php bin/phpunit
Docker Deployment: The Modern Approach
Multi-Stage Dockerfile for Production
# Stage 1: Build Dependencies
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock symfony.lock ./
RUN composer install \
--no-dev \
--no-scripts \
--no-autoloader \
--prefer-dist \
--ignore-platform-reqs
# Stage 2: Build Assets
FROM node:18-alpine AS assets
WORKDIR /app
COPY package.json yarn.lock webpack.config.js ./
RUN yarn install --frozen-lockfile
COPY assets ./assets
RUN yarn build
# Stage 3: Production Image
FROM php:8.3-fpm-alpine
# Install PHP extensions
RUN apk add --no-cache \
postgresql-dev \
icu-dev \
&& docker-php-ext-install \
pdo_mysql \
pdo_pgsql \
intl \
opcache
# Configure PHP for production
COPY docker/php/php.ini /usr/local/etc/php/
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/
# Copy application
WORKDIR /var/www
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
COPY . .
# Set permissions
RUN chown -R www-data:www-data var/cache var/log
# Warm up cache
RUN php bin/console cache:warmup --env=prod
EXPOSE 9000
CMD ["php-fpm"]
Nginx Configuration for Symfony
server {
listen 80;
server_name example.com;
root /var/www/public;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass php:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}
location ~ \.php$ {
return 404;
}
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
}
Zero-Downtime Deployment Strategy
Blue-Green Deployment with Docker Compose
# docker-compose.prod.yml
version: '3.8'
services:
app_blue:
build: .
environment:
- APP_ENV=prod
- DATABASE_URL=${DATABASE_URL}
volumes:
- ./var:/var/www/var
networks:
- symfony_network
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`example.com`)"
- "traefik.http.services.app.loadbalancer.server.port=9000"
app_green:
build: .
environment:
- APP_ENV=prod
- DATABASE_URL=${DATABASE_URL}
volumes:
- ./var:/var/www/var
networks:
- symfony_network
labels:
- "traefik.enable=false"
nginx:
image: nginx:alpine
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app_blue
networks:
- symfony_network
ports:
- "80:80"
networks:
symfony_network:
driver: bridge
Deployment Script
#!/bin/bash
# deploy.sh
set -e
echo "Starting deployment..."
# Build new image
docker-compose -f docker-compose.prod.yml build app_green
# Start green container
docker-compose -f docker-compose.prod.yml up -d app_green
# Wait for health check
sleep 10
# Run migrations on green
docker-compose -f docker-compose.prod.yml exec app_green \
php bin/console doctrine:migrations:migrate --no-interaction
# Switch traffic to green
docker-compose -f docker-compose.prod.yml exec traefik \
sed -i 's/app_blue/app_green/g' /etc/traefik/dynamic.yml
# Stop blue container
docker-compose -f docker-compose.prod.yml stop app_blue
echo "Deployment complete!"
Database Migration Best Practices
Safe Migration Strategy
// migrations/Version20250909120000.php
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250909120000 extends AbstractMigration
{
public function up(Schema $schema): void
{
// Add new column with default value (safe)
$this->addSql('ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT "active"');
// Create index concurrently (PostgreSQL)
$this->addSql('CREATE INDEX CONCURRENTLY idx_users_status ON users(status)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE users DROP COLUMN status');
}
public function isTransactional(): bool
{
// Disable transaction for concurrent index creation
return false;
}
}
Migration Deployment Process
# 1. Backup database
mysqldump -u root -p database_name > backup_$(date +%Y%m%d).sql
# 2. Test migrations locally
php bin/console doctrine:migrations:migrate --dry-run
# 3. Apply migrations
php bin/console doctrine:migrations:migrate --no-interaction
# 4. Verify migration status
php bin/console doctrine:migrations:status
Performance Optimization for Production
1. OpCache Configuration
; opcache.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.preload=/var/www/config/preload.php
opcache.preload_user=www-data
2. Symfony Performance Settings
# config/packages/prod/framework.yaml
framework:
cache:
pools:
cache.global_clearer:
adapter: cache.adapter.apcu
router:
strict_requirements: null
utf8: true
session:
handler_id: 'redis://redis:6379'
cookie_secure: true
cookie_httponly: true
cookie_samesite: strict
3. Asset Optimization
// webpack.config.js
const Encore = require('@symfony/webpack-encore');
Encore
.setOutputPath('public/build/')
.setPublicPath('/build')
.enableVersioning(true)
.enableSassLoader()
.enablePostCssLoader()
.enableBuildNotifications(false)
.enableSourceMaps(false)
.enableSingleRuntimeChunk()
.splitEntryChunks()
.configureTerserPlugin((options) => {
options.terserOptions = {
compress: {
drop_console: true,
},
};
})
.enableIntegrityHashes(Encore.isProduction());
module.exports = Encore.getWebpackConfig();
CI/CD Pipeline with GitHub Actions
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, intl, pdo_mysql
coverage: xdebug
- name: Install Dependencies
run: composer install --prefer-dist --no-progress
- name: Run Tests
run: php bin/phpunit
- name: Security Check
run: symfony security:check
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Build Docker Image
run: |
docker build -t myapp:${{ github.sha }} .
docker tag myapp:${{ github.sha }} myapp:latest
- name: Deploy to Server
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/myapp
git pull origin main
docker-compose -f docker-compose.prod.yml up -d --build
docker-compose exec app php bin/console cache:clear
docker-compose exec app php bin/console doctrine:migrations:migrate --no-interaction
Monitoring and Logging
Application Performance Monitoring
// src/EventListener/PerformanceListener.php
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Psr\Log\LoggerInterface;
class PerformanceListener
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function onKernelTerminate(TerminateEvent $event): void
{
$duration = microtime(true) - $event->getRequest()->server->get('REQUEST_TIME_FLOAT');
if ($duration > 1.0) {
$this->logger->warning('Slow request detected', [
'url' => $event->getRequest()->getUri(),
'duration' => $duration,
'memory' => memory_get_peak_usage(true) / 1024 / 1024,
]);
}
}
}
Error Tracking with Sentry
# config/packages/sentry.yaml
sentry:
dsn: '%env(SENTRY_DSN)%'
options:
environment: '%kernel.environment%'
release: '%env(APP_VERSION)%'
traces_sample_rate: 0.1
profiles_sample_rate: 0.1
Cloud Deployment Options
AWS EC2 with Auto-Scaling
# cloudformation.yml
Resources:
AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
MinSize: 2
MaxSize: 10
DesiredCapacity: 3
HealthCheckType: ELB
HealthCheckGracePeriod: 300
LaunchTemplate:
LaunchTemplateId: !Ref SymfonyLaunchTemplate
Version: !GetAtt SymfonyLaunchTemplate.LatestVersionNumber
TargetGroupARNs:
- !Ref ALBTargetGroup
DigitalOcean App Platform
# .do/app.yaml
name: symfony-app
services:
- name: web
github:
repo: username/symfony-app
branch: main
build_command: |
composer install --no-dev --optimize-autoloader
yarn install && yarn build
php bin/console cache:warmup
run_command: php-fpm
environment_slug: php
instance_size_slug: professional-xs
instance_count: 2
envs:
- key: APP_ENV
value: prod
- key: DATABASE_URL
value: ${db.DATABASE_URL}
databases:
- name: db
engine: MYSQL
version: "8"
Deployment Troubleshooting Guide
Common Issues and Solutions
1. 500 Error After Deployment
# Clear cache
php bin/console cache:clear --env=prod
# Check permissions
chown -R www-data:www-data var/cache var/log
# Check logs
tail -f var/log/prod.log
2. Database Connection Issues
# Test connection
php bin/console doctrine:query:sql "SELECT 1"
# Check environment variables
php bin/console debug:config doctrine
3. Asset Loading Problems
# Rebuild assets
yarn build
# Install assets
php bin/console assets:install --symlink
Ubuntu Server Setup for Symfony
Initial Server Configuration
# Update Ubuntu packages
sudo apt update && sudo apt upgrade -y
# Install essential packages
sudo apt install -y curl wget git vim nginx mysql-server redis-server supervisor ufw
# Install PHP 8.3 and extensions
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update
sudo apt install -y php8.3-fpm php8.3-cli php8.3-common php8.3-mysql \
php8.3-zip php8.3-gd php8.3-mbstring php8.3-curl php8.3-xml \
php8.3-bcmath php8.3-intl php8.3-redis php8.3-opcache
# Install Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
# Install Node.js and Yarn
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs
npm install -g yarn
# Configure firewall
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Automated Deployment Script for Ubuntu
#!/bin/bash
# /home/deploy/deploy-symfony.sh
set -e
DEPLOY_USER="deploy"
APP_DIR="/var/www/symfony"
REPO_URL="git@github.com:yourcompany/symfony-app.git"
BRANCH="main"
echo "Starting Symfony deployment on Ubuntu..."
# Pull latest code
cd $APP_DIR
git fetch origin $BRANCH
git reset --hard origin/$BRANCH
# Install dependencies
export COMPOSER_ALLOW_SUPERUSER=1
composer install --no-dev --optimize-autoloader --no-interaction
# Build assets
yarn install --frozen-lockfile
yarn build
# Run migrations
php bin/console doctrine:migrations:migrate --no-interaction
# Clear and warm cache
php bin/console cache:clear --env=prod
php bin/console cache:warmup --env=prod
# Set permissions
sudo chown -R www-data:www-data var/
sudo chmod -R 775 var/
# Restart services
sudo systemctl reload php8.3-fpm
sudo systemctl reload nginx
echo "Deployment complete!"
Conclusion
Deploying Symfony to an Ubuntu server requires careful configuration but provides complete control over your infrastructure. With proper setup of Nginx, PHP-FPM, MySQL, and Redis, plus automated deployment scripts and monitoring, you can achieve enterprise-grade reliability.
This guide has covered everything from initial server setup to zero-downtime deployments, giving you the knowledge to deploy Symfony applications confidently on Ubuntu servers. Remember to always test in staging before deploying to production, maintain regular backups, and monitor your application’s performance continuously.