First of all we’ll need to create an SSH Key pair.
you can generate ssh keys using this command:
ssh-keygen -t ed25519 -C "your_email@example.com"
I always save my ssh keys in .ssh folder, name it after server I want to get it and use a proper passphrase just in case.
If you are just creating a server on Hetzner then paste generated .pub file it into the SSH Keys section
You can copy using this commands
cat key_name.pub | xclip -selection clipboard # Linux
cat key_name.pub | pbcopy # macOS
type key_name.pub | clip # Windows
Now you can log in into the server through the terminal using this commant
ssh -i /path-to-ssh/.ssh/key_name root@Y.OURI.PAD.DR
You can add an alias to .zshrc or .bashrc terminal on your local machine and instead of writing the ssh -i
repeatadly you can use your defined alias
.
nano ~/.zshrc #or .bashrc
alias key_name='ssh -i /path-to-ssh/.ssh/key_name root@Y.OURI.PAD.DR' # save and quit
source ~/.zshr # or .bashrc
Basic Security Considerations for VPS
Create Sudo enabled user that you will use from now on
sudo adduser myusername
sudo usermod -aG sudo myusername # grant user sudo privileges
Will move the same ssh key to the new sudo enabled user.
first will check if myusername has required permission.
sudo - myusername
sudo apt update # check if user got sudo privileges
exit
then with exit we go back to root user and copy authorized keys from root to myusername.
sudo mkdir /home/myusername/.ssh
sudo cp /root/.ssh/authorized_keys /home/myusername/.ssh/
Then we need to make sure that myusername has correct permission for .ssh folder and authorized_keys file
sudo chown -R myusername:mygroup /home/myusername/.ssh
sudo chmod 700 /home/myusername/.ssh
sudo chmod 600 /home/myusername/.ssh/authorized_keys
These commands ensure that:
- The
.ssh
directory is owned by themyusername
user. - The
.ssh
directory has the correct permissions (700
means only the user can read/write/execute). - The
authorized_keys
file has the correct permissions (600
means only the user can read/write).
Now, let’s test ssh trying to log in through myusername. Go back to your local terminal and enter the command.
ssh -i /path-to-ssh/.ssh/key_name myusername@Y.OURI.PAD.DR
Now, you should be able to login to the VPS with sudo enabled user.
- Don’t Forget to change alias username to your created user
Afther this is done, we need to login to the VPS once more with the root user
ssh -i /path-to-ssh/.ssh/key_name root@Y.OURI.PAD.DR
Configuring SSH Security
Disable password authentication by editing /etc/ssh/sshd_config
and setting PasswordAuthentication no
.
sudo nano /etc/ssh/sshd_config
And change this two fields
PasswordAuthentication no # only allow SSH authentication
PermitRootLogin no # Disable root login via SSH
PubkeyAuthentication yes # enable pubkey usage
After you have saved the file run
sudo systemctl restart ssh
Now exit the vps with exit
command and try to login to vps with ssh and you shouldn’t be able to.
ssh -i /yourpath/.ssh/key_name root@Y.OURI.PAD.DR
Enter passphrase for key '/yourpath/.ssh/key_name':
root@Y.OURI.PAD.DR: Permission denied (publickey).
If that works, you should be able to login with your sudo enabled user and continue the guide.
ssh -i /yourpath/.ssh/key_name myusername@Y.OURI.PAD.DR
Just in case update everything in ubuntu
sudo apt update && sudo apt full-upgrade -y && sudo apt autoremove -y && sudo apt clean
Start firewall
sudo ufw enable
sudo ufw allow ssh
setup fail2ban
sudo apt install fail2ban -y
sudo systemctl start fail2ban # Start fail2ban
sudo systemctl enable fail2ban # enable on system boot
Configure Fail2Ban
Fail2Ban’s default configuration is located in /etc/fail2ban/
. To customize it, you’ll modify the jail.local
file instead of the default jail.conf
, as this ensures your settings won’t be overwritten during updates.
sudo nano /etc/fail2ban/jail.local
Configure Fail2Ban for SSH Protection: Ensure SSH is being monitored by Fail2Ban. In the [sshd]
section of the jail.local
file, make sure the settings look like this:
[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
Explanation:
enabled = true
enables SSH monitoring.maxretry = 3
means that if there are 3 failed login attempts within thefindtime
window, the IP will be banned.bantime = 3600
means the IP will be banned for 1 hour.findtime = 600
means the time window is 10 minutes.
restart fail2ban
sudo systemctl restart fail2ban
check fail2ban status
sudo fail2ban-client status
Check specific fail2ban status
sudo fail2ban-client status sshd
install nginx
sudo apt update
sudo apt install nginx -y
sudo systemctl start nginx # start the nginx service
sudo systemctl enable nginx # enable nginx to start at boot
sudo systemctl status nginx # verify the nginx service status
Allow Nginx traffic in firewall
sudo ufw allow 'Nginx Full'
Now you can go to your ip address and you should see this image
http://your_server_ip

Installing postgresql (if you want sqlite skip to sqlite part, this is not necessary)
sudo apt install postgresql postgresql-contrib -y
sudo systemctl start postgresql # start PostgreSQL service
sudo systemctl enable postgresql # Enable PostgreSQL to start at boot
sudo systemctl status postgresql # check service status
Configure Postgresql
sudo -i -u postgres # Switch to the postgres user
psql # Access the PostgreSQL prompt
Create a new user and database
CREATE USER myuser WITH PASSWORD 'mypassword';
CREATE DATABASE mydatabase OWNER myuser;
GRANT ALL PRIVILEGES ON DATABASE mydatabase TO myuser;
exit psql
\q
Secure Your Database
- Why: If your database is exposed or poorly secured, attackers can gain access to sensitive data.
- What to Do:
- Use strong passwords for database users.
- If possible, restrict access to the database to only local connections or trusted IPs (e.g., using
bind-address
in MySQL or PostgreSQL). - For PostgreSQL, edit
/etc/postgresql/postgresql.conf
to bind it to localhost (listen_addresses = 'localhost'
). - Use SSL for database connections if you need to access it remotely.
- Regularly back up your database and encrypt backups.
Install SQLlite
sudo apt install sqlite3 -y
sqlite3 --version # verify installation and possibility to use sqlite3 shell
install node and npm
sudo apt install nodejs -y
sudo apt install npm -y
Check if installed succesfully
node -v
npm -v
move your nextjs app from github to VPS
Because I mostly use github, I will show you how to import your nextjs app from github to your vps.
First, go to your account on github and access url https://github.com/settings/personal-access-tokens
Generate a new Fine-grained personal Access token
get token managers to be able to fetch etc. Choose a Token Name, resource owner and Expiration as long as you prefer.
I personally only allow token repository access per repository I am inteding to use for that token, so if you store your nextjs app in one repo just allow access to that repo.
See image below

Because I only perform push/pull actions and check commits on my vps mostly, and everything is done locally and also for thsi tutorial Content
and Commit statuses
read and write permissions are fine.

Press generate token and that’s done. Keep the tab with the access token open for a bit and now we will clone the repo.
At first I like to store my credentials. This will store your credentials in plain text file you can use credential manager for extra security if you like.
git config --global credential.helper store
Enter this into your terminal in the location where you want to store the app. (I mostly store it in /home directory)
git clone https://github.com/GitUSERNAME/REPO-name.git
When git asks for your git password instead of password you must entered your fine-grained personal access token. After you have cloned the repo you can run git pull
again to check if git has saved your credentials.
Now we are going to run the nextjs app process and manage it with pm2
npm install
npm run build
Run and build nextjs
install PM2 to manage node processes
# Install PM2 globally
sudo npm install -g pm2
Start your Next js Process
pm2 start npm --name "nextjs-app" -- start
pm2 startup # to start all the processes from reboot
# it might ask you to copy/paste the following command:
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u your_user --hp /home/your_user
# copy and paste the command given in the terminal and run it.
pm2 save # to save the current process list
setup nginx and cloudflare
First create a directory in Ngnix folder to store all the ssl certificates
cd /etc/ngnix/
sudo mkdir ssl
cd ssl
Then buy a domain or redirect your domain nameservers to cloudflare.
And add 2 DNS records (see table and image below)
Type | Name | Content | Proxy Status | TTL |
A | @ | Hetzner VPS IP | proxied | auto |
CNAME | www | yourdomain.com | proxied | auto |

Then go to create Origin certificate (See image below for instructions)

I always generate Private key type ECC, hostnames leave default, certificate validity 15 years

Add the generated certificates to the /etc/nginx/ssl
directory
sudo nano /etc/nginx/ssl/yourdomain.pem # paste the Origin Certificate here
sudo nano /etc/nginx/ssl/yourdomain.key # paste Private Key here
Turn on the ‘Authenticated Origin Pulls’

For this we need to add 'Cloudflare Origin ECC PEM'
to our /etc/nginx/ssl
directory. You can find it here or just copy the Cloudflare Origin CA root certificate from below.
-----BEGIN CERTIFICATE-----
MIIGCjCCA/KgAwIBAgIIV5G6lVbCLmEwDQYJKoZIhvcNAQENBQAwgZAxCzAJBgNV
BAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMRQwEgYDVQQLEwtPcmln
aW4gUHVsbDEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZv
cm5pYTEjMCEGA1UEAxMab3JpZ2luLXB1bGwuY2xvdWRmbGFyZS5uZXQwHhcNMTkx
MDEwMTg0NTAwWhcNMjkxMTAxMTcwMDAwWjCBkDELMAkGA1UEBhMCVVMxGTAXBgNV
BAoTEENsb3VkRmxhcmUsIEluYy4xFDASBgNVBAsTC09yaWdpbiBQdWxsMRYwFAYD
VQQHEw1TYW4gRnJhbmNpc2NvMRMwEQYDVQQIEwpDYWxpZm9ybmlhMSMwIQYDVQQD
ExpvcmlnaW4tcHVsbC5jbG91ZGZsYXJlLm5ldDCCAiIwDQYJKoZIhvcNAQEBBQAD
ggIPADCCAgoCggIBAN2y2zojYfl0bKfhp0AJBFeV+jQqbCw3sHmvEPwLmqDLqynI
42tZXR5y914ZB9ZrwbL/K5O46exd/LujJnV2b3dzcx5rtiQzso0xzljqbnbQT20e
ihx/WrF4OkZKydZzsdaJsWAPuplDH5P7J82q3re88jQdgE5hqjqFZ3clCG7lxoBw
hLaazm3NJJlUfzdk97ouRvnFGAuXd5cQVx8jYOOeU60sWqmMe4QHdOvpqB91bJoY
QSKVFjUgHeTpN8tNpKJfb9LIn3pun3bC9NKNHtRKMNX3Kl/sAPq7q/AlndvA2Kw3
Dkum2mHQUGdzVHqcOgea9BGjLK2h7SuX93zTWL02u799dr6Xkrad/WShHchfjjRn
aL35niJUDr02YJtPgxWObsrfOU63B8juLUphW/4BOjjJyAG5l9j1//aUGEi/sEe5
lqVv0P78QrxoxR+MMXiJwQab5FB8TG/ac6mRHgF9CmkX90uaRh+OC07XjTdfSKGR
PpM9hB2ZhLol/nf8qmoLdoD5HvODZuKu2+muKeVHXgw2/A6wM7OwrinxZiyBk5Hh
CvaADH7PZpU6z/zv5NU5HSvXiKtCzFuDu4/Zfi34RfHXeCUfHAb4KfNRXJwMsxUa
+4ZpSAX2G6RnGU5meuXpU5/V+DQJp/e69XyyY6RXDoMywaEFlIlXBqjRRA2pAgMB
AAGjZjBkMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgECMB0GA1Ud
DgQWBBRDWUsraYuA4REzalfNVzjann3F6zAfBgNVHSMEGDAWgBRDWUsraYuA4REz
alfNVzjann3F6zANBgkqhkiG9w0BAQ0FAAOCAgEAkQ+T9nqcSlAuW/90DeYmQOW1
QhqOor5psBEGvxbNGV2hdLJY8h6QUq48BCevcMChg/L1CkznBNI40i3/6heDn3IS
zVEwXKf34pPFCACWVMZxbQjkNRTiH8iRur9EsaNQ5oXCPJkhwg2+IFyoPAAYURoX
VcI9SCDUa45clmYHJ/XYwV1icGVI8/9b2JUqklnOTa5tugwIUi5sTfipNcJXHhgz
6BKYDl0/UP0lLKbsUETXeTGDiDpxZYIgbcFrRDDkHC6BSvdWVEiH5b9mH2BON60z
0O0j8EEKTwi9jnafVtZQXP/D8yoVowdFDjXcKkOPF/1gIh9qrFR6GdoPVgB3SkLc
5ulBqZaCHm563jsvWb/kXJnlFxW+1bsO9BDD6DweBcGdNurgmH625wBXksSdD7y/
fakk8DagjbjKShYlPEFOAqEcliwjF45eabL0t27MJV61O/jHzHL3dknXeE4BDa2j
bA+JbyJeUMtU7KMsxvx82RmhqBEJJDBCJ3scVptvhDMRrtqDBW5JShxoAOcpFQGm
iYWicn46nPDjgTU0bX1ZPpTpryXbvciVL5RkVBuyX2ntcOLDPlZWgxZCBp96x07F
AnOzKgZk4RzZPNAxCXERVxajn/FLcOhglVAKo5H0ac+AitlQ0ip55D2/mf8o72tM
fVQ6VpyjEXdiIXWUq/o=
-----END CERTIFICATE-----
paste this certificate into this file
sudo nano /etc/nginx/ssl/cloudflare_origin.pem
Let’s configure SSL/TLS encryption on cloudflare dashboard. Select Overview -> configure then Full (Strict) -> Save. (See images below).


now we will configure nginx configuration
sudo nano /etc/nginx/sites-available/mywebapp.com
paste your own edit config from here
server {
listen 443 ssl; # Listen on port 443 for HTTPS
server_name yourdomain.com; # Replace with your domain
# Path to the Cloudflare SSL certificates
ssl_certificate /etc/nginx/ssl/yourdomain.pem; # The Cloudflare SSL certificate for your domain
ssl_certificate_key /etc/nginx/ssl/yourdomain.key; # The private key for the Cloudflare SSL certificate
# The client certificate is verified using Cloudflare's certificate to ensure secure and authenticated connections
ssl_client_certificate /etc/nginx/ssl/cloudflare_origin.pem;
ssl_verify_client on;
# SSL settings: Force stronger encryption
ssl_protocols TLSv1.2 TLSv1.3; # Allow only secure protocols
ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384'; # Strong cipher suites
ssl_prefer_server_ciphers on; # Prioritize server ciphers
ssl_session_cache shared:SSL:10m; # SSL session cache size
# Enforce HTTPS connections only (disable HTTP)
ssl_dhparam /etc/nginx/ssl/dhparam.pem; # Diffie-Hellman parameters for key exchange (use OpenSSL to generate it)
# Secure headers to protect against attacks
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Enforce HTTPS for all subdomains
add_header X-Content-Type-Options nosniff; # Prevent MIME type sniffing
add_header X-Frame-Options DENY; # Prevent clickjacking
add_header X-XSS-Protection "1; mode=block"; # Enable XSS protection
add_header Referrer-Policy "no-referrer"; # Don't send referrer information
# Location block to serve the reverse proxy
location / {
proxy_pass http://localhost:3000; # Forward requests to the Next.js app running on localhost:3000
proxy_set_header Host $host; # Preserve the original host header
proxy_set_header X-Real-IP $remote_addr; # Forward the real IP address of the client
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Chain any forwarded IP addresses
proxy_set_header X-Forwarded-Proto $scheme; # Preserve the protocol (HTTP/HTTPS)
proxy_cache_bypass $http_upgrade; # Bypass caching for upgrade requests (like WebSockets)
}
}
Generate Diffie-Hellman Parameters (for SSL security):
Diffie-Hellman (DH) parameters enhance the security of your SSL/TLS connections. To generate them, use this command:
sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
To enable available sites on nginx we need to create a symbolic link between /etc/nginx/sites-available/yourdomain.com
to /etc/nginx/sites-enabled/yourdomain.com
sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/
Check if nginx configuration is correct with
sudo nginx -t
# if correct you'll get this
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
We restart nginx for configuration to take effect
sudo systemctl restart nginx
Some extra firewall settings to update and we will be done
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw delete allow 'Nginx Full'
sudo ufw delete allow 'Nginx Full (v6)'
sudo ufw allow 'Nginx HTTPS'
sudo ufw allow 'Nginx HTTPS (v6)'
Now run sudo ufw status
and you should see.
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
Nginx HTTPS ALLOW Anywhere
22/tcp (v6) ALLOW Anywhere (v6)
Nginx HTTPS (v6) ALLOW Anywhere (v6)
If you access yourdomain.com, you should be able to see your nextjs app accessible if DNS records have propogatted. You can confirm that on dnschecker.org.
If you have any problems and issues you can contact me on x.com or Linkedin.com. If I have the time, I’ll try to help you.