Skip to content
Fernando Valler

How to Prepare a VPS for a Symfony Application with Apache, PHP-FPM and MySQL

A practical guide to preparing an Ubuntu VPS to run a Symfony application in production using Apache, PHP-FPM, MySQL, Composer and HTTPS.

How to Prepare a VPS for a Symfony Application with Apache, PHP-FPM and MySQL

Deploying a Symfony application to a VPS does not start with git clone.

Before the application reaches production, the server needs to be prepared: the operating system must be updated, secure access configured, Apache and PHP-FPM installed, MySQL prepared, permissions configured, the domain connected, and HTTPS enabled.

In this article, we will start with an almost empty Ubuntu VPS and turn it into a server capable of running a Symfony application in production.

The goal is not to build an extremely sophisticated infrastructure, but to establish a secure, predictable and maintainable foundation.

The commands in this article use Ubuntu 24.04 as a reference. Adjust the PHP version, extensions and paths according to your application’s composer.json.

The architecture

The resulting architecture will look roughly like this:

text
Internet
   │
   ▼
Cloudflare / DNS
   │
   ▼
Apache
   │
   ▼
PHP-FPM
   │
   ▼
Symfony
   │
   ▼
MySQL

Apache receives HTTP/HTTPS requests and serves the public files of the application.

PHP-FPM executes PHP.

Symfony stays outside the public web root, with only the public/ directory exposed by Apache.

MySQL remains locally accessible on the server, without exposing port 3306 to the Internet.


1. Start with VPS security

A new VPS should be secured before deploying the application.

First, update the system:

bash
sudo apt update
sudo apt upgrade -y

Install a few basic packages:

bash
sudo apt install -y \
    curl \
    wget \
    git \
    unzip \
    zip \
    ca-certificates \
    software-properties-common

Avoid working as root permanently

Create a dedicated user:

bash
sudo adduser deploy

Add it to the sudo group:

bash
sudo usermod -aG sudo deploy

From this point on, prefer using the deploy user for application-related operations.

Configure SSH key authentication

On your local machine:

bash
ssh-keygen -t ed25519

Copy the public key:

bash
ssh-copy-id deploy@SERVER_IP

Test the connection:

bash
ssh deploy@SERVER_IP

Only after confirming key-based authentication should you consider disabling SSH password authentication.

Also avoid exposing unnecessary services to the public Internet.


2. Configure the firewall

Ubuntu’s ufw provides a simple firewall layer.

Install it:

bash
sudo apt install -y ufw

Allow SSH:

bash
sudo ufw allow OpenSSH

Allow HTTP:

bash
sudo ufw allow 80/tcp

Allow HTTPS:

bash
sudo ufw allow 443/tcp

Enable it:

bash
sudo ufw enable

Check the rules:

bash
sudo ufw status

For a traditional Apache + MySQL setup, there is normally no reason to expose MySQL port 3306 to the Internet.


3. Install Apache

Install Apache:

bash
sudo apt install -y apache2

Check the service:

bash
sudo systemctl status apache2

Enable it at boot:

bash
sudo systemctl enable apache2
sudo systemctl start apache2

Enable the modules commonly required by Symfony:

bash
sudo a2enmod rewrite
sudo a2enmod proxy_fcgi
sudo a2enmod setenvif
sudo a2enmod headers
sudo a2enmod ssl

Restart Apache:

bash
sudo systemctl restart apache2

At this point, opening the server IP in a browser should display the default Apache page.


4. Install PHP and PHP-FPM

Symfony needs PHP plus the extensions required by the application.

For example, with PHP 8.3:

bash
sudo apt install -y \
    php8.3 \
    php8.3-fpm \
    php8.3-cli \
    php8.3-common \
    php8.3-mysql \
    php8.3-intl \
    php8.3-mbstring \
    php8.3-xml \
    php8.3-curl \
    php8.3-zip \
    php8.3-opcache \
    php8.3-bcmath

Check PHP:

bash
php -v

Check PHP-FPM:

bash
sudo systemctl status php8.3-fpm

Enable its Apache configuration:

bash
sudo a2enconf php8.3-fpm

Restart Apache:

bash
sudo systemctl restart apache2

Do not install extensions blindly

Check the project’s composer.json:

bash
cat composer.json

Look for requirements such as:

json
"require": {
    "php": "...",
    "ext-intl": "*",
    "ext-pdo": "*",
    "ext-xml": "*"
}

The production server should satisfy the same runtime requirements as the application.


