Binary Spaceship website

in Software Development

Complete installation guide for LEMP stack with PHP 7 in Debian 8 “Jessie”

This guide will teach you how to install and configure LEMP stack with Nginx 1.10, MySQL 5.7, PHP 7.1 in Debian 8 “Jessie” on Digital Ocean droplet. All steps are covered, from creation of droplet to securing it with firewall and SSL encryption. You can use instructions from the guide not only for droplet on Digital Ocean, but for any VPS hosting with Debian 8 “Jessie” as well.


Account on Digital Ocean or any other VPS hosting. If you are using other VPS hosting, you need to install Debian 8 image and enable SSH access on your server, and then proceed to step 3.

Step 1: Create droplet via Digital Ocean control panel

Log into your account on Digital Ocean and create new droplet by clicking “Create droplet” button:

Create dropletSelect Debian 8 as your droplet image:

Select image for dropletSelect suitable droplet billing plan. You can start with the cheapest and upgrade later to more expensive when you will really need it, without need to reinstall software from scratch.

Seelct billing plan for dropletSelect server location. The closer server is located to your target audience, the faster it will be loading for them. For example, if you are targeting Germans, choose Frankfurt datacenter here.

Select region for dropletDon’t forget to enable monitoring in “Select additional options” section. It is new feature on Digital Ocean, but you definitely want to use it. Seeing stats about loading of your website can be useful to prevent downtimes. Even more, monitoring is free of charge, so there is no reason not to enable it.

