Categories
Case Studies

How to Configure your Web Server and Install WordPress on Digital Ocean (full step-by-step guide)

Last Updated 2/19/2021 TypeExpertiseReading TimeCompletion TimeTutorialIntermediate40 - 60 minutes2 - 4 hours Table of Contents PrerequisitesIntroductionDigital OceanCreate a New ProjectCreate a New Cloud Server (Droplet)Add a DomainUpdate Domain DNS Records (e.g.

Codeable.io

Last Updated 2/19/2021

TypeExpertiseReading TimeCompletion Time
TutorialIntermediate40 – 60 minutes2 – 4 hours

Table of Contents

  1. Prerequisites
  2. Introduction
  3. Digital Ocean
  4. Cloud Server
  5. Web Server
  6. WordPress
  7. Addons
  8. Conclusion

1. Prerequisites

This is an intermediate-level tutorial. To follow and complete all the topics discussed, you must have some previous Unix/Linux experience under your belt.

There are a couple of advanced items at the end, but most of the topics shouldn’t be a problem to follow for someone that’s using Ubuntu as their primary OS on his home/workplace machine.

I would recommend going over the basic Linux commands and reading the Command Line for Beginners by Ubuntu for beginner users. Then install the WordPress App from Digital Ocean, and after you feel comfortable within the command line, you come back and try to cover some of the topics discussed in this tutorial.

If you check all the items below and have a basic understanding of each one, you should be able to follow along with this tutorial.

  • Feel comfortable with the Terminal.
  • Navigate the Linux filesystem.
  • File manipulations – manage (add, edit, remove), preview, edit, copy and move files and directories.
  • Set ownership and permissions.
  • Understand the Linux Filesystem Hierarchy Standard – know where are your configs, logs, executables, etc. located.
  • Basic understanding of ufw Linux Firewall, apt package manager, and systemctl service manager.
  • Be able to find help and read the man pages for programs and packages available on your system.

2. Introduction

This tutorial will show you how to set up, install, and configure your cloud server on Digital Ocean and set up and secure your first WordPress website.

Moreover, you will learn how to configure Postfix with IMAP-server and RoundCube, install PHPMyAdmin, switch from Apache to Nginx, and install and configure a secure FTP connection.

This is a full step-by-step tutorial that shows you how to do it on a clean Ubuntu 20.04.2 LTS installation.

If you wish to have an automated installation, you should look at the WordPress App available on the Digital Ocean marketplace. However, I cover many more topics and programs in this tutorial, so once you install the app and play with it for a bit, you may want to come back to this tutorial again and try to build on what you have.

3. Digital Ocean

After creating a new account with Digital Ocean and logging in to your web interface, you are ready to create and set up your first cloud server (Droplet). Below I will describe this process step-by-step.

The green button Create in the top toolbar can access all the items below.

Create a New Project

  • Name your project: PROJECT NAME
  • Add a description: Some description…
  • Tell us what it is for: Website or blog
  • Click on the Create Project button
  • Skip Move resources into…

Create a New Cloud Server (Droplet)

  • Select a distribution: Ubuntu
  • Choose a plan: Basic $5
  • Choose a datacenter region: Select One
  • Select additional options: IPv6, Monitoring
  • Authentication: Password
  • Create root password:
  • Choose a hostname: projectname
  • Select project: PROJECT NAME

Add a Domain

  • Enter domain: domain.com
  • Select project: PROJECT NAME
  • Add A record
    • Hostname: @
    • Will director to: IP Address
    • TTL (seconds): 3600
  • Click on the Create Record button
  • Add CNAME record
    • Hostname: www
    • Is an alias of: @
    • TTL (seconds): 43200
  • Click on the Create Record button

The final domain DNS records table should look similar to the output below.

CNAME   www.domain.com   is an alias of   domain.com.           43200 
A	domain.com       directs to       111.111.111.111        3600
NS	domain.com       directs to       ns1.digitalocean.com   1800
NS	domain.com       directs to       ns2.digitalocean.com   1800
NS	domain.com       directs to       ns3.digitalocean.com   1800

Update Domain DNS Records (e.g. Google Domains)

You need to change your domain name DNS records to point to the Digital Ocean nameservers. For example, if you host your domain on Google Domains, you need to do the following:

  • go to My Domain > DNS (from the side menu)
  • Check Use custom name servers:
  • Click on the + sign to add a new nameserver and add each of the following in a separate row: ns1.digitalocean.com, ns2.digitalocean.com, ns3.digitalocean.com
  • At the end click on the Save button.

Login to your Cloud Server

At this point, all the configurations within the Digital Ocean web interface are completed, and we are ready to log in to our cloud server for the first time. We will use the root user with the password set when creating our Droplet.

If you are new to SSH, the easiest way to connect to your cloud server is with a program called PuTTY on Windows or Cyberduck on Mac. If you are a Linux user, you should probably know how to connect with OpenSSH.

Here is an example of step-by-step logging in with PuTTY on Windows.

  • Download PuTTY
  • Open the PuTTY executable file and enter:
    • Host Name (or IP address): Droplet IP Address or domain
    • Port: 22
    • Connection type: SSH
    • Saved Session: Digital Ocean Project Name
    • Click on the Save button for later use
    • Click on the Open button

Once you enter the root and correct password, you will be logged in successfully to your cloud server.

login as: root
root@111.111.111.111's password:
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-65-generic x86_64)

4. Cloud Server

Before setting up our Web Server, we should do some initial server updates and configuration.

Update and Upgrade your Server

Using the apt package manager, you must update and upgrade all the software initially installed on the server (answer all the questions you get during the process with Y).

# apt update && apt upgrade

To not compromise your server security, all the applications and software installed must be up to date, and you will need to run the above command regularly, create a cron job and automate the process.

Add New User and Disable root Login

Since we use the root password authentication method and do not authenticate with public and private SSH keys, it is best to disable our root access and improve our cloud server security level.

# nano /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication yes

Moreover, we need to create a new superuser that will be used to access and control our server.

The following sequence will create and test the user and restart the sshd service.

Next time you log in, you won’t do it as root. You should use the newly created superuser to log in and then type su username, so you don’t run all your commands with root access.

# adduser username
New password:
Retype new password:
passwd: password updated successfully
Changing the user information for username
Enter the new value, or press ENTER for the default
  Full Name []: 
  Room Number []:
  Work Phone []:
  Home Phone []:
  Other []:
Is the information correct? [Y/n] Y
# usermod -aG sudo,www-data username
# su username
Password:
$ id username
uid=1000(username) gid=1000(username) groups=1000(username),27(sudo),33(www-data)
$ exit
# systemctl restart sshd
# exit

If you mess up anything or forget your password, don’t worry, you are not completely locked out. Go to your Digital Once Droplet web interface click on the Access link from the side menu.

This is where you can either select to Launch Recovery Console or Reset Root Password and regain access to your cloud server Droplet.

DNS Propagation Check

Before we go ahead and start installing and configuring our Web Server, let us check if the domain name we have added earlier is pointing to the correct IP Address.

Sometimes DNS changes may take up to 72 hours to see the result on our end.

Firstly, we can run the ip addr show eth0 command to preview the IP Address of our cloud server. And then type any of the commands below to check if the domain IP Address matches the one from our cloud server.

# ip addr show eth0 | grep inet | awk '{ print $2; }' | sed 's//.*$//'
111.111.111.111
# dig domain.com
;; ANSWER SECTION:
domain.com.        3600    IN      A       111.111.111.111
# nslookup domain.com
Non-authoritative answer:
Name:    domain.com
Address: 111.111.111.111
# ping domain.com
PING domain.com (111.111.111.111) 56(84) bytes of data.
64 bytes from domain (111.111.111.111): icmp_seq=1 ttl=64 time=0.017 ms

Sometimes there is an issue with your local machine DNS cache, and you may still see the old domain.com loading in your browser even the propagation was completed. To resolve this search for how to flush DNS cache on Google, you will find the correct solution for your OS.

5. Web Server

From now on, you must run all root commands and software access with sudo before them. After you log in, you should start with su username (once in a while, you will be asked to re-enter your user password for authentication).

Install All the Required Software

