Deployment

Production-Ready Symfony on DigitalOcean with Managed Databases

July 26, 202617 min read

Disclosure: Some links in this guide are affiliate links. If you sign up for DigitalOcean through them, ToolsHubKit earns a small commission — at no cost to you. We only recommend services we use.

This guide walks through a full production deployment of a Symfony application on a DigitalOcean Droplet backed by a Managed Postgres database. You'll set up PHP 8.3 + PHP-FPM, Nginx, Doctrine, Messenger workers, and a repeatable deploy workflow — nothing toy, nothing skipped.

Prerequisites:

  • A Symfony app (5.4 LTS or 6.x/7.x) with Doctrine ORM
  • A domain name with DNS you control
  • Basic Linux comfort (SSH, sudo, file editing)

1. Provision the Droplet

Create a new Droplet at DigitalOcean. Get $200 in free credits for 60 days when you sign up.

Recommended sizing for Symfony:

Use case RAM vCPUs Droplet tier
Single app, low traffic 1 GB 1 Basic $6/mo
With Messenger workers 2 GB 2 Basic $12/mo
High-traffic or multiple apps 4 GB 2 Basic $24/mo

Choose Ubuntu 24.04 LTS x64. Select your nearest datacenter region, add your SSH public key under Authentication → SSH Keys.


2. Initial Server Hardening

SSH in as root, then create a deploy user:

bash
ssh root@YOUR_SERVER_IP

# Create deploy user
adduser deploy
usermod -aG sudo deploy

# Copy SSH key to deploy user
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

Lock down SSH and the firewall:

bash
# Disable password login
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
systemctl restart ssh

# UFW firewall
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable

# fail2ban
apt install -y fail2ban
systemctl enable --now fail2ban

Log out, re-login as deploy for all remaining steps.


3. Install PHP 8.3 and Required Extensions

bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y software-properties-common
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update

sudo apt install -y \
  php8.3 php8.3-fpm php8.3-cli php8.3-pgsql php8.3-intl \
  php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip \
  php8.3-opcache php8.3-redis php8.3-bcmath php8.3-gd

Tune OPcache for production in /etc/php/8.3/fpm/conf.d/10-opcache.ini:

ini
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.revalidate_freq=0
opcache.save_comments=1

Install Composer:

bash
curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer

4. Provision a Managed Postgres Database

In the DigitalOcean console go to Databases → Create Database Cluster.

  • Engine: PostgreSQL 16
  • Plan: Basic $15/mo (1 GB RAM, 1 vCPU, 10 GB SSD) — good for most Symfony apps
  • Region: same as your Droplet

Once provisioned, add your Droplet to the Trusted Sources list so only it can connect. Copy the Connection Details — you'll need the host, port, user, password, and database name.


5. Deploy the Application

Create the app directory and set permissions:

bash
sudo mkdir -p /var/www/myapp
sudo chown deploy:www-data /var/www/myapp
sudo chmod 775 /var/www/myapp

Clone your repository and install dependencies:

bash
cd /var/www/myapp
git clone https://github.com/your-org/your-repo.git .

composer install --no-dev --optimize-autoloader --no-interaction

Create /var/www/myapp/.env.local with your production secrets:

env
APP_ENV=prod
APP_SECRET=your_32_char_secret_here
DATABASE_URL="postgresql://db_user:db_pass@managed-db-host:25060/db_name?sslmode=require"
MAILER_DSN=smtp://...

Warm the Symfony cache and run migrations:

bash
php bin/console cache:clear --env=prod --no-debug
php bin/console cache:warmup --env=prod
php bin/console doctrine:migrations:migrate --no-interaction --env=prod

6. Configure PHP-FPM Pool

Edit /etc/php/8.3/fpm/pool.d/myapp.conf:

ini
[myapp]
user = deploy
group = www-data
listen = /run/php/php8.3-fpm-myapp.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 500

php_admin_value[error_log] = /var/log/php/myapp-error.log
php_admin_flag[log_errors] = on

Remove the default pool to avoid security leaks:

bash
sudo rm /etc/php/8.3/fpm/pool.d/www.conf
sudo mkdir -p /var/log/php
sudo systemctl restart php8.3-fpm

7. Nginx Virtual Host

