Symfony LEMP (Linux, nginx, PHP, MySQL) Setup

In this beginner friendly tutorial you will learn how to setup the LEMP stack on a new Digital Ocean Droplet.

For complete clarity, LEMP is:

  • Linux
  • nginx (pronounced Engine-x)
  • MySQL
  • PHP

PHP apps and Linux play very nicely together. We will be using Ubuntu flavoured Linux, but feel free to swap out for any distro you prefer. Please note that if you do not use Ubuntu, some of the demonstrated package management commands in this tutorial will not work.

We will make use of nginx as our webserver. nginx is pronounced Engine X, hence the E in the LEMP stack.

We will install MySQL for our database.

PHP 7.0 will be installed as part of this tutorial. This is not the latest and greatest, but is sufficient for our needs, and is available to install without additional complexity.

This is not intended to be a secure, or production optimised environment.

This is intended to get you comfortable with the basics of a LEMP stack, from which we can experiment with deploying (primarily Symfony) applications.

We will get onto the more real-world deployments later in this series.

Please note, this is not to be considered a secure, or optimised stack. This is for practice purposes only.

Using Digital Ocean

To begin with we are going to create a new Digital Ocean Droplet using the $10 plan, installing a fresh copy of Ubuntu 16.04.3 x64 as our virtual server / droplet Operating System.

You can follow this process on a different VPS provider such as Linode, or Amazon AWS.

If you do not yet have a Digital Ocean account, please create one now. By using this link you will get $10 initial credit, which is going to be more than enough to complete all the examples in this course. Full disclosure: this is an affiliate link.

Be sure to upload your SSH key(s) as available. If you do not have an SSH key, then please follow this guide.

Creating your new Droplet should take about 30 seconds.

Once done, you should be able to SSH into the box by running:

$ ssh root@

The authenticity of host ' (' can't be established.
ECDSA key fingerprint is SHA256:L6XYzRosi0Karlyq6vEYvnwGQF9FR/l40ChOrVtlY5k.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '' (ECDSA) to the list of known hosts.
Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-97-generic x86_64)

 * Documentation:
 * Management:
 * Support:

  Get cloud support with Ubuntu Advantage Cloud Guest:

0 packages can be updated.
0 updates are security updates.

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.


Cool, we are in.

Now, connecting as root is not good. But as mentioned, we are not concerned with security at this stage. In fact, once done here we will be destroying this droplet.

Because we are root, we don't need to use sudo before any of our commands. That's why you will see e.g. apt-get update, rather than sudo apt-get update sprinkled throughout this tutorial.

Our Fake Domain

Before we go further let's talk about the domain we will be using.

You can use a real domain name for this tutorial, if you have one.

You can use a subdomain of an existing domain, if you're feel adventurous (and know what you're doing / or there are no consequences if you break the root domain by accident).

But you can also make use of a host file entry. Or, to put it another way, you can save yourself a bit of cash and just make one up.

On Linux / Mac, to do this you need to edit your /etc/hosts file:

sudo vim /etc/hosts

At the bottom of this file add in a new line containing the IP address of your Digital Ocean Droplet, and the fake domain name you have made up.

If not using Vim then edit the /etc/hosts file accordingly.

The key combination in vim is rather strange.

shift + G

Takes you to the bottom of the open file.

o (the letter), makes a new line and puts you in INSERT mode.

From here, type as normal. In my case this line will be:

After this, press esc, then :wq to write and quit.

If at any point you make a mistake, press esc, then type :q! to quit without saving. Then just repeat the process till you get it right. Such is the beginners way with vim :D

At this point you should be able to ping your new fancy made up domain name and get a response from your real DO Droplet:

$ ping