5. Install Composer

Composer installs the PHP dependencies.

After installation:

bash
composer --version

A key production practice is to deploy the exact versions recorded in composer.lock.

Use:

bash
composer install --no-dev --prefer-dist --optimize-autoloader

Avoid running:

bash
composer update

as part of a normal production deployment.

composer update can change dependency versions. Production deployments should normally reproduce the versions already defined by the project.


6. Install MySQL

Install MySQL:

bash
sudo apt install -y mysql-server

Check the service:

bash
sudo systemctl status mysql

Enable it:

bash
sudo systemctl enable mysql

Then run the security configuration:

bash
sudo mysql_secure_installation

The available options can vary depending on the installed version.


7. Create the database

Open MySQL:

bash
sudo mysql

Create the database:

sql
CREATE DATABASE my_application
    CHARACTER SET utf8mb4
    COLLATE utf8mb4_unicode_ci;

Create a dedicated database user:

sql
CREATE USER 'my_application'@'localhost'
IDENTIFIED BY 'A_STRONG_PASSWORD';

Grant access only to that database:

sql
GRANT ALL PRIVILEGES
ON my_application.*
TO 'my_application'@'localhost';

Apply the changes:

sql
FLUSH PRIVILEGES;

Exit:

sql
EXIT;

The application should not use the MySQL root account.

Separating the application account limits the impact of a potential application compromise.


8. Create the application structure

A simple directory layout is:

text
/var/www/
└── my-application/
    ├── bin/
    ├── config/
    ├── migrations/
    ├── public/
    ├── src/
    ├── templates/
    ├── var/
    ├── vendor/
    └── composer.json

Create the directory:

bash
sudo mkdir -p /var/www/my-application

Set the owner:

bash
sudo chown -R deploy:www-data /var/www/my-application

The goal is to allow the deployment user to manage the application while PHP-FPM, running as www-data, gets access only where necessary.


9. Deploy the code

Enter the application directory:

bash
cd /var/www/my-application

Clone the project:

bash
git clone https://github.com/user/project.git .

Or use whatever deployment mechanism your team has chosen.

Then install dependencies:

bash
composer install \
    --no-dev \
    --prefer-dist \
    --optimize-autoloader

10. Configure environment variables

A Symfony application should not keep production credentials directly in the source code.

Production configuration can be provided through environment variables or a protected local configuration file that is not committed to Git.

For example:

bash
APP_ENV=prod
APP_DEBUG=0
APP_SECRET=your-secret-key
DATABASE_URL="mysql://my_application:[email protected]:3306/my_application?serverVersion=8.0"

Sensitive configuration must remain protected and should not be committed to the repository.

Before deploying:

bash
git status

Make sure secret files are not being tracked.


11. Configure Symfony permissions

Symfony mainly needs write access to:

text
var/cache/
var/log/

A simple approach is:

bash
sudo chown -R deploy:www-data /var/www/my-application

Then:

bash
sudo chmod -R ug+rwX /var/www/my-application/var

The important principle is to grant only the permissions required by the application.

Avoid:

bash
chmod -R 777 /var/www/my-application

Commands like this often hide permission problems while unnecessarily increasing the attack surface.


12. Configure Apache

The most important Apache setting is the document root.

It should point to:

text
/var/www/my-application/public

not:

text
/var/www/my-application

Create a VirtualHost:

bash
sudo nano /etc/apache2/sites-available/my-application.conf

Example:

apache
<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com

    DocumentRoot /var/www/my-application/public

    <Directory /var/www/my-application/public>
        AllowOverride All
        Require all granted
        Options FollowSymLinks
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/my-application-error.log
    CustomLog ${APACHE_LOG_DIR}/my-application-access.log combined
</VirtualHost>

Enable the site:

bash
sudo a2ensite my-application.conf

Disable the default site:

bash
sudo a2dissite 000-default.conf

Check the configuration:

bash
sudo apachectl configtest

Expected:

text
Syntax OK

Reload Apache:

bash
sudo systemctl reload apache2

13. Why should DocumentRoot point to public/?

This is one of the most important Symfony deployment details.

The:

text
public/

directory is the public boundary of the application.

Files such as:

text
.env
composer.json
composer.lock
config/
src/
var/

should not be directly accessible from the web.

A request such as:

text
https://example.com/

should reach:

text
public/index.php

Symfony then handles the request from there.

This separation also reduces the risk of accidentally exposing internal project files.


