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 -yReboot the system:
rebootLog 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:
passwdEnter a new password and re-type to confirm.
Then create a new user (e.g. superman):
adduser --gecos "" supermanChoose 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 supermanSet permissions for the home directory to ensure that the web roots within this directory are accessible by the web server.
chmod o+x /home/supermanEdit the new user’s .bashrc file:
nano /home/superman/.bashrcAdd 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.
# Set directory permissions to 755 and file permissions to 644
umask 022To 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:
# Set the terminal type for compatibility with other terminal emulators (i.e. supports the "clear" command)
export TERM=xtermCreate a sudoers file for the new user:
sudo nano /etc/sudoers.d/supermanAdd 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.
superman ALL=(ALL) NOPASSWD: /usr/bin/rsyncSet proper permissions:
sudo chmod 440 /etc/sudoers.d/supermanEnable 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/supermanHarden OpenSSH Config
Create a backup of the sshd_config file:
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bakEdit the config:
nano /etc/ssh/sshd_configUpdate the following settings:
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 noNote that the DebianBanner option may not be present in the configuration file; it may need to be added manually.
Next, validate the syntax:
sshd -tIf everything is correct, no message will be displayed.
Setup Firewall
Allow SSH through the firewall:
ufw allow OpenSSHEnable the firewall and check its status:
ufw --force enable
ufw statusReboot the system:
rebootNow, SSH into the server using the new user:
Add Swap Space
Create a 1GB swap file:
sudo fallocate -l 1G /swapfileChange the permissions of the swap file so that only the root user can read and write to it:
sudo chmod 600 /swapfileSet up the swap file by marking it as swap space:
sudo mkswap /swapfileActivate the swap file so the system starts using it:
sudo swapon /swapfileCheck that the swap is now active:
sudo swapon --showor
free -hTo 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/fstabSet swappiness to a lower value (i.e. 10) for the system to avoid using swap space unless absolutely necessary:
sudo sysctl vm.swappiness=10Make this change permanent:
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.confInstall LEMP Stack
At this stage, we will install Nginx, MariaDB, and PHP on the server.
Install Nginx
Run:
sudo apt install -y nginxAllow both HTTP and HTTPS traffic:
sudo ufw allow 'Nginx Full'Verify the change by checking the status:
sudo ufw statusKeep 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/defaultPaste the following content:
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
server_tokens off;
return 444;
}Reload Nginx:
sudo systemctl reload nginxInstall MariaDB
We will install MariaDB, as it is the open-source counterpart of MySQL (owned by Oracle):
sudo apt install -y mariadb-serverSecure the MariaDB server:
sudo mysql_secure_installationChoose 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 mysqlThis 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:
exitImageMagick
Install ImageMagick to be used with PHP for image manipulation:
sudo apt install -y imagemagickInstall 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-imagickSSL 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-nginxGenerate 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 2048Configure 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.confssl_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.comPaste the following content:
# 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;
}
}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.comReload Nginx:
sudo systemctl reload nginxObtain 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-emailIn 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.comReload Nginx:
sudo systemctl reload nginxTo 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.comlocation / {
# 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 mysqlCreate 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.gzEdit wp-config.php:
cp wp-config-sample.php wp-config.php && nano wp-config.phpUpdate the database settings:
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 nginxInstall Laravel
To install Laravel, we first need to install Composer:
curl -sS https://getcomposer.org/installer -o composer-setup.phpVerify the hash:
HASH=$(curl -sS https://composer.github.io/installer.sig)
echo "$HASH composer-setup.php" | sha384sum --checkRun the Composer installer:
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.phpVerify that Composer has been installed correctly:
composer --versionComposer is now installed. Now, edit the Nginx config file and uncomment the Laravel configurations:
sudo nano /etc/nginx/sites-available/example.comlocation / {
# 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 nginxKey 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
- Initial Server Setup with Ubuntu
- How To Harden OpenSSH on Ubuntu 20.04
- How to Install LEMP Stack on Ubuntu
- How To Secure Nginx with Let’s Encrypt on Ubuntu
- Secure Nginx with Let’s Encrypt on Ubuntu 20.04
- Mozilla SSL Configuration Generator
- How to Install WordPress with LEMP on Ubuntu 22.04
Published on 7th Dhul Qadah, 1445 AH at Tampa, FL.


Leave a Reply