Firstly, we need to install all the software we need for our Web Server. To do this, we will use the apt package manager again.

$ sudo apt install apache2 mysql-server php php-mysql certbot python3-certbot-apache postfix s-nail 
  • Confirm all questions with Y
  • When you get the Postfix initial configuration just select OK > Internet Site > OK and domain.com for mail name.

If you are stuck and don’t know how to use any program, or you wonder if it is even installed on your system, you can run man <command> or <command> --help. In addition, typing -v or –version after each program or command will show you the installed version. Or if you know the package name type sudo apt show <package> , see detailed information about the package.

As a reference, below are all the software versions for the Web Server I am using for this tutorial.

$ sudo cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.2 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.2 LTS"
VERSION_ID="20.04"
$ sudo apache2 -v
Server version: Apache/2.4.41 (Ubuntu)
Server built:   2020-08-12T19:46:17
$ sudo mysql --version
mysql  Ver 8.0.23-0ubuntu0.20.04.1 for Linux on x86_64 ((Ubuntu))
$ sudo php -v
PHP 7.4.3 (cli) (built: Oct  6 2020 15:47:56) ( NTS )
$ sudo postconf -d | grep mail_version
mail_version = 3.4.13
$ sudo certbot --version
certbot 0.40.0

Configure the Linux Firewall

We are now ready to configure, open some ports, and enable our firewall. This is something we need to do to allow the outside world to access our cloud server.

$ sudo ufw app list
Available applications:
  Apache
  Apache Full
  Apache Secure
  OpenSSH
  Postfix
  Postfix SMTPS
  Postfix Submission
$ sudo ufw status
Status: inactive
$ sudo ufw limit 'OpenSSH'
$ sudo ufw allow 'Apache Full'
$ sudo ufw allow 'Postfix'
$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active
To                         Action      From
--                         ------      ----
OpenSSH                    LIMIT       Anywhere
Apache Full                ALLOW       Anywhere
Postfix                    ALLOW       Anywhere
OpenSSH (v6)               LIMIT       Anywhere (v6)
Apache Full (v6)           ALLOW       Anywhere (v6)
Postfix (v6)               ALLOW       Anywhere (v6)
$ sudo systemctl status apache2
● apache2.service - The Apache HTTP Server
   Loaded: loaded (/lib/systemd/system/apache2.service; enabled; vendor preset: enabled)
   Active: active (running) since Sat 2021-02-13 08:30:58 UTC; 13h ago

If you complete all the steps above and the apache service is active and running as a background process, you can go ahead, open your favorite browser and enter http://domain.com. This will show you the default Apache web server index page. The document root for the default Apache server is located at /var/www/html.

If Apache isn’t running, you should start it with sudo systemctl start apache2.

Create and Configure a Virtual Host

If you are running only one domain within a Droplet, you don’t need to set up virtual hosts. However, if you plan to have subdomains or host multiple websites on a single cloud server, then learning to create and configure a virtual host is a must.

The process is pretty straightforward, and after you do it a couple of times, you should be able to add a new virtual host in no time. Here are the steps required:

  1. Create the file and directory structure.
  2. Assign the appropriate user and group ownership.
  3. Create a config file and enable your virtual host within Apache.
$ sudo chmod -R 755 /var/www
$ sudo mkdir -p /var/www/domain.com/html/.logs
$ sudo chown -R www-data:www-data /var/www/domain.com/html
$ sudo chown -R $USER:$USER /var/www/domain.com/html/.logs
$ sudo nano /var/www/domain.com/html/index.html
<html><head><title>Hello World!</title></head><body><h1>Hello World</h1></body></html>
$ sudo nano /var/www/domain.com/html/phpinfo.php
<?php phpinfo(); ?>
$ sudo cp /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/domain.com.conf
$ sudo nano /etc/apache2/sites-available/domain.com.conf
<VirtualHost *:80>
  ServerName domain.com
  ServerAlias www.domain.com
  ServerAdmin webmaster@domain.com
  DocumentRoot "/var/www/domain.com/html"
  DirectoryIndex index.php index.html
  <Directory "/var/www/domain.com/html">
    Options Indexes FollowSymLinks MultiViews
    AllowOverride All
    Order deny,allow
    Allow from all
  </Directory>
  ErrorLog "/var/www/domain.com/html/.logs/error.log"
  CustomLog "/var/www/domain.com/html/.logs/access.log" common
</VirtualHost>
$ sudo a2dissite 000-default.conf
$ sudo a2ensite domain.com.conf
$ sudo systemctl restart apache2

There are a couple more steps added above for you to visualize the final result, but at this point, all your files associated with http://domain.com will be served from /var/www/domain.com/html/ instead of /var/www/html.

If you wish to add another domain or subdomain, you follow the same procedure just replace domain.com with the new domain or subdomain name.

Add SSL Support for https:// with HTTP/2

Serving your site via SSL isn’t any more a recommendation nowadays but a requirement. For this, we will use the certbot program we installed earlier, and the process is straightforward to follow.

$ sudo certbot --apache -d domain.com -d www.domain.com
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): webmaster@domain.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about our work
encrypting the web, EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2
$ sudo certbot renew --dry-run
$ sudo cat /etc/apache2/sites-available/domain.com-le-ssl.conf
<IfModule mod_ssl.c>
  <VirtualHost *:443>
    ServerName domain.com
    ServerAlias www.domain.com
    ServerAdmin webmaster@domain.com
    DocumentRoot "/var/www/domain.com/html"
    DirectoryIndex index.php index.html
    <Directory "/var/www/domain.com/html">
      Options Indexes FollowSymLinks MultiViews
      AllowOverride All
      Order deny,allow
      Allow from all
    </Directory>
    ErrorLog "/var/www/domain.com/html/.logs/error.log"
    CustomLog "/var/www/domain.com/html/.logs/access.log" common
    SSLCertificateFile /etc/letsencrypt/live/domain.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/domain.com/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf
  </VirtualHost>
</IfModule>

The last two commands verify if the certificate is generated successfully and show you the new config file with SSL support.

Now you can open your site with https://, and all the http:// requests will be automatically redirected to https:// as specified above.

Enable HTTP/2

The default Apache 2.4 doesn’t come with HTTP/2 support enabled. Luckily making the switch from HTTP/1.1 to HTTP/2 is not that complicated.

The steps we need to follow to do the switch are:

  • Tell Apache to use PHP FastCGI
  • Change MPM from “prefork” to “event”
  • Update your SSL Virtual Host file
  • Enable the mod_http2 Apache module
$ sudo add-apt-repository ppa:ondrej/apache2
$ sudo apt install php7.4-fpm
$ sudo a2enmod proxy_fcgi setenvif
$ sudo a2enconf php7.4-fpm
$ sudo a2dismod php7.4
$ sudo a2dismod mpm_prefork 
$ sudo a2enmod mpm_event  
$ sudo nano /etc/apache2/sites-available/domain.com-le-ssl.conf
#Add the following configuration lines at end of the file
    
    Protocols h2 h2c http/1.1
  </VirtualHost>
</IfModule>
$ sudo a2enmod http2
$ sudo systemctl restart php7.4-fpm
$ sudo systemctl restart apache2

To test if you have successfully enabled HTTP/2, you can either go to your browser DevTools and check the Header Request for any of the loaded resources or just run the command below, and you should get HTTP/2 200.

$ sudo curl -sSL -D - domain.com -o /dev/null
HTTP/2 200
date: Fri, 19 Feb 2021 22:58:16 GMT
server: Apache/2.4.41 (Ubuntu)
link: <https://perfomentor.com/wp-json/>; rel="https://api.w.org/"
cache-control: max-age=172800
expires: Sun, 21 Feb 2021 22:58:16 GMT
vary: Accept-Encoding
content-type: text/html; charset=UTF-8

Configure and Secure the MySQL Server

Let us move along to configure and secure our MySQL server. As you might already know, we need a username, password, and database to install WordPress. But before we do that, let us improve the security of our MySQL Server and update the root password.

If MySQL isn’t running, you should start it with sudo systemctl start mysql.

$ sudo systemctl status mysql
● mysql.service - MySQL Community Server
  Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: enabled)
  Active: active (running) since Sat 2021-02-13 08:14:36 UTC; 14h ago
