Tutorial: How to Set Up a Secure VPS Server with SSH, UFW, Nginx, SSL, and Docker

Tutorial: How to Set Up a Secure VPS Server with SSH, UFW, Nginx, SSL, and Docker

Diogo Parente

Diogo Parente

Introduction

As an indie hacker, I often need to quickly set up environments for new app ideas while keeping costs low. I developed a simple, budget-friendly approach that lets me deploy projects efficiently without sacrificing security or scalability. In this guide, I’ll walk you through the steps to set up a secure VPS, covering everything from basic SSH configuration to deploying essential services like Nginx, Docker, and SSL.

For sake of simplicity, I'll assume:

  • you have a VPS (I use hetzner's CX22 with 4GB of ram and 40GB of NVMe SSD, but you can use anything else)
  • your app's domain is example.com
  • your private and public keys file names are example and example.pub
  • your server ip address is 1.1.1.1
  • your email is your-email@email.com
  • your server user name is new_user_name

Don't worry, this will all make sense soon.

1. Connect to Your VPS

Once you have your server up and running, the first step is to connect to it using SSH.

ssh root@<server_ip_address>

2. Run System Updates

It’s important to make sure your server is running the latest updates for security and performance. Run the following commands to update and upgrade all packages.

sudo apt-get update && sudo apt-get upgrade

This will fetch and install the latest updates for your system packages.

3. Create a New User with Sudo Privileges

sudo adduser <new_user_name>
sudo usermod -aG sudo <new_user_name>

This will allow the new user to perform administrative tasks when needed.

4. Set Up SSH for New User

Switch to the new user:

su <new_user_name>

On your local machine, generate a new SSH key pair (if you don’t have one already):

ssh-keygen -t ed25519 -C "your-email@email.com"

Save it with a name you can remember as you'll need it soon. For example purposes, lets call it "example", as mentioned in the beginning.

On the VPS, create the .ssh directory and paste your public key:

mkdir ~/.ssh && mkdir ~/.ssh/authorized_keys

Back at your local machine, assuming your public key is at ~/.ssh/example.pub:

cat ~/.ssh/example.pub | ssh <new_user_name>@<server_ip_address> 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys'```

5. Disable Root Login and Password Authentication

For added security, you should disable root login and password-based authentication.

Provide the correct permissions to your server's .ssh folder:

chmod 755 ~
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Then, edit the SSH configuration:

sudo vim /etc/ssh/sshd_config

Set the following configurations to disable root login and password authentication:

PubkeyAuthentication yes
PermitRootLogin no
PasswordAuthentication no

Edit your host SSH config

vim ~/.ssh/config
Host 1.1.1.1 # your server's ip here
  HostName 1.1.1.1 # your server's ip here
  User your_user
  IdentityFile ~/example # your public key file (example.pub)

6. Enable Firewall (UFW)

Enable UFW to allow only specific traffic:

sudo apt install ufw
sudo ufw enable
sudo ufw allow OpenSSH
sudo ufw allow 22

This ensures that SSH traffic is allowed while securing other ports.

7. Install Fail2Ban for Brute Force Protection

Fail2Ban helps protect your server from brute force attacks. Install it and configure:

sudo apt install fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo vim /etc/fail2ban/jail.local

Ensure SSHD jail is enabled:

[sshd]
enabled = true

8. Install and Configure Nginx

Install Nginx as your web server:

sudo apt-get install nginx
sudo ufw allow 'Nginx Full'

Then, configure it to act as a reverse proxy for your app. Open the configuration file:

sudo vim /etc/nginx/sites-available/default

Replace the contents with:

server {
    listen 80;
    server_name example.com www.example.com;
 
    location / {
        proxy_pass http://localhost:3000; # Replace 3000 with the port your app is running on
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Replace example.com with your actual domain name.

The proxy_pass line should point to the port your application is running on (e.g., port 3000 for a Node.js app).

9. Enable Nginx

To ensure everything is configured properly, test the configuration and restart Nginx:

sudo nginx -t
sudo systemctl restart nginx

10. Setup SSL with Certbot

Certbot will automatically configure SSL for your domain and generate the certificates.

Install Certbot and the Nginx plugin:

sudo apt update
sudo apt install certbot python3-certbot-nginx

Note: Before running this step, you need to set you domains and point them to your server's ip (using 1.1.1.1 as an example). If you just did it, wait a few minutes before moving to the next step so that DNS can broadcast.

Type Host Answer TTL Priority Options
A example.com 1.1.1.1 600
A www.example.com 1.1.1.1 600
CNAME *.example.com 1.1.1.1 600

Run Certbot for Nginx

sudo certbot --nginx

When prompted: Enter your email address (for renewal and security notices). Agree to the terms of service. Select the domain(s) you want to enable SSL for (e.g., example.com and www.example.com).

Verify the Nginx Configuration:

sudo vim /etc/nginx/sites-available/default

It should look similar to this:

server {
    listen 443 ssl; # managed by Certbot
    server_name example.com www.example.com;
 
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
 
    # Recommended security settings
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384";
 
    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
 
server {
    if ($host = www.example.com) {
        return 301 https://$host$request_uri;
    }
    if ($host = example.com) {
        return 301 https://$host$request_uri;
    }
 
    listen 80;
    server_name example.com www.example.com;
    return 404; # managed by Certbot
}

(Optional) - Force Https

If Certbot didn’t already set up HTTP-to-HTTPS redirection, you can do this manually by modifying your server block for port 80 to redirect all traffic to HTTPS:

server {
    listen 80;
    server_name example.com www.example.com;
 
    location / {
        return 301 https://$host$request_uri;
    }
}

Test SSL and Reload Nginx:

Certbot should automatically reload Nginx after successful installation, but you can test your configuration and reload Nginx manually just to ensure everything is working.

bash
sudo nginx -t
sudo systemctl reload nginx

Test automatic renewal:

Let’s Encrypt certificates are valid for 90 days, but Certbot will automatically renew them for you. To make sure it’s working, you can test the renewal process:

sudo certbot renew --dry-run

11. Install Docker

Update and install prerequisites

sudo apt update
sudo apt install apt-transport-https ca-certificates curl software-properties-common

Add Docker GPG key

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

Add Docker repository

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Install Docker

sudo apt update
sudo apt install docker-ce

Verify Docker installation

sudo docker --version
sudo docker run hello-world

(Optional) Add current user to Docker group

sudo usermod -aG docker ${USER}
newgrp docker

Enable Docker at startup

sudo systemctl enable docker

12. Set Up Unattended Upgrades

sudo apt install unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades

Now your VPS is fully secured and configured with essential services like Nginx, SSL, and Docker. You’ve also set up SSH key-based authentication, a firewall, and brute-force protection with Fail2Ban.

In the next post, I'll teach you how to integrate this deployment strategy with a CI/CD pipeline so you can manage your code remotely.

Happy deploying!

Diogo

Contact me