14. Configure DNS

Point the domain to the VPS public IP.

For example:

text
A
example.com
203.0.113.10

And, if needed:

text
A
www.example.com
203.0.113.10

Check DNS resolution:

bash
dig example.com

or:

bash
nslookup example.com

The domain should resolve to the correct server.


15. Enable HTTPS

For a public application, HTTPS should be part of the initial deployment.

Install Certbot:

bash
sudo apt install -y certbot python3-certbot-apache

Run:

bash
sudo certbot --apache -d example.com -d www.example.com

Check Apache:

bash
sudo apachectl configtest

Check automatic renewal:

bash
sudo systemctl status certbot.timer

16. Run Symfony production tasks

With the code and environment configured:

bash
cd /var/www/my-application

Clear and warm the production cache:

bash
APP_ENV=prod php bin/console cache:clear

Run database migrations when applicable:

bash
php bin/console doctrine:migrations:migrate --no-interaction

Inspect the environment:

bash
php bin/console about

The application should explicitly run in production:

text
APP_ENV=prod
APP_DEBUG=0

17. Test each layer independently

Before blaming Apache, test each component separately.

PHP:

bash
php -v

PHP-FPM:

bash
sudo systemctl status php8.3-fpm

MySQL:

bash
sudo systemctl status mysql

Apache:

bash
sudo systemctl status apache2

Listening ports:

bash
sudo ss -lntp

VirtualHosts:

bash
sudo apachectl -S

Apache logs:

bash
sudo tail -f /var/log/apache2/my-application-error.log

and:

bash
sudo tail -f /var/log/apache2/my-application-access.log

Symfony logs:

bash
tail -f var/log/prod.log

when the application uses the standard production log file.


18. VPS Symfony checklist

Before considering the server ready:

text
[ ] Operating system updated
[ ] Dedicated deploy user created
[ ] SSH key authentication working
[ ] Root SSH access reviewed
[ ] Firewall configured
[ ] Only required ports exposed
[ ] Apache installed
[ ] PHP installed
[ ] PHP-FPM running
[ ] Required PHP extensions installed
[ ] Composer installed
[ ] MySQL installed
[ ] Database created
[ ] Dedicated database user created
[ ] Port 3306 not publicly exposed
[ ] Application code deployed
[ ] composer.lock used during deployment
[ ] Environment variables configured
[ ] public/ configured as DocumentRoot
[ ] var/ permissions configured
[ ] VirtualHost configured
[ ] DNS configured
[ ] HTTPS enabled
[ ] APP_ENV=prod
[ ] APP_DEBUG=0
[ ] Database migrations executed
[ ] Production cache warmed
[ ] Logs working
[ ] Database backups configured

19. Backups are part of the infrastructure

A working application does not mean that the data is protected.

At minimum, an application with important data should have automated MySQL backups.

For example:

bash
mysqldump \
    -u my_application \
    -p \
    my_application \
    > backup.sql

But a backup stored only on the same VPS is not enough.

A more resilient strategy looks like:

text
VPS
 │
 ├── Production database
 │
 └── Local backup
       │
       ▼
   External storage

Backups should also be restored periodically to verify that they actually work.

A backup that has never been restored is only an assumption.


20. What about future deployments?

Once the server is working, we should not manually repeat every setup step.

A basic deployment flow could be:

bash
git pull
composer install --no-dev --prefer-dist --optimize-autoloader
php bin/console doctrine:migrations:migrate --no-interaction
php bin/console cache:clear

For small applications, that may be enough.

As the application grows, the workflow can evolve into:

text
Git
 │
 ▼
CI/CD
 │
 ▼
Build
 │
 ▼
Deploy
 │
 ├── composer install
 ├── migrations
 ├── cache
 └── restart workers

For critical applications, versioned releases, rollback, staging environments and zero-downtime deployment strategies can also be introduced.

The important point is that server configuration and application deployment are separate concerns.


Conclusion

Preparing a VPS for Symfony is more than installing Apache, PHP and MySQL.

A production-ready foundation needs at least four layers:

text
Security
    ↓
Operating system
    ↓
Application runtime
    ↓
Deployment and operations

Apache needs to know exactly what is public.

PHP-FPM needs to run the application with the required extensions.

MySQL needs to be protected and accessible only to the components that require it.

And deployment needs to be reproducible.

Once this foundation is in place, deploying new Symfony versions becomes less about manually typing commands on a server and more about following a predictable process.

Official references