$ sudo mysql_secure_installation
Press y|Y for Yes, any other key for No: Y
Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 1
New password:
Re-enter new password:
Remove anonymous users? (Press y|Y for Yes, any other key for No) : Y
Disallow root login remotely? (Press y|Y for Yes, any other key for No) : Y
Remove test database and access to it? (Press y|Y for Yes, any other key for No) : Y
Reload privilege tables now? (Press y|Y for Yes, any other key for No) : Y
$ sudo mysql -u root -p
mysql> SELECT user, authentication_string, plugin, host FROM mysql.user;
mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
mysql> FLUSH PRIVILEGES;
mysql> SELECT user, authentication_string, plugin, host FROM mysql.user;

Of course, replace your username and password with your own.

It is not recommended, but if you wish your user to access all databases, you can replace the WordPress.* with *.*.

And then, if you want to limit the privileges, you can replace ALL with any of the actions from the following list: CREATE, ALTER, DROP, INSERT, UPDATE, DELETE, SELECT, REFERENCES, RELOAD.

mysql> CREATE DATABASE wordpress;
mysql> CREATE USER 'username'@'localhost' IDENTIFIED BY 'password';
mysql> GRANT ALL PRIVILEGES ON wordpress.* TO 'username'@'localhost';
mysql> FLUSH PRIVILEGES;
mysql> QUIT;
$ sudo mysql -u username -p
Enter password:
mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| wpblueprint        |
+--------------------+
2 rows in set (0.01 sec)
mysql> QUIT;

Configure PHP and Manage Extensions

I won’t be able to cover everything here regarding php.ini, but I want to give you some common PHP configuration settings you might want to update. Also, a way to preview, add, and remove extensions and install packages with apt.

I am using PHP 7.4 as of the writing of this tutorial, but if you have a newer version, you should change the path to php.ini appropriately.

$ sudo nano /etc/php/7.4/apache2/php.ini
;Add the following configuration lines at end of the file

register_globals = Off 
allow_url_fopen = On
memory_limit = 128M
max_execution_time= 300
max_input_time= -1
upload_max_filesize= 25M
post_max_size= 20M

The first two commands can search or preview installed PHP modules and packages. Then you can use either a2enmod or a2dismod to manage them.

$ php -m
$ sudo dpkg --get-selections | grep -v deinstall | grep php
$ sudo a2enmod deflate
$ sudo a2enmod rewrite
$ sudo a2dismod headers
$ sudo systemctl restart apache2

If you need to install a PHP extension as a module, you can use the apt package manager to search and install or remove it.

$ sudo apt search php-
$ sudo apt install <php-module>
$ sudo apt remove <php-module>

After removing a package, run sudo apt auto-remove to get rid of all the dependencies left on your server.

Install and Configure PHP OPcache

Opcache is a powerful PHP extension used to increase PHP performance by storing precompiled script bytecode in shared memory.

$ sudo apt install php-opcache
$ sudo nano nano /etc/php/7.4/apache2/php.ini
;Add the following configuration lines at end of the file

opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=3000
opcache.revalidate_freq=200
$ sudo systemctl restart apache2

Upgrade to PHP 8

[Coming Soon]

6. WordPress

Our initial Web Server should now be configured, and we can go ahead, install and configure our WordPress.

Install WP-CLI

Firstly, let us install WP-CLI to speed up the process. Of course, you can do this by downloading the latest WordPress vision, unzipping, and then installing via your browser, but since we are in the console, using WP-CLI is much more efficient.

The commands below will download, set permissions and make the WP-CLI executable global to our system.

$ sudo curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
$ sudo chmod +x wp-cli.phar
$ sudo mv wp-cli.phar /usr/local/bin/wp
$ sudo wp --info
OS:     Linux 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 UTC 2021 x86_64
Shell:  /bin/bash
PHP binary:     /usr/bin/php7.4
PHP version:    7.4.3
php.ini used:   /etc/php/7.4/cli/php.ini
WP-CLI root dir:        phar://wp-cli.phar/vendor/wp-cli/wp-cli
WP-CLI vendor dir:      phar://wp-cli.phar/vendor
WP_CLI phar path:       /home/username
WP-CLI packages dir:
WP-CLI global config:
WP-CLI project config:
WP-CLI version: 2.4.0

Install and Configure WordPress

Once we have WP-CLI installed, we will install and configure our WordPress. I will run all the commands as root and then, in the next section, fix all the permissions and ownership of our files and directories.

$ sudo wp core download --path=/var/www/domain.com/html --allow-root
Success: WordPress downloaded.
$ sudo wp config create --path=/var/www/domain.com/html --dbname=wordpress --dbuser=<username> --dbpass=<password> --allow-root
Success: Generated 'wp-config.php' file.
$ sudo wp core install --path=/var/www/domain.com/html --url=https://domain.com --title=WordPress --admin_user=<username> --admin_password=<password> --admin_email=webmaster@domain.com --allow-root
Success: WordPress installed successfully.

Secure WordPress Files and Directories

We are almost ready, but before that, let us secure our WordPress installation. Also, if you need to secure your WordPress further, I would recommend using either Sucuri Security or WordFence plugins.

$ sudo chown -R www-data:www-data /var/www/domain.com/html
$ sudo chmod 640 /var/www/domain.com/html/wp-config.php
$ sudo find /var/www/domain.com/html -type d -exec chmod 755 {} ;
$ sudo find /var/www/domain.com/html -type f -exec chmod 644 {} ;

If you plan to do web development and need write access for themes and plugins, you can temporarily allow it.

Remember when we created our superuser initially, we added it to the www-data group. We will use the same user to log in once we set up our FTP (for better security, you should consider creating a new user with FTP-only privileges, lock it to the document root directory and disable his SSH access, I do show you how to do that in the Addons section).

$ sudo find /var/www/domain.com/html/wp-content/themes/ /var/www/domain.com/html/wp-content/plugins/ -type d -exec chmod 775 {} ;
$ sudo find /var/www/domain.com/html/wp-content/themes/ /var/www/domain.com/html/wp-content/plugins/ -type f -exec chmod 664 {} ;

At this point, your WordPress is installed, secured and you can go ahead and open your domain to preview the fresh install. Then go WP Admin, log in with the specified user and password, and start building your site.

Configure and Create a Staging Site

You need to have this if you do any web development work on your site, and it is never a good idea to work on your production site.

You need to navigate your Digital Ocean web interface domain area and add an A record with hostname staging.

A    staging.domain.com    directs to 111.111.111.111    3600

Next, before we do anything else, we need to make a backup for our production website, including the database. Running the commands below will create these backups and store them in the /var/backups directory.

If you want regular backups of your site, you can either pay Digital Ocean to do this for you as an extra service or create a cron job using the commands below.

$ now=`date +"%m-%d-%y"`
$ sudo tar cvzf /var/backups/domain.com-${now}.tar.gz /var/www/domain.com
$ sudo mysqldump -u root -p database > database-${now}.sql | sudo mv database-${now}.sql /var/backups
Enter password:
$ ls -al /var/backups
-rw-r--r--  1 root root  1036864 Feb 15 16:30 database-02-15-21.sql
-rw-r--r--  1 root root 31644895 Feb 15 15:06 domain.com-02-15-21.tar.gz

The next step is to create the directory structure for our staging, copy all the files from production to staging, create a staging database, update user privileges, update the DB_NAME in the wp-config.php file replace the main domain with a subdomain in our staging database.

$ sudo mkdir -p /var/www/staging.domain.com
$ sudo cp -rf /var/www/domain.com/html /var/www/staging.domain.com
$ sudo mysql -u root -p -e "create database database_staging"
Enter password:
$ sudo mysqldump database | sudo mysql -u root -p database_staging
Enter password:
$ sudo mysql -u root -p -e "GRANT ALL PRIVILEGES ON database_staging.* TO 'username'@'localhost'; FLUSH PRIVILEGES;"
Enter password:
$ sudo nano /var/www/staging.domain.com/html/wp-config.php
define( 'DB_NAME', 'database_staging' );
$ sudo nano /var/www/staging.domain.com/html/wp-cli.local.yml
path: .
url: http://staging.domain.com
$ cd /var/www/staging.domain.com/html
$ wp search-replace 'https://domain.com/' 'http://staging.domain.com/'
Success: Made 188 replacements.

