How to Set Up a Web Server for Multiple Sites

How to Set up a Web Server for Multiple Sites at Just $7/Month

Over the past few years, I’ve focused on reducing my living expenses, including the costs associated with running my websites. After streamlining and cutting down on various services like Amazon AWS, DigitalOcean, and Laravel Forge, I decided to return to a more cost-effective approach. I’ve opted for a DIY VPS server with minimal configuration to further cut costs and simplify management. In this guide, I’ll show you how to set up a web server for just $7/month to host multiple sites efficiently.

Do not spend wastefully. Surely the wasteful are like brothers to the devils. And the Devil is ever ungrateful to his Lord.

Quran 17:26-27

I will walk you through the process of setting up a LEMP stack (Linux, Nginx, MariaDB, PHP) on DigitalOcean to host static websites, WordPress, and Laravel applications. This setup costs me only $7 per month (as of May 15, 2024).

Create Droplet

We decided to go with the bare minimum configuration for a droplet that is sufficient for this demo.

  • Datacenter: NYC3
  • OS: Ubuntu 24.04 LTS
  • Type: Basic
  • MEM / CPU: 1GB / 1 AMD vCPU (Premium)
  • Disk: 25 GB NVMe SSD
  • Price: $7/mo
  • Authentication Method: SSH Key

Initial Server Setup

After creating the droplet, SSH into it:

Run update and upgrade:

apt update && apt upgrade -y

Reboot the system:

reboot

Log back in via SSH:

Create New User

The root user has high privileges but is risky for regular use. We’ll create a standard user account with reduced privileges.

Before we create a new user, let’s change root password first:

passwd

Enter a new password and re-type to confirm.

Then create a new user (e.g. superman):

adduser --gecos "" superman

Choose a strong password for the user. The --gecos "" option skips additional questions about the user during the user creation process.

Grant admin privileges to the new user:

usermod -aG sudo superman

Set permissions for the home directory to ensure that the web roots within this directory are accessible by the web server.

chmod o+x /home/superman

Edit the new user’s .bashrc file:

nano /home/superman/.bashrc

Add the following lines at the top of the file. This will ensure that every directory has a chmod of 755 and every file has 644.

/home/superman/.bashrc
# Set directory permissions to 755 and file permissions to 644
umask 022

To ensure cross-terminal compatibility, tell the terminal to use xterm as the terminal type, which is commonly supported by most terminal emulators. Add this line at the bottom of the .bashrc file:

/home/superman/.bashrc
# Set the terminal type for compatibility with other terminal emulators (i.e. supports the "clear" command)
export TERM=xterm

Create a sudoers file for the new user:

sudo nano /etc/sudoers.d/superman

Add the NOPASSWD configuration so that the user can run rsync command without a password. It’s useful while running remote deploy as a different user, i.e. www-data.

/etc/sudoers.d/superman
superman ALL=(ALL) NOPASSWD: /usr/bin/rsync

Set proper permissions:

sudo chmod 440 /etc/sudoers.d/superman

Enable Access for New User

Add our local computer’s public key to the new user’s ~/.ssh/authorized_keys file:

rsync --archive --chown=superman:superman ~/.ssh /home/superman

Harden OpenSSH Config

Create a backup of the sshd_config file:

cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

Edit the config:

nano /etc/ssh/sshd_config

Update the following settings:

/etc/ssh/sshd_config
LoginGraceTime 20
PermitRootLogin no
MaxAuthTries 3
PasswordAuthentication no
PermitEmptyPasswords no
KbdInteractiveAuthentication no
KerberosAuthentication no
GSSAPIAuthentication no
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
PermitUserEnvironment no
PermitTunnel no
DebianBanner no

Note that the DebianBanner option may not be present in the configuration file; it may need to be added manually.

Next, validate the syntax:

sshd -t

If everything is correct, no message will be displayed.

Setup Firewall

Allow SSH through the firewall:

ufw allow OpenSSH

Enable the firewall and check its status:

ufw --force enable
ufw status

Reboot the system:

reboot

Now, SSH into the server using the new user:

Add Swap Space

Create a 1GB swap file:

sudo fallocate -l 1G /swapfile

Change the permissions of the swap file so that only the root user can read and write to it:

sudo chmod 600 /swapfile

Set up the swap file by marking it as swap space:

sudo mkswap /swapfile

Activate the swap file so the system starts using it:

sudo swapon /swapfile

Check that the swap is now active:

sudo swapon --show

or

free -h

To ensure the swap file is used after a reboot, add it to the /etc/fstab file:

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Set swappiness to a lower value (i.e. 10) for the system to avoid using swap space unless absolutely necessary:

sudo sysctl vm.swappiness=10

Make this change permanent:

echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf

Install LEMP Stack

At this stage, we will install Nginx, MariaDB, and PHP on the server.

Install Nginx