Select monitoring and, optionally, IPv6I recommend enabling IPv6 support as well (see why at,, but you can stay with IPv4, if you don’t care.

Optionally, you can setup SSH key for login onto your website right away. If you don’t provide SSH key at this point, you will be able to login with password that Digital Ocean will send you in email message after droplet creation. Later on you can add SSH keys or change provided password to something stronger.

Step 2: Establish SSH connection with server

During first time, you can use web interface to connect to your droplet. It can be found in your droplet control panel:

Run consoleClicking on that button will provide you simple SSH console directly in browser:

Web SSH console previewHowever, I recommend using openssh or PuTTY to connect to your droplet from your local machine. Using local ssh client will prevent from some console graphics glitches (I had some during installation of certbot) and provide capability to insert commands from clipboard. Personally I use openssh client that comes bundled inside Git for Windows. Command line interface of this ssh client is absolutely the same as openssh in Linux, so if you know how to use it in Linux, then learning curve is zero:
Login via local SSH client

Step 3: Install LEMP software on the server

Usually, people use abbreviation LEMP to describe server setup that uses Linux, Nginx, MySQL and PHP. We need to install this software, and also vsftpd for FTP access to our server.

  1. Nginx 1.10 as HTTP/HTTPS server.
  2. MySQL 5.7 as RDBMS.
  3. PHP-FPM 7.1 as scripts interpreter.
  4. vsftpd as FTP server.

You may ask, why not to use only SSH to transfer files to server, and use FTP as well? Because FTP times faster, see Also you will be able to update WordPress website that is hosted on your server by using FTP, provide restricted access to server files to 3rd party, etc.

3.1. Add external packages repositories

We need to add several additional repositories to list of known sources for `apt`. For example, PHP 7 in Debian 8 is hosted by third party (Ondřej Surý), and Nginx 1.10 is in Debian’s `testing` repository.

You will still remain on stable Debian 8 branch for other packages after installation, if you follow instructions.

3.1.1. Add PHP 7.1 repositories

Execute next script in shell to add PHP 7.1 repositories to apt:

apt-get install apt-transport-https ca-certificates
wget -O /etc/apt/trusted.gpg.d/php.gpg
echo "deb jessie main" > /etc/apt/sources.list.d/php.list

3.1.2. Add Nginx and vsftpd repositories

Execute next script in shell to add testing Debian 8 repositories to apt:

echo "APT::Default-Release "stable";" > /etc/apt/apt.conf.d/99default-release
echo "deb testing main contrib non-free" > /etc/apt/sources.list.d/debian-testing.list

3.1.3. Add MySQL repositories

Look for latest link to .deb installer with APT repository on MySQL official website, and substitute it for wget in following script, if it has been changed.

dpkg -i mysql-apt-config_0.8.4-1_all.deb
rm mysql-apt-config_0.8.4-1_all.deb

During .deb installation you will be asked about preferred version of MySQL. Select 5.7 (5.6 is default), because it provides significant performance boost (3x faster, accordingly to change list).

3.1.4. Update packages cache

Execute next script in shell to update apt packages cache, so you can use apt-get install with newly added packages:

apt-get update

3.2. Install required software

Execute next commands to install Nginx 1.10, MySQL 5.7 and PHP 7 in Debian 8:

apt-get -t testing install nginx
apt-get -t testing install vsftpd
apt-get install php7.1-fpm
apt-get install mysql-server

Remember, you need not PHP 7.1 (which comes with apache2 module), but PHP7.1-FPM binding for PHP 7 to work with Nginx.

It is important to install vsftpd not from stable, but from testing branch of Debian 8, because stable version of vsftpd contains major bug. Due to this bug, WordPress can’t update by FTP. You can find more details about this bug in ticket on WordPress issue tracker.

Step 4: Improve and customize software configuration

At this point we installed all required software for our server, but it is far from working condition. We have to configure it, so PHP7.1-FPM and Nginx could work together, vsftpd could provide access to website files via FTP and MySQL would allow remote access to database for easy database management.

4.1. Configure Nginx server

Let’s start with Nginx server configuration. Since we want to have secure webserver, we will use SSL encryption for our website, and HTTP version of site will redirect to HTTPS always, because benchmarks show that SSL encryption doesn’t add much overhead to loading times.

I suggest you to start with removing default Nginx configuration from enabled sites configurations by executing next command in console:

rm /etc/nginx/sites-enabled/default

Then, create website configurations in /etc/nginx/sites-available/ and add them as symbolic links to /etc/nginx/sites-enabled/ directory by executing next commands:

cd /etc/nginx/sites-available/
ln -s ../sites-available/ ../sites-enabled/

where is domain name of your website.

Here I provide real world configurations for Nginx. You can freely adapt them for your website.

Sample configuration for WordPress website:

server {

  listen 80;
  listen [::]:80;

  return 301$request_uri;

server {

  listen 443 ssl;
  listen [::]:443 ssl;

  ssl_certificate /etc/letsencrypt/live/;
  ssl_certificate_key /etc/letsencrypt/live/;

  root /var/www/;
  index index.php index.html index.htm;

  location ~ /\. {
    deny all;

  location ~* /(?:uploads|files)/.*\.php$ {
    deny all;

  location = /favicon.ico {
    log_not_found off;
    access_log off;

  location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;

  location / {
    try_files $uri $uri/ /index.php?$args;

  location ~ \.php$ {
    fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
    fastcgi_split_path_info ^(.+\.php)(/.*)$;
    include fastcgi_params;

    fastcgi_intercept_errors on;

    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    fastcgi_param DOCUMENT_ROOT $realpath_root;

  error_log /var/log/nginx/;
  access_log /var/log/nginx/;

Sample configuration for Symfony website (alternative to official configuration):

server {

  listen 80;
  listen [::]:80;

  return 301$request_uri;

server {

  listen 443 ssl;
  listen [::]:443 ssl;

  return 301$request_uri;

server {

  listen 443 ssl;
  listen [::]:443 ssl;

  ssl_certificate /etc/letsencrypt/live/;
  ssl_certificate_key /etc/letsencrypt/live/;

  root /var/www/;

  location / {
    try_files $uri /app.php$is_args$args;

  location ~ ^/app\.php(/|$) {
    fastcgi_pass unix:/var/run/php/php7.1-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;


  location ~ \.php$ {
    return 404;

  location ~ /\.ht {
    deny all;

  error_log /var/log/nginx/;
  access_log /var/log/nginx/;

And the last, but not least, step is to give proper permissions to Nginx log directories and files. Nginx uses www-data user for access to filesystem, so you need to execute next commands:

cd /var/log/nginx
chown www-data:adm

Execute ls -l to verify that directory has correct permissions. You need to have something like next:

drwxr-xr-x 2 www-data adm 4096 Apr 9 06:25

Don’t forget to restart Nginx by running service nginx restart. After that you can check if it is running correctly by executing service nginx status.

4.2. Configure PHP 7

Before changing any configuration, install all PHP extension that you may need. For example, I installed all necessary extensions for me by executing next commands:

apt-get install php-apcu
apt-get install -t testing php7.1-opcache
apt-get install -t testing php7.1-xml
apt-get install -t testing php7.1-readline
apt-get install -t testing php7.1-mysql
apt-get install -t testing php7.1-mbstring
apt-get install -t testing php7.1-json
apt-get install -t testing php7.1-intl
apt-get install -t testing php7.1-gd

After that you need to make at least minor changes to your /etc/php/7.1/fpm/php.ini.

Configure default timezone for PHP:

date.timezone = Europe/Berlin

You can check to find your timezone name.

Don’t forget to restart PHP7.1-FPM by running service php7.1-fpm restart. After that you can check if it is running correctly by executing service php7.1-fpm status.

4.3. Configure vsftpd

The only configuration file for the FTP server is /etc/vsftpd.conf.

Use next sample configuration for vsftpd to enable passive transferring mode and SSL for FTP:

# Example config file /etc/vsftpd.conf
# The default compiled in settings are fairly paranoid. This sample file
# loosens things up a bit, to make the ftp daemon more usable.
# Please see vsftpd.conf.5 for all compiled in defaults.
# READ THIS: This example file is NOT an exhaustive list of vsftpd options.
# Please read the vsftpd.conf.5 manual page to get a full idea of vsftpd's
# capabilities.
# Run standalone? vsftpd can run either from an inetd or as a standalone
# daemon started from an initscript.
# This directive enables listening on IPv6 sockets. By default, listening
# on the IPv6 "any" address (::) will accept connections from both IPv6
# and IPv4 clients. It is not necessary to listen on *both* IPv4 and IPv6
# sockets. If you want that (perhaps because you want to listen on specific
# addresses) then you must run two copies of vsftpd with two configuration
# files.
# Allow anonymous FTP? (Disabled by default).
# Uncomment this to allow local users to log in.
# Uncomment this to enable any form of FTP write command.
# Default umask for local users is 077. You may wish to change this to 022,
# if your users expect that (022 is used by most other ftpd's)
file_open_mode=0666 # Important! Proper umask for uploaded files.
# Uncomment this to allow the anonymous FTP user to upload files. This only
# has an effect if the above global write enable is activated. Also, you will
# obviously need to create a directory writable by the FTP user.
# Uncomment this if you want the anonymous FTP user to be able to create
# new directories.
# Activate directory messages - messages given to remote users when they
# go into a certain directory.
# If enabled, vsftpd will display directory listings with the time
# in your local time zone. The default is to display GMT. The
# times returned by the MDTM FTP command are also affected by this
# option.
# Activate logging of uploads/downloads.
# Make sure PORT transfer connections originate from port 20 (ftp-data).
# If you want, you can arrange for uploaded anonymous files to be owned by
# a different user. Note! Using "root" for uploaded files is not
# recommended!
# You may override where the log file goes if you like. The default is shown
# below.
# If you want, you can have your log file in standard ftpd xferlog format.
# Note that the default log file location is /var/log/xferlog in this case.
# You may change the default value for timing out an idle session.
# You may change the default value for timing out a data connection.
# It is recommended that you define on your system a unique user which the
# ftp server can use as a totally isolated and unprivileged user.
# Enable this and the server will recognise asynchronous ABOR requests. Not
# recommended for security (the code is non-trivial). Not enabling it,
# however, may confuse older FTP clients.
# By default the server will pretend to allow ASCII mode but in fact ignore
# the request. Turn on the below options to have the server actually do ASCII
# mangling on files when in ASCII mode.
# Beware that on some FTP servers, ASCII support allows a denial of service
# attack (DoS) via the command "SIZE /big/file" in ASCII mode. vsftpd
# predicted this attack and has always been safe, reporting the size of the
# raw file.
# ASCII mangling is a horrible feature of the protocol.
# You may fully customise the login banner string:
#ftpd_banner=Welcome to blah FTP service.
## You may specify a file of disallowed anonymous e-mail addresses. Apparently
# useful for combatting certain DoS attacks.
# (default follows)
# You may restrict local users to their home directories. See the FAQ for
# the possible risks in this before using chroot_local_user or
# chroot_list_enable below.
# You may specify an explicit list of local users to chroot() to their home
# directory. If chroot_local_user is YES, then this list becomes a list of
# users to NOT chroot().
# (Warning! chroot'ing can be very dangerous. If using chroot, make sure that
# the user does not have write access to the top level directory within the
# chroot)
# (default follows)
# You may activate the "-R" option to the builtin ls. This is disabled by
# default to avoid remote users being able to cause excessive I/O on large
# sites. However, some broken FTP clients such as "ncftp" and "mirror" assume
# the presence of the "-R" option, so there is a strong case for enabling it.
# Customization
# Some of vsftpd's settings don't fit the filesystem layout by
# default.
# This option should be the name of a directory which is empty. Also, the
# directory should not be writable by the ftp user. This directory is used
# as a secure chroot() jail at times vsftpd does not require filesystem
# access.
# This string is the name of the PAM service vsftpd will use.

# Enable SSL


# Enable passive transferring mode


# Show hidden files for FTP client


Notice that you also set custom umask in configuration, so uploaded files have proper permissions.

After that, generate SSL certificate for FTP server. To do this, create directory for SSL certificates, if it doesn’t exist yet:

mkdir -p /etc/ssl/private
chmod 700 /etc/ssl/private

Afterwards, we can generate the SSL certificate by executing next command:

openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout /etc/ssl/private/vsftpd.pem -out /etc/ssl/private/vsftpd.pem

vstfpd uses Linux authentication mechanism to provide access to FTP server, so in order to connect to your FTP, create new user:

useradd ftpguy
passwd ftpguy

All new uploaded files will belong to that user. However, we will need to access folders that were created by this user from www-data user, which is used by PHP7.1-FPM and Nginx. To provide permissions for www-data user, you need to add it to ftpguy group:

usermod -a -G ftpguy www-data

vsftpd displays home directory of ftpguy user in FTP client, so you can’t reach root of filesystem. However, sometimes you need to access some upper level directory, e.g. /var/www/ directory. To do this, you can mount external directory inside home directory of FTP user, by using next commands:

cd /home/ftpguy
chown ftpguy:ftpguy
mount --bind /var/www/ /home/ftpguy/

Don’t forget to restart vsftpd by running service vsftpd restart. After that you can check if it is running correctly by executing service vsftpd status.

4.4. Configure MySQL

In my setup I connect to MySQL server remotely from my local machine by using HeidiSQL. It is really the best SQL client I tried, and I tried about dozen of them so far.

Connect to MySQL using HeidiSQLBefore configuring MySQL, let’s remove its default trash and prepare for production by executing next command:


Then, we need to configure MySQL in such way that it will accept remote connections. To do this, open /etc/mysql/mysql.conf.d/mysqld.cnf in your favorite text editor (vim, emacs, nano, etc.) and comment next line with # symbol:

#bind-address =

Use default MySQL client to create user for remote connection:

mysql -u root -p

SQL code to create new user and give them required permissions is next:

CREATE USER 'newuser'@'%' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON mydatabase.* TO 'newuser'@'%';

In the best case, you shouldn’t create a user that has all rights to all databases and tables and can be used from remote machine! This can lead to serious security flaws, because if somebody gets your user password, he will control what is going on in all your databases. So, create admin user for each specific database and limit responsibilities to new users to prevent security problems.

Execute next command to create initial performance schema table in MySQL 5.7, so you will not encounter Table ‘performance_schema.session_variables’ doesnt exist problem with HeidiSQL or other MySQL clients:

mysql_upgrade -u root -p --force

We will not open port 3306 in firewall for access to MySQL. This is because by default MySQL doesn’t encrypt connection to server, so your passwords and sensitive information is vulnerable to man-in-the-middle attack. To prevent this, you have 4 ways:

  1. Disable remote access to MySQL completely by having bind-address = inside MySQL configuration (safest, and should be used if you don’t plan to do any last minute fixes in your database).
  2. Configure SSL encryption for MySQL port.
  3. Configure VPN network for access to server machine.
  4. Use SSH tunnel to connect to MySQL.

For my setup I needed remote access to MySQL, so going first way wasn’t an option. Configuring SSL encryption for MySQL or VPN network is a bit complicated, but sometimes useful. SSH tunneling for MySQL connections is a king, since it is easy to configure on server side (just by commenting out bind-address, what we already done) and works pretty fast on client.

To use SSH tunnel in HeidiSQL you will need to select “MySQL (SSH tunnel)” in “Network type” combobox, and then specify SSH parameters in “SSH tunnel” tab:

HeidiSQL SSH Tunnel ConfigurationDon’t forget to restart MySQL by running service mysql restart. After that you can check if it is running correctly by executing service mysql status.

Step 5: Add SSL certificates to server

If you looked carefully to Nginx configuration, you could notice that it uses SSL certificates to secure HTTPS access. These certificates can be obtained by one of three ways:

  1. You can generate them yourself, but this way browsers will not show green lock icon in location bar, so you website will be considered not secured.
  2. You can buy SSL certificates from trustful security provider like Comodo.
  3. You can use free SSL certificates from Let’s Encrypt initiative.

If you are running serious business, stick with SSL certificates from professional security provider, because SSL certificates from Let’s Encrypt don’t include ownership information. However, if you are running blog, pet project or early stage startup, Let’s Encrypt certificates will be breath of fresh air for you, because they easy to install, use, and free of charge.

You can install Let’s Encrypt certificate by using certbot application. It is available in testing branch of Debian 8 “Jessie”:

apt-get -t testing install certbot

You can generate certificates for your website by simply running:

certbot certonly --webroot -w /var/www/ -d -d -w /var/www/ -d

After that you will receive congratulations message that your certificates were generated successfully:certbot congratulations messageYou will find new certificates inside /etc/letsencrypt/live/, where is name of your website, of course.

The only minor problem for me with certbot is that it is implemented as Python script, so I need to have Python interpreter on my server. I saw several implementation of certbot alternatives in PHP and Bash script language, but they are not as reliable and well-supported as certbot at the moment, unfortunately.

Step 6: Install firewall to secure the server

There are several firewalls available for Linux, but I prefer ufw, because it is fast to install and easy to use:

apt-get install ufw

After installation of ufw, enable it for IPv6 by modifying /etc/default/ufw configuration. You need to set next option where:

# Set to yes to apply rules to support IPv6 (no means only IPv6 on loopback
# accepted). You will need to 'disable' and then 'enable' the firewall for
# the changes to take affect.

Enable access for SSH, FTP, HTTP, HTTPS, FTP passive ports. It is very easy in ufw and can be done by executing next commands:

ufw allow 22

ufw allow 80/tcp

ufw allow 443

ufw allow 21/tcp

# FTP passive ports
ufw allow 62100:62150/tcp

Execute ufw status numbered to see that rules are properly set:

Status: active

     To                         Action      From
     --                         ------      ----
[ 1] 22                         ALLOW IN    Anywhere
[ 2] 80/tcp                     ALLOW IN    Anywhere
[ 3] 443                        ALLOW IN    Anywhere
[ 4] 21/tcp                     ALLOW IN    Anywhere
[ 5] 62100:62150/tcp            ALLOW IN    Anywhere
[ 6] 22                         ALLOW IN    Anywhere (v6)
[ 7] 80/tcp                     ALLOW IN    Anywhere (v6)
[ 8] 443                        ALLOW IN    Anywhere (v6)
[ 9] 21/tcp                     ALLOW IN    Anywhere (v6)
[10] 62100:62150/tcp            ALLOW IN    Anywhere (v6)

Then, reload ufw configuration by executing:

sudo ufw disable
sudo ufw enable

Step 7: Connect your website to CDN

You may want to use CDN to deliver your website content to users faster. If you need free and simple CDN, I recommend using Cloudflare.

I will not cover installation of website on Cloudflare in this article, but you may find necessary information on Cloudflare website in Cloudflare 101 section.

However, since we use redirection from HTTP to HTTPS version of website in Nginx, there is simple configuration point that you have to know to prevent Cloudflare looping problem. Looping problem causes web browser to receive code 301 Moved Permanently constantly when your website uses Cloudflare.

Cloudflare SSL loopYou need to select Full SSL mode in “Crypto” tab in your website profile to prevent SSL access loop, because in Flexible mode Cloudflare will try to access HTTP version of your website and don’t understand how to behave correctly if you redirect it to HTTPS version of site in Nginx.


Binary Spaceship website on LEMP serverHopefully by this point you have working server with LEMP (Nginx 1.10, MySQL 5.7, PHP 7.1) in Debian 8. Also your server is secured with firewall and SSL encryption. You have remote access to your MySQL database and ready to meet every challenge that may occur during development using PHP 7. Your website is available with high speed in any point around the world due to Cloudflare CDN. My congratulations!

Update: You can continue to harden your security by enabling fail2ban and following more advanced Linux security tips. For example, see,

Write a Comment


This site uses Akismet to reduce spam. Learn how your comment data is processed.

  1. Instead of manually setting up a PHP server on DigitalOcean isn’t it better to use some server provisioning or managed services to start a server? It is much quicker and easier this way, here is an example: .Not only this saves time, but also saves money that otherwise would have gone into hiring sysadmin team for maintaining the server. So, is there any benefit of going directly to DigitalOcean instead of using such services?

    • You can use Ansible to provision your droplet (, Chef, Puppet, rsync, Cloudways or whatever. This will require less time for you to setup initial version of system, but you will need to tweak it afterwards in 99% of cases (e.g. provide additional Ansible configurations for your specific project like what version of MySQL/PHP you need, what plugins to use, etc.). In long run, provisioning by using specialized software costs less and takes less time, so I would definitely recommend you using something 🙂 However, if you ever need to know what is going on from bottom to top (even inside your Ansible/Chef/etc. scripts), this guide can give you the idea.


  • How to setup LEMP stack with PHP 7 in Debian Linux 8 “Jessie” January 17, 2018

    […] Read more:Complete installation guide for LEMP stack with PHP 7 in Debian 8 “Jessie” […]

  • cron.weekly issue #77: OpenStack, Moby, Caddy, Devuan, Linuxkit, Tmux, Jenkins & more January 17, 2018

    […] Complete installation guide for LEMP stack with PHP 7 in Debian 8 “Jessie” […]

  • Complete installation guide for LEMP stack with PHP 7 in Debian 8 “Jessie” – Blog – Binary Spaceship – myDigiDump January 17, 2018

    […] […]