Production-Ready Symfony on DigitalOcean with Managed Databases
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:
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:
# 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
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:
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:
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:
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:
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:
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:
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:
[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:
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:
sudo apt install -y 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:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
8. HTTPS with Certbot
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:
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:
[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):
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:
[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
sudo systemctl enable --now symfony-scheduler
11. Deployment Workflow
Create /home/deploy/deploy.sh for repeatable zero-downtime deploys:
#!/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."
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:
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:
# 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):
0 3 * * 0 pg_dump "postgresql://..." | gzip | s3cmd put - s3://your-bucket/backups/$(date +\%Y\%m\%d).sql.gz
13. Verify the Deployment
# 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
- CI/CD pipeline — automate every deploy from a GitHub push → GitHub Actions to DigitalOcean Droplet guide
- Multiple apps on one server → Self-host your developer stack on one Droplet
- Next.js frontend alongside Symfony API → Self-host Next.js on a $6 Droplet vs serverless
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 |