And the final step is to create a config file for our subdomain, enable it, and restart Apache. Same as we did go our primary domain but without SSL support.

$ sudo nano /etc/apache2/sites-available/staging.domain.com.conf
<VirtualHost *:80>
  ServerName staging.domain.com
  ServerAlias www.staging.domain.com
  ServerAdmin webmaster@staging.domain.com
  DocumentRoot "/var/www/staging.domain.com/html"
  DirectoryIndex index.php index.html
  <Directory "/var/www/staging.domain.com/html">
    Options Indexes FollowSymLinks MultiViews
    AllowOverride All
    Order deny,allow
    Allow from all
  </Directory>
  ErrorLog "/var/www/staging.domain.com/html/.logs/error.log"
  CustomLog "/var/www/staging.domain.com/html/.logs/access.log" common
</VirtualHost>
$ sudo a2ensite staging.domain.com.conf
$ sudo systemctl restart apache2

At this point, you can go ahead open http://staging.domain.com and start your development process.

Note that if your production domain is set up with www., you need to change the search-replace URL.

Push Staging to Production

Once you are done with your development, troubleshooting, or testing on staging, you may wonder how to push it to production.

First things first, create a backup of your production site, including the database as shown earlier.

Then you can revert the above process, but an easier solution is to use a plugin. I highly recommend All-in-One WP Migration, but you can, of course, use another one.

With the above plugin, you create a single file of your staging with the .wpress extension. Then go to your production site and import the staging with all the changes and updates. Beware will wipe out everything from your production site (this is why backups are important).

Secondly, you need to change the URLs from http://staging.domain.com to https://domain.com. Again, you may use a plugin, and I recommend the Velvet Blues Update URLS one. At this point, your production site will be all good.

Other things to consider while updating production and changing URLs are installing a maintenance mode plugin, e.g., WP Maintenance Mode, so visitors won’t see broken sites while migrating.

Automate the Production <> Staging Process

[Coming Soon]

Setup Visual Studio Code for Remote Development with SSH

Go to Visual Studio Code marketplace, search, and install Remote-SSH.

Next, use the shortcut Ctrl+Shift+P, search for Remote-SSH: Open Configuration File… and select C:UsersUser.sshconfig if you are on Windows (possible SSH paths for macOS, ~/. ssh‘, and Linux, /root/.ssh).

Host MyRemoteServer
  HostName 111.111.111.111
  User root
  IdentityFile C:UsersUser.sshid_rsaexample

Finally, use the shortcut Ctrl+Shift+P but search for Remote-SSH: Connect to Host… This will open a new VS Code window, ask you for the Host you created above, select MyRemoteServer for our example, and select Linux as your remote host OS.

If you are connecting for the first time, you need to select Continue to save the connection fingerprint locally and then enter the SSH passphrase you had typed when you created your public and private SSH keys (Go to the Authentication with SSH Keys in this tutorial to learn how to create and/or add SSH to your Digital Ocean cloud server).