Run:

sudo apt install -y nginx

Allow both HTTP and HTTPS traffic:

sudo ufw allow 'Nginx Full'

Verify the change by checking the status:

sudo ufw status

Keep a backup of the default config and update the original:

sudo mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak
sudo nano /etc/nginx/sites-available/default

Paste the following content:

/etc/nginx/sites-available/default
server {
  listen 80 default_server;
  listen [::]:80 default_server;
  server_name _;
  server_tokens off;
  return 444;
}

Reload Nginx:

sudo systemctl reload nginx

Install MariaDB

We will install MariaDB, as it is the open-source counterpart of MySQL (owned by Oracle):

sudo apt install -y mariadb-server

Secure the MariaDB server:

sudo mysql_secure_installation

Choose the following options:

Enter current password for root: (↵)
Switch to unix_socket authentication [Y/n]: (↵)
Change the root password? [Y/n]: (n↵)
Remove anonymous users? [Y/n]: (↵)
Disallow root login remotely? [Y/n]: (↵)
Remove test database and access to it? [Y/n]: (↵)
Reload privilege tables now? [Y/n]: (↵)

Test if we’re able to log in to the MySQL console:

sudo mysql

This will connect to the MySQL server as the administrative database user root, which is inferred by the use of sudo while running this command.

Exit the MySQL console:

exit

ImageMagick

Install ImageMagick to be used with PHP for image manipulation:

sudo apt install -y imagemagick

Install PHP

Now let’s install PHP and some of the most popular PHP extensions for use with WordPress and Laravel:

sudo apt install -y php-fpm php-cli php-mysql php-curl php-gd php-intl php-mbstring php-soap php-xml php-xmlrpc php-zip php-imagick

SSL Certificate

We’ll use certbot, an easy-to-use tool that automates obtaining, renewing, and configuring Let’s Encrypt SSL certificates for web servers.

To install certbot on our server, run:

sudo apt install -y certbot python3-certbot-nginx

Generate DH (Diffie-Hellman) Group

Diffie–Hellman key exchange (DH) is a method of securely exchanging cryptographic keys over an unsecured communication channel.

Generate a new set of 2048-bit DH parameters:

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

Configure Nginx

Create a snippet, ssl.conf, which includes the chippers recommended by Mozilla, enables OCSP Stapling, HTTP Strict Transport Security (HSTS), and enforces a few security-focused HTTP headers.

sudo nano /etc/nginx/snippets/ssl.conf
/etc/nginx/snippets/ssl.conf
ssl_dhparam /etc/ssl/certs/dhparam.pem;

ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers on;

ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 30s;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;

Create New Site

Now, create an Nginx config file for the site:

sudo nano /etc/nginx/sites-available/example.com

Paste the following content:

/etc/nginx/sites-available/example.com
# Redirect every request to HTTPS
# server {
#   listen 80;
#   listen [::]:80;
#   server_name example.com www.example.com;
#   return 301 https://$host$request_uri;
# }

# Redirect root domain to www
# server {
#   listen 443 ssl http2;
#   listen [::]:443 ssl http2;
#   ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
#   ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
#   ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
#   include snippets/ssl.conf;
#   server_name example.com;
#   return 301 https://www.example.com$request_uri;
# }

server {
#   listen 443 ssl http2;
#   listen [::]:443 ssl http2;
#   ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
#   ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
#   ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
#   include snippets/ssl.conf;
#   server_tokens off;
#   server_name www.example.com;

  # Remove this line after running certbot
  server_name example.com www.example.com;

  root /home/superman/example.com;
  index index.html index.htm index.php;

  charset utf-8;

  location / {
    try_files $uri $uri/ =404;  # Static
    # try_files $uri $uri/ /index.php?$args;  # WordPress
    # try_files $uri $uri/ /index.php?$query_string;  # Laravel
  }

  location = /favicon.ico {
    access_log off; log_not_found off;
  }
  
  location = /robots.txt {
    access_log off; log_not_found off;
  }

  access_log off;
  error_log  /var/log/nginx/example.com-error.log error;

  # For PHP sites only, remove for static websites
  location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/var/run/php/php-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
  }

  location ~ /\.(?!well-known).* {
    deny all;
  }
}
Expand

Activate the new site:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

Create the root directory for our website example.com:

mkdir ~/example.com

Reload Nginx:

sudo systemctl reload nginx

Obtain SSL Certificate

Obtain an SSL certificate for our new site:

sudo certbot certonly -d example.com,www.example.com --nginx -n --agree-tos -m [email protected] --no-eff-email

In addition, it also takes care of auto-renewal once the certificate expires every 90 days.

Now, edit the Nginx config file and uncomment the SSL configurations:

sudo nano /etc/nginx/sites-available/example.com

Reload Nginx:

sudo systemctl reload nginx