Install Nginx and create /etc/nginx/sites-available/myapp:

bash
sudo apt install -y nginx
nginx
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/myapp/public;

    index index.php;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/run/php/php8.3-fpm-myapp.sock;
        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;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|woff|woff2|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    client_max_body_size 20M;
    gzip on;
    gzip_types text/plain text/css application/javascript application/json;
}

Enable the site:

bash
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

8. HTTPS with Certbot

bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot patches your Nginx config automatically. Verify auto-renewal:

bash
sudo systemctl status certbot.timer
sudo certbot renew --dry-run

9. Messenger Workers Under systemd

If your app uses Symfony Messenger, create /etc/systemd/system/messenger-worker@.service:

ini
[Unit]
Description=Symfony Messenger Worker %i
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/php bin/console messenger:consume async --time-limit=3600 --memory-limit=256M
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=messenger-worker-%i
Environment=APP_ENV=prod

[Install]
WantedBy=multi-user.target

Start two workers (scale by adding more instances):

bash
sudo systemctl enable --now messenger-worker@1
sudo systemctl enable --now messenger-worker@2
sudo journalctl -u messenger-worker@1 -f

10. Scheduler Under systemd

For symfony/scheduler or cron-based tasks, create /etc/systemd/system/symfony-scheduler.service:

ini
[Unit]
Description=Symfony Scheduler
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/php bin/console scheduler:run
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
Environment=APP_ENV=prod

[Install]
WantedBy=multi-user.target
bash
sudo systemctl enable --now symfony-scheduler

11. Deployment Workflow

Create /home/deploy/deploy.sh for repeatable zero-downtime deploys:

bash
#!/bin/bash
set -e

APP_DIR=/var/www/myapp

cd "$APP_DIR"

echo "==> Pulling latest code..."
git pull origin main

echo "==> Installing dependencies..."
composer install --no-dev --optimize-autoloader --no-interaction

echo "==> Running migrations..."
php bin/console doctrine:migrations:migrate --no-interaction --env=prod

echo "==> Clearing cache..."
php bin/console cache:clear --env=prod --no-debug
php bin/console cache:warmup --env=prod

echo "==> Reloading PHP-FPM..."
sudo systemctl reload php8.3-fpm

echo "==> Restarting Messenger workers..."
sudo systemctl restart messenger-worker@1 messenger-worker@2

echo "==> Done."
bash
chmod +x /home/deploy/deploy.sh
# Grant passwordless sudo for just the needed commands
echo "deploy ALL=(ALL) NOPASSWD: /bin/systemctl reload php8.3-fpm, /bin/systemctl restart messenger-worker@*" | sudo tee /etc/sudoers.d/deploy

From your machine, deploy with a single command:

bash
ssh deploy@YOUR_SERVER_IP /home/deploy/deploy.sh

12. Backups

Managed Databases on DigitalOcean include daily automated backups retained for 7 days. Enable them under Database → Backups → Enable.

For point-in-time recovery, also configure a weekly manual dump to DigitalOcean Spaces:

bash
# Install s3cmd
sudo apt install -y s3cmd
s3cmd --configure  # enter Spaces access key, secret, and endpoint

# Dump and upload
pg_dump "postgresql://user:pass@host:25060/db?sslmode=require" | \
  gzip | s3cmd put - s3://your-bucket/backups/$(date +%Y%m%d).sql.gz

Add to crontab (crontab -e):

code
0 3 * * 0 pg_dump "postgresql://..." | gzip | s3cmd put - s3://your-bucket/backups/$(date +\%Y\%m\%d).sql.gz

13. Verify the Deployment

bash
# Check PHP-FPM is running
sudo systemctl status php8.3-fpm

# Tail application logs
sudo journalctl -u php8.3-fpm -f

# Check Symfony logs
tail -f /var/www/myapp/var/log/prod.log

# Test the health endpoint (add one if you haven't)
curl -I https://yourdomain.com/health

What to Add Next


Resources

Resource Link
DigitalOcean Droplets (+ $200 free credit) Sign up here
Symfony Deployment Documentation https://symfony.com/doc/current/deployment.html
Doctrine Migrations https://www.doctrine-project.org/projects/migrations.html
Certbot (Let's Encrypt) https://certbot.eff.org