PING ( 56(84) bytes of data.
64 bytes from ( icmp_seq=1 ttl=47 time=126 ms
64 bytes from ( icmp_seq=2 ttl=47 time=125 ms
64 bytes from ( icmp_seq=3 ttl=47 time=127 ms
--- ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2000ms
rtt min/avg/max/mdev = 125.997/126.603/127.064/0.447 ms

We're ready to roll.

nginx Server Setup

Switch back to your server's terminal session now.

To start with, let's make sure we have the latest list of available packages:

$ apt-get update

Anecdotally I've found choosing NYC-based Digital Ocean droplets to have a faster experience doing these sorts of commands. If it's taking you ages, consider trashing your Droplet and going again with an NYC-droplet.

Installing PHP

For Symfony 2 or Symfony 3, we can use PHP 7.0. Whilst not the very latest release, it is perfectly valid for our needs:

apt-get install -y php7.0 \
  php7.0-cli \
  php7.0-common \
  php7.0-mbstring \
  php7.0-gd \
  php7.0-fpm \
  php7.0-intl \
  php7.0-mcrypt \
  php7.0-mysql \
  php7.0-xml \

# or if you prefer a one-liner
apt-get install -y php7.0 php7.0-cli php7.0-common php7.0-mbstring php7.0-gd php7.0-intl php7.0-xml php7.0-mysql php7.0-mcrypt php7.0-zip

Ok, that's PHP done:

php -v
PHP 7.0.22-0ubuntu0.16.04.1 (cli) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2017 Zend Technologies
    with Zend OPcache v7.0.22-0ubuntu0.16.04.1, Copyright (c) 1999-2017, by Zend Technologies

Installing nginx

Next, install nginx:

$ apt-get install nginx

apt-get install nginx
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following package was automatically installed and is no longer required:
Use 'apt autoremove' to remove it.
The following additional packages will be installed:
  fontconfig-config fonts-dejavu-core libfontconfig1 libgd3 libjbig0 libjpeg-turbo8 libjpeg8 libtiff5 libvpx3 libxpm4 libxslt1.1 nginx-common
Suggested packages:
  libgd-tools fcgiwrap nginx-doc ssl-cert
The following NEW packages will be installed:
  fontconfig-config fonts-dejavu-core libfontconfig1 libgd3 libjbig0 libjpeg-turbo8 libjpeg8 libtiff5 libvpx3 libxpm4 libxslt1.1 nginx nginx-common
0 upgraded, 14 newly installed, 0 to remove and 17 not upgraded.
Need to get 3000 kB of archives.
After this operation, 9783 kB of additional disk space will be used.
Do you want to continue? [Y/n] y

This spills a bunch of stuff out to the terminal, then dumps you back to the prompt once done.

nginx is now installed.

Configuring nginx

We're going to be deploying a Symfony site.

Therefore, we need a Symfony nginx config.

cd /etc/nginx/conf.d

ls -la

total 12
drwxr-xr-x 2 root root 4096 Nov  4 11:33 .
drwxr-xr-x 6 root root 4096 Nov  4 11:33 ..

We're going to be using this as our site config:

server {
    listen 80 default;
    root /var/www/;

    location / {
        # try to serve file directly, fallback to app.php
        try_files $uri /app.php$is_args$args;

    # DEV
    # This rule should only be placed on your development environment
    # In production, don't include this and don't deploy app_dev.php or config.php
    location ~ ^/(app_.*|config)\.php(/|$) {
        fastcgi_pass unix:/run/php/php7.0-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;

    # PROD
    location ~ ^/app\.php(/|$) {
        fastcgi_pass unix:/run/php/php7.0-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;

    # return 404 for all other php files not matching the front controller
    # this prevents access to other php files you don't want to be accessible.
    location ~ \.php$ {
      return 404;

    error_log /var/log/nginx/crvfakeexample.com_error.log;
    access_log /var/log/nginx/crvfakeexample.com_access.log;

Copy this block to your clipboard. Change now if wanting to use a different domain name.

If this were a real deploy you would not want the dev section, so please remove as appropriate.

Now I will create a file to store this new config:


And then edit the file:


Once inside vim, press i. You should see -- INSERT -- at the bottom of your terminal window.

At this point you can right click in the window, and paste the contents in.

Next, press esc to exit insert mode, and then type :wq to write and quit. In other words, save and close.

Boom, done.

Check your nginx config with a config test:

service nginx configtest

 * Testing nginx configuration                                                                                                                [ fail ] 


Now, this process fails because there's already a site running on port 80. The default site.

When installing nginx on Ubuntu systems, a unusual default is used to mimic the Apache-style sites-available / sites-enabled setup. I don't like this.

To 'fix' this, I am going to delete the default nginx site from sites-enabled:

rm /etc/nginx/sites-enabled/default

Now, I can re-run the configtest, and because my new config uses port 80, and there is no longer an existing site using port 80, the conflict is resolved:

service nginx configtest

 * Testing nginx configuration                                                                                                                [ OK ] 

Now I need to load the new config into nginx:

service nginx reload

Lastly, to complete our LEMP stack, we will install MySQL.

Installing MySQL

We will need a database. We're going to use MySQL. Again, you can switch this out to anything else you like, so long as you are comfortable working with it.

apt-get install mysql-server

Follow the prompts.

You will be asked to provide a root password.

To make your life as easy as possible, use the password of pass.

This is the default value in use as part of the demo apps coming up during the deploy parts of this series.

Think of this password as being similar - but not the same - as the root user of your server. Only this time, this user will have access to all your database data.

It goes without saying that the root user's password should be something suitably complex.

Outside of this demo environment: Not pass.

Welp, that was easy enough.

Wrapping Up

We have a working LEMP environment in to which we can start deploying our Symfony / other PHP applications.

This environment is neither optimised, nor secure.

This is designed for practicing deployments only.

Server security is a complex and in-depth topic. I do not profess to be a security expert.

What I will demonstrate later in this series are ways you can leverage the expertise of security professionals. But for now, again this is a practice environment.