To verify that the SSL certificate is successfully installed, open our website and notice that all HTTP traffic is now redirected to HTTPS, and the browser should display a secure lock icon.

If we test our domains using the SSL Labs Server Test, it should receive an A+ grade.

Install WordPress

Edit the Nginx config file and uncomment the WordPress configurations:

sudo nano /etc/nginx/sites-available/example.com
/etc/nginx/sites-available/example.com
location / {
  # try_files $uri $uri/ =404;  # Static
  try_files $uri $uri/ /index.php?$args;  # WordPress
  # try_files $uri $uri/ /index.php?$query_string;  # Laravel
}

Log in to the MySQL admin:

sudo mysql

Create a new database and user:

CREATE DATABASE wordpress;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'wps3cr3t';
GRANT ALL ON wordpress.* TO 'wpuser'@'localhost';
EXIT;

Create the root directory for our website example.com and cd into it:

mkdir ~/example.com && cd $_

Download and install WordPress:

curl -LO https://wordpress.org/latest.tar.gz
tar xzvf latest.tar.gz --strip-components=1
rm latest.tar.gz

Edit wp-config.php:

cp wp-config-sample.php wp-config.php && nano wp-config.php

Update the database settings:

wp-config.php
define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'wpuser' );
define( 'DB_PASSWORD', 'wps3cr3t' );
define( 'DB_COLLATE', 'utf8_unicode_ci' );

Generate unique authentication keys and salts:

curl -s https://api.wordpress.org/secret-key/1.1/salt/

You will receive unique values that look like this:

define('AUTH_KEY',         '(DIWR|;6RAWO>V6<5GzP9|5E)=3W`xNA4RA;7Bsu-)F+K[u+n/XRZU)G)0-~L8$R');
define('SECURE_AUTH_KEY',  'ZM_2)$adDiw1G~aZS_[Ck6n)10C`&g#70=LIi! :kz-+YST@E[+gwY#i|.D.5zNm');
define('LOGGED_IN_KEY',    'IT7=3TU6;+}]ogDJ8Gk7i=Ft3L%D=*xS^uz{sN++=E2#@|nsW-?[@B-+|=w?;nb+');
define('NONCE_KEY',        '+$V-/)mf6n^ ,cWFJt|j>tFIHcKJD>.P>1BP|t|h`a*y521zOY5(0g/5KUb+?D|c');
define('AUTH_SALT',        '6PFc5Z,}&=TFh,-6aG-_al?LB4)o}R zI!O`fi$N-k#glI1-uLwIy&DC+8aNRv1S');
define('SECURE_AUTH_SALT', 'ua %?YxfShL?je^`$D7?;z!a(/BtWhmmwG^M5pGug&6O.^[l^N,0cZVc]wPg{6t+');
define('LOGGED_IN_SALT',   '5(K~Z^fE--O.+F?)fh-*)N-vF0gn;m%T,r~;ZpiN0E[#(Py.?p2p^aYiI}X`jy|;');
define('NONCE_SALT',       '~m^QBW-AfNvs:_&{|2ZPy|}9vg&$5mGzW8k-L5M^bl`%Jd`&|q|idfQ?zt6w:U+]');

Update the wp-config.php file with these generated values to enhance the security of your WordPress installation.

Ensure the WordPress root directory is owned by www-data:

sudo chown -R www-data:www-data .

Reload Nginx:

sudo systemctl reload nginx

Install Laravel

To install Laravel, we first need to install Composer:

curl -sS https://getcomposer.org/installer -o composer-setup.php

Verify the hash:

HASH=$(curl -sS https://composer.github.io/installer.sig)
echo "$HASH composer-setup.php" | sha384sum --check

Run the Composer installer:

sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php

Verify that Composer has been installed correctly:

composer --version

Composer is now installed. Now, edit the Nginx config file and uncomment the Laravel configurations:

/etc/nginx/sites-available/example.com
sudo nano /etc/nginx/sites-available/example.com
location / {
  # try_files $uri $uri/ =404;  # Static
  # try_files $uri $uri/ /index.php?$args;  # WordPress
  try_files $uri $uri/ /index.php?$query_string;  # Laravel
}

Create the root directory for our website example.com and cd into it:

mkdir ~/example.com && cd $_

Now install Laravel via Composer:

cd ~/example.com && rm -rf ./*
composer create-project laravel/laravel .

Reload Nginx:

sudo systemctl reload nginx

Key Takeways

By now, you should have a clear understanding of how to set up a web server from scratch. This guide covers the essential steps, from initial server setup and user creation to configuring Nginx, MySQL, and PHP.

With this comprehensive overview, you should be well-equipped to manage your own server and streamline your hosting process. For those looking to delve deeper into how to set up a web server, additional resources and support are available to guide you through more advanced configurations and optimizations.

References

Published on 7th Dhul Qadah, 1445 AH at Tampa, FL.