If everything went well and you are connected, you will see the following when you use the Ctrl+` shortcut to open your VS Code terminal. root@hostname:~#

Now, you know how to connect your Visual Studio Code to SSH.

You can now navigate to the Explorer tab in VS Code and click Open Folder. This will allow you to open a directory directly from your Digital Ocean cloud server.

At this point, you have everything you need to control within your Visual Studio Code application; you don’t need to have separate windows for Terminal, FTP, and/or Code Editor.

Install Git with GitHub CLI and Use Version Control for Your Development

In the section above, we set up an SSH connection and open our Web Server document root.

This section will install and configure Git with GitHub CLI and use version control for our development process. (Since everything is in Visual Studio Code, we can utilize its git control functions)

Install and Configure Git

You may already have Git installed, but if you don’t, you can run or check with the commands below. Also, add a name and an email as global variables. (If you use GitHub, add the same registration email)

$ sudo apt install git
$ sudo git --version
git version 2.25.1
$ git config --global user.name "Your Name" 
$ git config --global user.email "name@domain.com"

Next, navigate to the directory where you need version control and run the following command.

$ cd /var/www/domain.com/html/wp-content/themes/twentytwentyone-child/
$ sudo git init
Initialized empty Git repository in...

You will see that all the files and folders will get highlighted green and have version control from now on.

GitHub CLI

In addition, we are going to install GitHub CLI so we can store our repository on their hosting platform (You can do that as a public or private repo, I assume that you already have a GitHub account created).

$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key C99B11DEB97541F0
$ sudo apt-add-repository https://cli.github.com/packages
$ sudo apt install gh

Below, I will show you how to authenticate, create a repository and push our first commit.

Before we do anything else, you need to generate a Personal Access Token for authentication. To do this, log in to your GitHub account go to Settings > Developer Settings > Personal Access Tokens. Generate New Token select the scope of your access (for our example, I selected the minimum required repo: all, admin.org > read: org).

Finally, save the token somewhere save (You can always generate a new one if you lose it or delete the file).

Now we are ready to authenticate, create and make our first push. For the sake of demonstration, let us use the example from the previous section where we initialized a version control directory.

$ cd /var/www/domain.com/html/wp-content/themes/twentytwentyone-child/
$ sudo gh auth login
? What account do you want to log into? GitHub.com
? What is your preferred protocol for Git operations? HTTPS
? Authenticate Git with your GitHub credentials? Yes
? How would you like to authenticate GitHub CLI? Paste an authentication token
Tip: you can generate a Personal Access Token here https://github.com/settings/tokens
The minimum required scopes are 'repo', 'read:org', 'workflow'.
? Paste your authentication token: ****************************************
- gh config set -h github.com git_protocol https
✓ Configured git protocol
✓ Logged in as username
$ sudo gh repo create wwentytwentyone-child --description "Twenty Twenty One Child" --private
? This will add remote origin to your current directory. Continue?  Yes
✓ Created repository username/twenty-twenty-one-child on GitHub
✓ Added remote https://github.com/username/twenty-twenty-one-child.git
$ sudo git add .
$ sudo git commit -m 'Initial commit'
$ sudo git push origin master

Now you can go ahead to your GitHub repositories and see a new private repository with the files we committed from your theme.

Again, as I showed you in the last two sections, you can do everything within Visual Code Studio by setting up your development environment.

7. Addons

In addition to everything above, you may wish to install and configure some additional software to maintain, secure, and improve your cloud server.

Below I will show you how to configure mail transfer agent, replace Apache with Nginx server, configure secure FTP server, authenticate with public & private SSH keys, configure PHPMyAdmin for easier MySQL maintenance, and setup RoundCube mailing client with which you can check your email via the browser without the need to login to your server.

Postfix with s-nail and Gmail Relay

You need to send, receive, read, or forward emails at some point. If you want plugins like Contact Form 7, you need to have a mail transfer agent configured and a basic mailing client to check your email messages.

First, we need to open port 25 and allow it in our Linux Firewall.

$ sudo ufw allow '25/tcp'
$ sudo ufw status
Status: active
To                         Action      From
--                         ------      ----
25/tcp                     ALLOW       Anywhere
25/tcp                     ALLOW       Anywhere (v6)

Configure Postfix

Next, let us configure Postfix and add a couple of virtual email addresses. You will automatically have an email associated with your user name, e.g., username > username@domain.com.

$ sudo dpkg-reconfigure postfix
Postfix configuration: OK
General type of mail configuration?: Internet Site
System mail name: domain.com
Root and postmaster mail recipient: username
Other destinations to accept mail for: domain.com, mail.domain.com, localhost.domain.com, localhost
Force synchronous updates on mail queue?: No
Local networks: 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
Mailbox size limit: 0
Local address extension character: +
Internet protocols to use: all
$ sudo cat /etc/postfix/main.cf
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
myhostname = domain.com
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
mydestination = domain.com, mail.domain.com, localhost.domain.com, localhost
relayhost =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = all
home_mailbox = Maildir/
virtual_alias_maps = hash:/etc/postfix/virtual
$ sudo postconf -e 'home_mailbox= Maildir/'
$ sudo postconf -e 'virtual_alias_maps= hash:/etc/postfix/virtual'
$ sudo nano /etc/postfix/virtual
webmaster@domain.com username
hello@domain.com username
unique@domain.com username@gmail.com
$ sudo postmap /etc/postfix/virtual
$ sudo systemctl restart postfix

You can relay emails to email services like Gmail (Not the correct approach, but it should work; your emails will probably end up in the SPAM folder and maybe blocked sometimes by Google)

I noticed with forwarding that if you use username@domain.com to username@gmail.com, you get some issues. The forward domain user shouldn’t be an existing user within your system. Always check your logs first, e.g. sudo tail /var/log/mail.log If you have any issues.

Properly Configure Postfix to use Gmail as a Mail Relay

To properly relay emails to Gmail, you need to Allow Less Secure Apps or create App Password if you are using 2-Step Verification for your Gmail account login (The second method is recommended for better security. If haven’t converted to 2-Step Verification, you should do it)

Once you follow the Google steps by step guides above, you need to reconfigure Postfix as shown below. (Note, passwords below should be either your real password or the automatically generated one if you use the 2-Step Verification with App Password)

I am showing you an example where you send emails to different Gmail accounts, but sending to a single one can be done the same way. Just remove the sender_relay parts from the example.

$ sudo apt install mailutils
$ sudo nano /etc/postfix/sender_relay
user1@gmail.com [smtp.gmail.com]:587
user2@gmail.com [smtp.gmail.com]:587
$ sudo nano /etc/postfix/sasl_passwd
#Pre-sender Relay Authentication
user2@gmail.com          user2@gmail.com:password

#Default Relay Host
[smtp.gmail.com]:587    user1@gmail.com:password
$ sudo chmod 0600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
$ sudo nano /etc/postfix/main.cf
#Configure Postfix to use Gmail as a Mail Relay

relayhost = [smtp.gmail.com]:587
smtp_use_tls = yes
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = encrypt
smtp_sasl_mechanism_filter = plain
header_size_limit = 4096000
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd

smtp_sender_dependent_authentication = yes
sender_dependent_relayhost_maps = hash:/etc/postfix/sender_relay
$ sudo postmap /etc/postfix/sender_relay
$ sudo postmap /etc/postfix/sasl_passwd
$ sudo systemctl restart postfix
$ echo 'Hello World!' | s-nail -s 'Hello World!' -Snorecord user1@domain.com
$ echo 'Hello World!' | s-nail -s 'Hello World!' -Snorecord user2@domain.com

Now Gmail won’t block your email messages, and you will receive them directly in your INBOX.

Hint, if you have multiple Gmail accounts and have several domains under the same Digital Ocean cloud served as virtual hosts, you can add all the authorizations in the /etc/postfix/sasl_passwd and /etc/postfix/sender_relay files as shown above. (Remember to reset the postmap and restart postfix after each change to the file)

Check Email Messages with s-nail

Next, we will configure the s-nail mailing client we previously installed. Other mail client options considered could be SquirrelMail or RoundCube (these usually require more extensive configuration, and I show you how to set up RoundCube in the Addons section below)

$ echo 'export MAIL=~/Maildir' | sudo tee -a /etc/bash.bashrc | sudo tee -a /etc/profile.d/mail.sh
$ source /etc/profile.d/mail.sh
$ sudo nano /etc/s-nail.rc
#Add the following configuration lines at end of the file

set emptystart
set folder=Maildir
set record=+sent
$ echo 'Hello World!' | s-nail -s 'Hello World!' -Snorecord username
$ echo 'Hello World!' | s-nail -s 'Hello World!' -Snorecord unique@domain.com
$ ls -R ~/Maildir
/home/username/Maildir:
cur  new  tmp
/home/username/Maildir/cur:
/home/username/Maildir/new:
1613008884.Vfc01I81112M312975.domain  
/home/username/Maildir/tmp:
$ s-nail
/home/username/Maildir: 1 message 1 new
▸N  1 First Last      2021-02-13 02:01   14/411   init
? q

In s-nail, enter a number to preview a message, d to delete, h to list all, and q to quit.

If you have any issues, head up and preview the mail log file sudo tail /var/log/mail.log and see if there is any clue why things don’t work as they should.

Testing the Postfix with s-nail

If you remember, in the section above, we set webmaster@domain.com as our primary WordPress email. Let us change it to hello@domain.com.

To do this, login into your WP Admin, go to Settings > General, switch the emails, and Update. Now, if you type s-nail in your console, you will see the following output:

$ s-nail
/home/username/Maildir: 2 messages 1 new
 O  1 First Last         2021-02-11 02:01   14/411   init
▸N  2 WordPress          2021-02-11 02:03   31/1015  New Admin Email Address
? q

Open the email, copy-paste the confirm email URL into your browser, and voila, your WP Admin email address is switched and confirmed.

Moreover, you can now install the Contact Form 7 plugin and test if you receive contact messages locally or get forwarded to your Gmail account successfully.

Authentication with SSH Keys

Running your Web Server gives you a lot of freedom, but at the same time, you need to be very careful when it comes to security.

When we set up our cloud server at the beginning of this tutorial, we used the root password authentication method and even disabled root access to improve the security. There is a better and more secure way to do that.

Below I will show you how to generate public and private SSH keys and use them to log in to your cloud server.

You have the option to download PuTTYgen for Windows or use its ported versions on Mac or Linux, but for this example, we are going to use OpenSSH, which is a universal way to generate public and private SSH keys.

Firstly, You need to check if you have it installed on your operating system. Just open the terminal or command prompt in your OS and type ssh -V. You will see similar output if you have it installed.

#Window
OpenSHH_for_Windows_7.7p1, LibreSSL 2.6.5
#Linux
OpenSSH_8.2p1 Ubuntu-4ubuntu0.1, OpenSSL 1.1.1f 31 Mar 2020
#MacOS
OpenSSH_8.1p1, LibreSSL 2.7.3

Next, we are going to generate the keys on your local machine. The example below is done on Windows, but it should be similar to macOS or Linux.

C:UsersUser> ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (C:UsersUsername/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in example.
Your public key has been saved in example.pub.
The key fingerprint is:
C:UsersUser> cd C:UsersUser/.ssh/id_rsa
C:UsersUser/.ssh/id_rsa> dir
02/15/2021  11:20 AM    1,766 example
02/15/2021  11:20 AM      405 example.pub

If you don’t have the id_rsa directory created, you may need to create it manually.

When you create a new Droplet, you need to select SSH keys as your Authentication method. Then click on the New SSH Key button, copy/paste the example.pub content in the text area and click on Add SHH Key button.

You can proceed with your setup as normal, but when you go and login with PuTTY, this time you should go to SSH > Auth tab and under Private key file for authentication: browser and select the other file in the id_rsa directory, in the sample above named, without the .pub extension. Then follow the same steps from the Login to your Cloud Server section at the beginning of the tutorial.

Lastly, once prompted, you should log in with root as username and enter the specified SSH key passphrase you have selected earlier.

If you have a cloud server already created with a password and want to switch to the SSH keys authentication method, just do the following procedure.

$ sudo nano /root/.ssh/authorized_keys
[add the example.pub file content]
$ sudo nano /etc/ssh/sshd_config
PermitRootLogin without-password
$ sudo systemctl restart sshd

Replace Apache with Nginx

At some point, you may wish to switch from Apache to the Nginx server. Here is a very well-written article about the pros and cons of Apache vs. Nginx.

There are several steps we need to take to accomplish the switch. We will not delete Apache because, at some point, you might decide to revert.

Firstly, let us install Nginx with all the additional packages and reconfigure our Linux firewall.

$ sudo apt install nginx python3-certbot-nginx php-fpm
$ sudo ufw app list
Available applications:
  Apache
  Apache Full
  Apache Secure
  Nginx Full
  Nginx HTTP
  Nginx HTTPS
  OpenSSH
  Postfix
  Postfix SMTPS
  Postfix Submission
$ sudo ufw status
Status: active
To                         Action      From
--                         ------      ----
Apache Full                ALLOW       Anywhere
Apache Full (v6)           ALLOW       Anywhere (v6)
$ sudo ufw delete allow 'Apache Full'
$ sudo ufw allow 'Nginx Full'
$ sudo ufw status
Status: active
To                         Action      From
--                         ------      ----
Nginx Full                 ALLOW       Anywhere
Nginx Full (v6)            ALLOW       Anywhere (v6)
$ sudo systemctl stop apache2
$ sudo systemctl disable apache2
$ sudo systemctl enable nginx
$ sudo systemctl start nginx
$ sudo systemctl status | grep apache2
[no results found]
$ sudo systemctl status | grep nginx
  nginx.service
  │ ├─54972 /usr/sbin/nginx -k start

At this point, if you open http://domain.com, you will get the default Nginx index page.

Configure Nginx and Enable SSL Support

The next step is to configure our Nginx server similar to Apache and enable SSL support.

$ sudo nano /etc/nginx/sites-available/domain.com
server {
  listen 80;
  listen [::]:80;
  root /var/www/domain.com/html;
  index index.php index.html index.htm index.nginx-debian.html;
  server_name domain.com www.domain.com;
  location / {
    try_files $uri $uri/ /index.php?$args;
  }
  location ~ .php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php7.4-fpm.sock;
  }
  location ~ /.ht {
    deny all;
  }
}
$ sudo ln -s /etc/nginx/sites-available/domian.com /etc/nginx/sites-enabled/
$ sudo unlink /etc/nginx/sites-enabled/default
$ sudo nano /etc/nginx/nginx.conf
#Uncomment the line below.

server_names_hash_bucket_size 64;
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

When writing this tutorial, I am using PHP 7.4, but if you are using a newer version, update the following line: fastcgi_pass unix:/run/php/php7.4-fpm.sock; with your version.

Now, let us reconfigure and regenerate our SSL certificate. It is the same process as Apache, but this time for Nginx.

$ sudo certbot --nginx -d domain.com -d www.domain.com
What would you like to do?
1: Attempt to reinstall this existing certificate
2: Renew & replace the cert (limit ~5 per 7 days)
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 1
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for new sites, or if you're confident your site works on HTTPS. You can undo this change by editing your web server's configuration.
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2
$ sudo certbot renew --dry-run
$ sudo cat /etc/nginx/sites-enabled/domain.com
server {
  listen 80;
  listen [::]:80;
  root /var/www/domain.com/html;
  index index.php index.html index.htm index.nginx-debian.html;
  server_name domain.com www.domain.com;
  location / {
    try_files $uri $uri/ /index.php?$args;
  }
  location ~ .php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php7.4-fpm.sock;
  }
  location ~ /.ht {
    deny all;
  }
  listen [::]:443 ssl ipv6only=on; # managed by Certbot
  listen 443 ssl; # managed by Certbot
  ssl_certificate /etc/letsencrypt/live/domain.com/fullchain.pem; # managed by Certbot
  ssl_certificate_key /etc/letsencrypt/live/domain.com/privkey.pem; # managed by Certbot
  include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

Be sure to check the configuration file. It should look like the output above after the configuration of the SSL.

Testing the Nginx

Finally, let us test and see if we run an Nginx server. You can check this using the DevTools of your browser as well.

$ sudo curl -sSL -D - domain.com -o /dev/null
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 11 Feb 2021 03:28:17 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Link: <https://domain.co/wp-json/>; rel="https://api.w.org/"

Secure FTP with vsftpd

Having a way to securely access your server via FTP is a must if you want to do WordPress web development and store and upload files to your cloud server. First, let us install vsftpd create the file structure, and allow the ports we need in our Linux firewall.

$ sudo apt install vsftpd
$ sudo ufw allow 20/tcp
$ sudo ufw allow 21/tcp
$ sudo ufw allow 990/tcp
$ sudo ufw allow 40000:50000/tcp
$ sudo ufw status
Status: active
To                         Action      From
--                         ------      ----
20/tcp                     ALLOW       Anywhere
21/tcp                     ALLOW       Anywhere
990/tcp                    ALLOW       Anywhere
40000:50000/tcp            ALLOW       Anywhere
20/tcp (v6)                ALLOW       Anywhere (v6)
21/tcp (v6)                ALLOW       Anywhere (v6)
990/tcp (v6)               ALLOW       Anywhere (v6)
40000:50000/tcp (v6)       ALLOW       Anywhere (v6)
$ sudo mkdir /home/username/ftp
$ sudo chown nobody:nogroup /home/username/ftp
$ sudo chmod a-w /home/username/ftp
$ sudo ls -la /home/username/ftp
total 8
dr-xr-xr-x 2 nobody nogroup 4096 Feb 11 13:16 .
drwxr-xr-x 7 username username  4096 Feb 11 13:16 ..
$ sudo mkdir /home/username/ftp/files
$ sudo chown $USER:$USER /home/username/ftp/files
$ sudo ls -la /home/username/ftp
$ echo "vsftpd test file" | sudo tee /home/username/ftp/files/test.txt
$ sudo nano /etc/vsftpd.conf
#Add the following configuration lines at end of the file

anonymous_enable=NO
local_enable=YES
write_enable=YES
chroot_local_user=YES
user_sub_token=$USER
local_root=/home/$USER/ftp
pasv_min_port=40000
pasv_max_port=50000
userlist_enable=YES
userlist_file=/etc/vsftpd.userlist
userlist_deny=NO
$ sudo nano /etc/ssh/sshd_config
# Change the line below if you haven't done it earlier

PasswordAuthentication yes 
$ echo "username" | sudo tee -a /etc/vsftpd.userlist
$ sudo cat /etc/vsftpd.userlist
username
$ sudo systemctl restart sshd
$ sudo systemctl restart vsftpd

Testing the FTP Server

Now we are ready to test our FTP server. You can see that anonymous login is denied, and you can either log in with your IP Address or domain name.

$ sudo ftp -p 111.111.111.111
$ sudo ftp -p domain.com
Connected to domain.com.
220 (vsFTPd 3.0.3)
Name (domain.com:root): username
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> quit
$ sudo ftp -p username@domain.com
Connected to domain.com.
220 (vsFTPd 3.0.3)
Name (domain.com:root): anonymous
530 Permission denied.
Login failed.
ftp> quit

Securing the FTP Server

In addition, it would be wise to secure our FTP server and add SSL support.

$ sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/vsftpd.pem -out /etc/ssl/private/vsftpd.pem
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:
$ sudo nano /etc/vsftpd.conf
#Add the following configuration lines at end of the file
#After what we added in the section above

rsa_cert_file=/etc/ssl/private/vsftpd.pem
rsa_private_key_file=/etc/ssl/private/vsftpd.pem
ssl_enable=YES
allow_anon_ssl=NO
force_local_data_ssl=YES
force_local_logins_ssl=YES
ssl_tlsv1=YES
ssl_sslv2=NO
ssl_sslv3=NO
require_ssl_reuse=NO
ssl_ciphers=HIGH
$ sudo systemctl restart vsftpd
$ sudo ftp -p domain.com
Connected to domain.com.
220 (vsFTPd 3.0.3)
Name (domain.com:root): username
530 Non-anonymous sessions must use encryption.
Login failed.
421 Service not available, remote server has closed connection
ftp> quit
$ sudo sftp -p username@domain.com
The authenticity of host 'domain.com (111.111.111.111)' can't be established.
ECDSA key fingerprint is SHA256:tk6x32...
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'domain.com,111.111.111.111' (ECDSA) to the list of known hosts.
username@domain.com's password:
Connected to domain.com.
sftp> quit

In the above example, after setting up SSL, you can see that a normal FTP connection isn’t available anymore, and we need to use sFTP.

You can now go ahead and test the same with your favorite FTP client (e.g., FileZilla, WinSCP, or CyberDuck) or install a Visual Studio Code sFTP extension and do the same.

Add FTP-only Users

I mentioned this earlier, but to read, write, and delete files and directories from your Web Server, you need to have the correct permissions and ownership. Go back to the WordPress section of this tutorial to see how I configured it.

Below we will create an FTP-only user, remove his SSH access and lock it in the /var/www/domain.com/html directory.

$ sudo adduser ftponly
New password:
Retype new password:
passwd: password updated successfully
Changing the user information for username
Enter the new value, or press ENTER for the default
  Full Name []: 
  Room Number []:
  Work Phone []:
  Home Phone []:
  Other []:
Is the information correct? [Y/n] Y
$ sudo usermod -aG www-data ftponly
$ sudo usermod --home /var/www/domain.com/html ftponly
$ sudo usermod -s /sbin/nologin ftponly
$ sudo nano /etc/ssh/sshd_config
#Add the following configuration lines at end of the file
#And comment the old line below

#Subsystem      sftp    /usr/lib/openssh/sftp-server
Subsystem       sftp    internal-sftp
Match group ftponly
  ChrootDirectory /var/www/domain.com
  X11Forwarding no
  AllowTcpForwarding no
  ForceCommand internal-sftp
$ sudo systemctl restart sshd
$ sudo cat /etc/group | grep ftponly
ftponly:x:1001:
$ sudo cat /etc/passwd | grep ftponly
ftponly:x:1001:1001:Ftponly,,,:/var/www/domain.com/html:/sbin/nologin
$ sudo id ftponly
uid=1001(ftponly) gid=1001(ftponly) groups=1001(ftponly),33(www-data),121(ftp)
$ su ftponly
Password:
su: Authentication failure
$ sudo sftp -p ftponly@domain.com
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
ftponly@domain.com password:
Connected to domain.com.
sftp> ls
drwxr-xr-x    3 root     root         4096 Feb 13 08:19 .
drwxr-xr-x    3 root     root         4096 Feb 13 08:19 ..
drwxr-xr-x    6 33       33           4096 Feb 14 08:57 html
sftp> cd /
sftp> ls
drwxr-xr-x    3 root     root         4096 Feb 13 08:19 .
drwxr-xr-x    3 root     root         4096 Feb 13 08:19 ..
drwxr-xr-x    6 33       33           4096 Feb 14 08:57 html

In the end, I run a couple of commands to show you how to preview your user mods.

Note that the –home directory must always be one level deeper than the ChrootDirectory.

Also, the Match group ftponly rule in the sshd_config would apply only for the new ftponly user we created. If you wish to have multiple users, you should add them to the ftp group with usermod and change the match line to Match group ftp.

PHPMyAdmin for Easier MySQL Administration

PHPMyAdmin application is an easier and faster way to interact with your MySQL server via a web interface. In the steps below, we will configure it and make it secure.

Install PHPMyAdmin

$ sudo apt install phpmyadmin php-mbstring php-zip php-gd php-json php-curl
$ sudo phpenmod mbstring
$ sudo systemctl restart apache2
  • When you get the server selection question, choose apache2, then select OK
  • Configure database for phpmyadmin with dbconfig-common? Yes
  • Enter the PHPMyAdmin database password twice.

If you get a message that the password doesn’t satisfy the specified rules (If you had selected 1 or 2 when we secured MySQL), select ignore.

Then uninstall the MySQL component responsible for password validation, rerun PHPMyAdmin installation, and reinstall the MySQL password component to bring back the security level to normal.

$ sudo mysql -u root -p
Enter password:
mysql> UNINSTALL COMPONENT "file://component_validate_password";
mysql> QUIT;
$ sudo dpkg-reconfigure phpmyadmin
$ sudo mysql -u root -p
Enter password:
mysql> INSTALL COMPONENT "file://component_validate_password";
mysql> QUIT;

Remember when we installed our MySQL Server above, we selected medium-level passwords. Your password must adhere to these rules.

If you log in to MySQL with your root, you will see the newly created table for PHPMyAamin.

$ sudo mysql -u root -p
Enter password:
mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| database           |
| mysql              |
| performance_schema |
| phpmyadmin         |
| sys                |
+--------------------+
6 rows in set (0.00 sec)

Now you can go ahead and open https://domain.com/phpmyadmin and login to your PHPMyAdmin with the MySQL username and password you have specified earlier (not root). You can access the PHPMyAdmin URL with any virtual host domain name.

Secure PHPMyAdmin

In addition, you can use the following steps to make PHPMyAdmin access even more secure.

$ sudo nano /etc/apache2/conf-available/phpmyadmin.conf
<Directory /usr/share/phpmyadmin>;
  Options SymLinksIfOwnerMatch
  DirectoryIndex index.php
  AllowOverride All ### Add this line after the lines above
$ sudo nano /usr/share/phpmyadmin/.htaccess
AuthType Basic
AuthName "Restricted Files"
AuthUserFile /etc/phpmyadmin/.htpasswd
Require valid-user
$ sudo htpasswd -c /etc/phpmyadmin/.htpasswd username
New password:
Re-type new password:
Adding password for user username
$ sudo systemctl restart apache2

If you go back and refresh the PHPMyAdmin URL, you will be prompted to enter another username and password to access the actual URL address. This adds another layer of security to your MySQL Server access.

Dovecot IMAP-server with RoundCube

RoundCube is an excellent option for a customizable IMAP-based webmail client, and it has a large set of features for viewing, organizing, and composing emails. Alternative options you may consider are SquirrelMail or MailHog.

Before installing and configuring RoundCube, we need to have an IMAP-based server. We can always use the Gmail IMAP-server, but we will install our own with Dovecot for this tutorial. If you wish to go the Gmail route, look at this tutorial from Digital Ocean.

First, let us install and configure the Dovecot IMAP server. For this, you should already have a running and configured Web Server (the example below is with Apache, but it can easily be used with Nginx with a couple of modifications) and Postfix.

Enable Submission Service and Reconfigure Postfix

$ sudo nano /etc/postfix/master.cf
#Add the following configuration lines at end of the file

submission     inet     n    -    y    -    -    smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_tls_wrappermode=no
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth
smtps     inet  n       -       y       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth
$ sudo nano /etc/postfix/main.cf
#Add the following configuration lines at end of the file
#Comment the old TLS parameter rules

#TLS parameters
#smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
#smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
#smtpd_tls_security_level=may

smtpd_tls_cert_file=/etc/letsencrypt/live/mail.domain.com/fullchain.pem
smtpd_tls_key_file=/etc/letsencrypt/live/mail.domain.com/privkey.pem
smtpd_tls_security_level=may 
smtpd_tls_loglevel = 1
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_security_level = may
smtp_tls_loglevel = 1
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
mailbox_transport = lmtp:unix:private/dovecot-lmtp
smtputf8_enable = no
$ sudo systemctl restart postfix
$ sudo ss -lnpt | grep master

Create a Subdomain for our Mail Server

You need to navigate your Digital Ocean web interface domain area and add an A record with hostname mail.

A    mail.domain.com    directs to 111.111.111.111    3600

Additionally, we should open some ports into our Linux Firewall, set up the mail.domain.com, create the directory structure with the correct permissions ownership, and add SSL support for our subdomain.

Reconfigure the Linux Firewall

$ sudo ufw allow 80
$ sudo ufw allow 443
$ sudo ufw allow 587
$ sudo ufw allow 465
$ sudo ufw allow 143
$ sudo ufw allow 993/tcp
$ sudo ufw allow 110
$ sudo ufw allow 995/tcp
$ sudo ufw status
Status: active
To                         Action      From
--                         ------      ----
80                         ALLOW       Anywhere
443                        ALLOW       Anywhere
587                        ALLOW       Anywhere
465                        ALLOW       Anywhere
143                        ALLOW       Anywhere
993/tcp                    ALLOW       Anywhere
110                        ALLOW       Anywhere
995/tcp                    ALLOW       Anywhere
80 (v6)                    ALLOW       Anywhere (v6)
443 (v6)                   ALLOW       Anywhere (v6)
587 (v6)                   ALLOW       Anywhere (v6)
465 (v6)                   ALLOW       Anywhere (v6)
143 (v6)                   ALLOW       Anywhere (v6)
993/tcp (v6)               ALLOW       Anywhere (v6)
110                        ALLOW       Anywhere (v6)
995/tcp                    ALLOW       Anywhere (v6)
$ sudo nano /etc/apache2/sites-available/mail.domain.com.conf
<VirtualHost *:80>        
  ServerName mail.domain.com
  DocumentRoot /var/www/mail.domain.com/html
</VirtualHost>
$ sudo mkdir -p /var/www/mail.domain.com/html
$ sudo chown -R www-data:www-data /var/www/mail.domain.com/html
$ sudo a2ensite mail.domain.com.conf
$ sudo systemctl restart apache2
$ sudo certbot --apache -d mail.domain.com
What would you like to do?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: Attempt to reinstall this existing certificate
2: Renew & replace the cert (limit ~5 per 7 days)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 1
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2

Install and Configure Dovecot

Now we are ready to install and configure Dovecot.

$ sudo apt install dovecot-core dovecot-imapd dovecot-lmtpd dovecot-pop3d
$ sudo nano /etc/dovecot/dovecot.conf
#Add this line before the next one

protocols = imap lmtp pop3 
!include_try /usr/share/dovecot/protocols.d/*.protocol
$ sudo postconf mail_spool_directory
mail_spool_directory = /var/mail
$ sudo nano /etc/dovecot/conf.d/10-mail.conf
#Comment the first line and the next two after it

#mail_location = mbox:~/mail:INBOX=/var/mail/%u
mail_location = maildir:~/Maildir
mail_privileged_group = mail
$ sudo nano /etc/dovecot/conf.d/10-auth.conf
#Add the following configuration lines at end of the file

disable_plaintext_auth = yes
auth_username_format = %n
auth_mechanisms = plain
auth_mechanisms = plain login
$ sudo nano /etc/dovecot/conf.d/10-ssl.conf
#Add the following configuration lines at end of the file

ssl = required
ssl_cert = </etc/letsencrypt/live/mail.domain.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.domain.com/privkey.pem
ssl_prefer_server_ciphers = yes
#ssl_protocols = !SSLv3 !TLSv1 !TLSv1.1 #Dovecot version 2.2.x
ssl_min_protocol = TLSv1.2 #Dovecot version 2.3.x
$ sudo nano /etc/dovecot/conf.d/10-master.conf
service lmtp {
  #Add the following configuration lines at the end inside this block
  
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0600
    user = postfix
    group = postfix
  }
}
service auth {
  #Add the following configuration lines at the end inside this block
  
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
}
$ sudo nano /etc/dovecot/conf.d/15-mailboxes.conf
namespace inbox {
  #Add the following configuration at the end ofinside this block
  
  mailbox Trash {
    auto = create
    special_use = Trash
  }
}
$ sudo adduser dovecot mail
$ sudo systemctl restart postfix dovecot apache2

Install and Configure RoundCube

Finally, we are ready to install and configure our RoundCube Webmail client. You can download the latest stable complete version from their website. Be sure to have the correct URL for our wget command.

$ sudo apt-get install php-imagick php-xml php-mbstring php-intl php-zip php-pear zip unzip git composer
$ sudo systemctl restart apache2
$ sudo nano /etc/php/7.4/apache2/php.ini
;Add the following configuration lines at end of the file

date.timezone = "America/New_York"
upload_max_filesize = 12M
post_max_size = 18M
mbstring.func_overload = 0
$ wget https://github.com/roundcube/roundcubemail/releases/download/1.4.11/roundcubemail-1.4.11-complete.tar.gz
$ tar xvzf roundcubemail-1.4.11-complete.tar.gz   
$ sudo mv roundcubemail-1.4.11/* /var/www/mail.domain.com/html
$ sudo chown -R www-data:www-data /var/www/mail.domain.com/html
$ sudo chmod 775 /var/www/mail.domain.com/html/temp/ /var/www/mail.domain.com/html/logs/
$ sudo mysql -u root -p
Enter password:
mysql> CREATE DATABASE roundcubemail;
mysql> CREATE USER 'roundcube'@'localhost' IDENTIFIED BY 'password';
mysql> GRANT ALL PRIVILEGES ON roundcubemail.* to 'roundcube'@'localhost';
mysql> FLUSH PRIVILEGES;
mysql> QUIT;
$ sudo mysql -u roundcube -p roundcubemail < /var/www/mail.domain.com/html/SQL/mysql.initial.sql
$ sudo mysql -u roundcube -p
Enter password:
mysql> USE roundcubemail;
mysql> SHOW tables;
+-------------------------+
| Tables_in_roundcubemail |
+-------------------------+
| cache                   |
| cache_index             |
| cache_messages          |
| cache_shared            |
| cache_thread            |
| contactgroupmembers     |
| contactgroups           |
| contacts                |
| dictionary              |
| filestore               |
| identities              |
| searches                |
| session                 |
| system                  |
| users                   |
+-------------------------+
15 rows in set (0.01 sec)

Now, you are ready to go and install RoundCube via the web interface. Open https://mail.domain.com/installer/ in your favorite browser.

  • Check Environment – should have OK for all except LDAP and All databases but MySQL.
  • Create Config
    • Enter product_name, support_url like e.g webmaster@domain.com & username_domain like domain.com
    • Enter your database_password, verify if the user and database name are correct
    • IMAP Settings default_host to ssl://mail.domain.com and default_port to 993
    • SMTP Settings smtp_server to ssl://mail.domain.com and smtp_port to 465
    • Check the following plugins to install: archive, emoticons, enigma, filesystem_attachments, hide_blockquote, identity_select, markasjunk, newmail_notifier
    • Click on the Update and then Continue buttons
  • Test Config – should have everything OK
    • Test SMTP config – enter username@domain.com and password for authentication and then Sender and Recipient email addresses. You should get SMTP send: OK
    • Test IMAP config – enter username@domain.com and password and click on the Check Login button. You should get IMAP connect: OK (SORT capability: yes)

After all the testing is successful, you can open https://mail.domain.com/ and log in with your user and send and receive emails. Note that you will probably receive your message in your Gmail SPAM folder. I discussed this earlier in the Postfix section and further fixed it using POP. We already have POP available in our configuration.

You may also like to remove the RoundCube installer directory by running the command below.

$ sudo rm -rf /var/www/mail.domain.com/html/installer

Add and View Mail Users

Adding and listing Dovecot users can be done as easily as a normal Linux user. Your new user is added to the Dovecot group.

After his first login to RoundCube, the email newuser@domain.com will be created automatically.

You should see newuser below after his first login

$ sudo adduser newuser
$ sudo systemctl restart dovecot
$ sudo doveadm user '*'
nobody
systemd-coredump
lxd
do-agent
username
newuser
nobody

Mail Troubleshooting

If you get any hick-ups, here are a couple of methods to troubleshoot your Dovecot and mailing configurations.

$ sudo tail -f /var/log/mail.log
$ sudo journalctl -eu dovecot
systemd[1]: dovecot.service: Succeeded.
systemd[1]: Stopped Dovecot IMAP/POP3 email server.
systemd[1]: Started Dovecot IMAP/POP3 email server.
dovecot[100250]: master: Dovecot v2.3.7.2 (3c910f64b) starting up for imap, lmtp... 
$ sudo systemctl status dovecot
● dovecot.service - Dovecot IMAP/POP3 email server
   Loaded: loaded (/lib/systemd/system/dovecot.service; enabled; vendor preset: enabled)
   Active: active (running) since Mon 2021-02-15 07:10:23 UTC; 41s ago
$ sudo postconf mydestination
mydestination = domain.com, mail.domain.com, localhost.domain.com, localhost

Postfix and Dovecot Automation

Finally, it is nice to add some automation to our setup. The following commands will add a cron job to auto-renew our TSL certificate and automatically restart Dovecot if its process is stopped or killed by mistake.

$ sudo crontab -e
@daily certbot renew --quiet && systemctl reload postfix dovecot apache2
$ sudo systemctl restart dovecot
$ sudo mkdir -p /etc/systemd/system/dovecot.service.d/
$ sudo nano /etc/systemd/system/dovecot.service.d/restart.conf
[Service]
Restart=always
RestartSec=5s
$ sudo systemctl daemon-reload
$ sudo pkill dovecot
$ sudo systemctl status dovecot

8. Conclusion

Hopefully, this tutorial (or parts of it) was useful, and you have completed the steps without any hick-ups. Please share your experience and ask your questions in the comment section below.

Leave a Reply

Your email address will not be published. Required fields are marked *