Initial Steps Required for backup after UBUNTU LTS 24.04 Server install on Hostinger VPS

First let’s connect and verify the new server:

bash

ssh root@31.220.58.108

Once connected run:

bash

# Verify Ubuntu version
lsb_release -a

# Check you are root
whoami

# Check disk space
df -h

In case error connecting— you reinstalled the VPS so the server’s SSH fingerprint changed. Not a security issue. Fix it:

powershell

ssh-keygen -R 31.220.58.108

Then connect again:

powershell

ssh root@31.220.58.108

Type yes when asked to confirm the new fingerprint. You’re in.

Now we go step by step. Do one step at a time, paste output after each.


Step 1 — System Update

bash

apt update && apt upgrade -y

Step 2 — Install All Required Software

bash

apt install -y curl git nginx mariadb-server ufw fail2ban

Step 3 — Install Node.js 20

bash

curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
node --version
npm --version

Step 4 — Install PM2

bash

npm install -g pm2
pm2 --version

Step 5 — Create User mks

bash

adduser mks
usermod -aG sudo mks

It will ask you to set a password for mks. Set it then press Enter through the rest.

Everything installed perfectly. Let me confirm what’s done:

ComponentVersionStatus
Ubuntu 24.04 LTSnoble
Nginx1.24
MariaDB10.11.14
Node.jsv20.20.2
npm10.8.2
PM27.0.1
fail2ban1.0.2
ufw0.36.2
user mkscreated

One thing to note: There’s a pending kernel upgrade message. This just means the new kernel was installed but needs a reboot to activate. Not urgent — we’ll reboot after setup is complete.


Step 6 — Reboot to Load New Kernel

bash

reboot

Reconnect after 30 seconds:

powershell

ssh root@31.220.58.108

Then verify:

bash

uname -r

Should show 6.8.0-117-generic.

New kernel loaded ✅

Step 7 — Configure UFW Firewall

bash

ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status verbose

Type y when it asks “Command may disrupt existing ssh connections. Proceed with operation?”

UFW configured perfectly ✅

Only ports 22, 80, 443 open. Clean and secure.


Step 8 — Secure MariaDB

Run the security script:

bash

mysql_secure_installation

Answer each question like this:

Enter current password for root: (just press Enter — no password yet)
Switch to unix_socket authentication: N
Change the root password: N
Remove anonymous users: Y
Disallow root login remotely: Y
Remove test database: Y
Reload privilege tables: Y

After it finishes:

bash

sudo mysql -e "SELECT user, host, plugin FROM mysql.user;"

What that command means

bash

sudo mysql -e "SELECT user, host, plugin FROM mysql.user;"
PartMeaning
sudoRun as root/administrator
mysqlOpen MariaDB client
-e "..."Execute this SQL query directly without opening interactive shell
SELECT user, host, pluginShow 3 columns: username, where they can connect from, how they authenticate
FROM mysql.userFrom the built-in mysql database that stores all user accounts

It simply shows who can log into your database and how. Run it now and paste output.


Are we right to install as root?

Yes — completely correct for a VPS server. Here is the logic:

On a PC, you install software for one person. On a VPS server, you install software for the entire system — Nginx, MariaDB, Node.js — these serve all users and all websites. Only root can install system-level software.

The standard practice is:

  • root — installs system software (Nginx, MariaDB, Node.js, UFW)
  • mks (your user) — runs your projects day to day
  • www-data — Nginx worker runs as this user automatically

We created mks user for daily use. Root is only for system administration.


Step 9 — Create Database and User

bash

sudo mysql

Then run these inside MariaDB:

sql

CREATE DATABASE ksmquiz_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

sql

CREATE USER 'ksmquiz_db_user'@'localhost' IDENTIFIED BY 'YOUR_DB_PASSWORD';

sql

GRANT ALL PRIVILEGES ON ksmquiz_db.* TO 'ksmquiz_db_user'@'localhost';

sql

FLUSH PRIVILEGES;

sql

SHOW DATABASES;

sql

SELECT user, host FROM mysql.user WHERE user='ksmquiz_db_user';

sql

EXIT;

Why sudo mysql even as root?

On Ubuntu, MariaDB uses unix socket authentication for the root user. This means:

  • MariaDB root login is tied to the Linux root user
  • When Linux root runs mysql — it authenticates automatically via the socket
  • No password needed
  • sudo mysql = “run mysql as Linux root” = automatic MariaDB root access

If you try mysql as user mks without sudo — it will ask for a password and fail because mks is not the Linux root user.


Should You Create DB as Root or mks?

As root — because creating databases and users is a system administration task. Same reason we installed Nginx and MariaDB as root.

Think of it this way:

TaskWho Does ItWhy
Install MariaDBrootSystem level
Create databasesrootAdmin level
Create DB usersrootAdmin level
App connects to DBksmquiz_db_userApp level — limited access
Run Node.js appmksUser level

The Flow

root creates ksmquiz_db
root creates ksmquiz_db_user with password
root grants ksmquiz_db_user access to ksmquiz_db only

App (running as mks via PM2) connects using:
  host: localhost
  user: ksmquiz_db_user
  password: from .env file
  database: ksmquiz_db

Root never touches the app. App never touches root.


You are currently logged in as root so just run:

bash

sudo mysql

The sudo works even as root — it just confirms you want admin privileges. Some people run mysql directly as root without sudo and it also works on Ubuntu. Both are fine.

WhatStatus
Database ksmquiz_db✅ Created
User ksmquiz_db_user✅ Created, localhost only
Privileges granted✅ Full access to ksmquiz_db only

Step 10 — Restore Database from Backup

What we are doing and why: We have the compressed backup file quizapp_full_20260516_190913.sql.gz on your PC. We need to:

  1. Upload it from PC to VPS
  2. Restore it into the new ksmquiz_db database

Why gunzip and pipe directly: The file is compressed (.gz). Instead of decompressing first then importing (takes double space), we decompress and import in one single command using a pipe |. The | means “send output of left command directly into right command”.


First — Upload backup from PC to VPS

Open Windows PowerShell and run:

powershell

scp "C:\Users\User\Desktop\KSM QUIZ\KSM_QUIZ_BACKUP\quizapp_full_20260516_190913.sql.gz" root@31.220.58.108:~/

What this does: Copies the backup file from your PC to the home directory (~/) of root on your VPS.

Paste output after upload completes.


Then verify it arrived on VPS:

bash

ls -lh ~/quizapp_full_20260516_190913.sql.gz

Then we restore.

Step 10 — Restore Database

What this command does:

bash

gunzip < ~/quizapp_full_20260516_190913.sql.gz | sudo mysql ksmquiz_db
PartMeaning
gunzip <Decompress the file and send its contents forward
|Pipe — send output of left command into right command
sudo mysql ksmquiz_dbFeed that SQL directly into ksmquiz_db database

It decompresses and imports in one step — no temporary files created.

Run it:

bash

gunzip < ~/quizapp_full_20260516_190913.sql.gz | sudo mysql ksmquiz_db

Then verify tables were restored:

bash

sudo mysql ksmquiz_db -e "SHOW TABLES;"

Then verify data exists in a table:

bash

sudo mysql ksmquiz_db -e "SELECT COUNT(*) FROM users;"

Database restored perfectly! 🎉

WhatStatus
All 10 tables restored
4 users in database
All data intact

Step 11 — Clone quizapp Code from GitHub

What we are doing and why: Your code is on GitHub. We clone it directly to the server — faster and cleaner than uploading files manually. We will put it in /var/www/ksmquiz — standard location for web projects on Linux.

Why /var/www/? This is the standard directory for websites on Linux servers. Nginx expects projects here by convention. Keeping all projects in one place makes management easier.

bash

mkdir -p /var/www/ksmquiz
git clone https://github.com/KidsScienceMagazine/kidssciencemagazine-quiz.git /var/www/ksmquiz
ls /var/www/ksmquiz

What git clone URL folder does: Downloads all code from GitHub into the specified folder. The /var/www/ksmquiz at the end tells git where to put it instead of creating a folder named after the repo.

before doing step 11, tell me why project folder ksmquiz is being created by root ? also all projects files are being backed up by root not mks? i want standard approach with explanation.

May 18

Excellent question. This is exactly the right thing to ask. Let me explain properly.


The Problem with Root Owning Project Files

When root creates /var/www/ksmquiz and clones into it:

  • Files are owned by root
  • Your app runs as mks via PM2
  • mks cannot write to root-owned files
  • This causes permission errors when app tries to create logs, upload files, write cache etc.

Standard Professional Approach

On a real production server the ownership model is:

WhoOwns WhatWhy
rootSystem software — Nginx, MariaDB, UFWAdmin level
mksProject files — /var/www/ksmquizApp level
www-dataNginx worker processWeb server level
ksmquiz_db_userDatabase ksmquiz_db onlyDB level

Project files should be owned by mks because:

  • PM2 runs as mks — needs read access to all files
  • Node.js app needs to write logs, temp files
  • Git operations done as mks — pushing, pulling updates
  • No need for root to touch project files after initial setup

The Correct Approach — Switch to mks Now

From this point forward — all project work is done as mks, not root.

Step A — Give mks ownership of /var/www

bash

chown mks:mks /var/www

Step B — Switch to mks user

bash

su - mks

You will see prompt change from root@srv1111860 to mks@srv1111860. You are now operating as mks.

Step C — Verify who you are

bash

whoami

Should return mks.


What About Git on the Server?

On your old server, git was configured for KidsScienceMagazine account using credential store — meaning HTTPS with saved token. On this fresh server we need to configure git for mks user properly.

Two options:

OptionMethodBest For
HTTPS with tokenStore token in git credentialsSimple, works everywhere
SSH keysSame as your PC setupMore secure, no token expiry

Since you already understand SSH keys from your PC setup — we will set up SSH keys on the VPS too. This way mks can pull/push to GitHub without ever entering a password.


Full Correct Flow From Here

root  → created system software ✅ done
root  → created database and user ✅ done
root  → gives /var/www to mks
mks   → sets up SSH key for GitHub
mks   → clones project into /var/www/ksmquiz
mks   → restores .env file
mks   → runs npm install
mks   → starts app with PM2
root  → configures Nginx (system level)
root  → installs SSL certificate (system level)

Run these now as root:

bash

# Give mks ownership of /var/www
chown mks:mks /var/www

# Also give mks a backups folder
mkdir -p /home/mks/backups
chown mks:mks /home/mks/backups

# Switch to mks
su - mks
whoami

you are already mks user so you can create your own home directory folders without sudo:

bash

mkdir -p ~/backups
ls -la ~/

~/ means your home directory — when logged as mks it is /home/mks/. You own everything inside it so no sudo needed.

Step 11 — Set Up SSH Key for GitHub on VPS

Why SSH key on VPS: When mks clones from GitHub or pushes updates, the server needs to authenticate with GitHub. SSH key on the server works exactly like your PC — no password ever needed.

We generate one key for KidsScienceMagazine account since quizapp belongs to that account.

Generate SSH key

bash

ssh-keygen -t ed25519 -C "KidsScienceMagazine@gmail.com" -f ~/.ssh/id_github_kids

Press Enter for no passphrase both times.

Verify key created

bash

ls -la ~/.ssh/

Show public key — you will add this to GitHub

bash

cat ~/.ssh/id_github_kids.pub

Create SSH config file

bash

cat > ~/.ssh/config << 'EOF'
# GitHub KidsScienceMagazine
Host github-kids
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_github_kids
EOF

Set correct permissions on SSH files

bash

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_github_kids
chmod 644 ~/.ssh/id_github_kids.pub
chmod 644 ~/.ssh/config

Why permissions matter: SSH is very strict about file permissions. If private key is readable by others, SSH refuses to use it as a security measure.

PermissionMeaning
700 on folderOnly owner can read/write/enter
600 on private keyOnly owner can read/write — no one else
644 on public keyOwner can write, everyone can read

Key generated ✅ but SSH config file is missing. Let me check:

bash

ls -la ~/.ssh/
cat ~/.ssh/config 2>/dev/null || echo "config file missing"

If missing, create it:

bash

cat > ~/.ssh/config << 'EOF'
# GitHub KidsScienceMagazine
Host github-kids
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_github_kids
EOF

chmod 644 ~/.ssh/config
ls -la ~/.ssh/

Then show the public key to add to GitHub:

bash

cat ~/.ssh/id_github_kids.pub

All files correct ✅

FileOwnerPermissionsStatus
.ssh/ foldermks:mks700 (drwx——)
configmks:mks644 (rw-r–r–)
id_github_kidsmks:mks600 (rw——-)
id_github_kids.pubmks:mks644 (rw-r–r–)

All owned by mks and permissions are exactly correct.


Now Add Public Key to GitHub

Get the public key:

bash

cat ~/.ssh/id_github_kids.pub

Copy the entire output. Then:

  1. Log into GitHub as KidsScienceMagazine
  2. Click profile photo → Settings
  3. Left sidebar → SSH and GPG keys
  4. Click New SSH key
  5. Title: VPS Ubuntu Server
  6. Key type: Authentication Key
  7. Paste the key
  8. Click Add SSH key

Then test connection from VPS:

bash

ssh -T github-kids

Expected response:

Hi KidsScienceMagazine! You've successfully authenticated, but GitHub does not provide shell access.

mks@srv1111860:~$

May 18

GitHub SSH authentication working perfectly ✅


Step 12 — Clone quizapp Code from GitHub

What we are doing: Cloning the project into /var/www/ksmquiz as user mks. Since we use SSH URL (git@github-kids) instead of HTTPS, no password or token needed.

Why /var/www/ksmquiz and not /var/www/kidssciencemagazine-quiz: We renamed the project to ksmquiz — cleaner, shorter, matches our database name convention.

bash

git clone git@github-kids:KidsScienceMagazine/kidssciencemagazine-quiz.git /var/www/ksmquiz

Then verify:

bash

ls -la /var/www/ksmquiz
ls /var/www/ksmquiz/backend
ls /var/www/ksmquiz/frontend

Code cloned perfectly ✅ All files owned by mks


Step 13 — Restore .env File

Why .env is critical: The .env file contains all your secrets — database password, JWT secrets, SMTP credentials. It was never pushed to GitHub (correctly). Without it the app cannot connect to database, send emails, or authenticate users.

We need to upload it from your PC to the server.

On Windows PowerShell (open a new PowerShell window, keep VPS terminal open):

powershell

scp "C:\Users\User\Desktop\KSM QUIZ\KSM_QUIZ_BACKUP\quizapp_backend.env.txt" mks@31.220.58.108:/var/www/ksmquiz/backend/.env

What this does: Uploads your saved .env file directly to the correct location on the server, renaming it from .env.txt to .env in one step.

Then on VPS verify it arrived and check the contents look correct:

bash

ls -la /var/www/ksmquiz/backend/.env
cat /var/www/ksmquiz/backend/.env | grep -E "^DB_|^PORT|^NODE_ENV" | sed 's/=.*/=***/'

What we are checking:

  • File exists with correct permissions
  • DB_NAME, DB_USER, PORT, NODE_ENV lines are present
  • We mask values with *** for security

.env file restored perfectly ✅

But I notice one issue — the .env file has permissions rw-rw-r-- (664) which means any user on the server can read it. This file contains your database password, JWT secrets, SMTP credentials. Fix permissions immediately:

bash

chmod 600 /var/www/ksmquiz/backend/.env
ls -la /var/www/ksmquiz/backend/.env

Why 600:

  • Owner (mks) can read and write
  • Group — cannot read
  • Others — cannot read
  • Only mks and root can see the contents

Now verify DB_NAME and DB_USER match what we created in Step 9:

bash

cat /var/www/ksmquiz/backend/.env | grep -E "^DB_NAME|^DB_USER"

It must show:

DB_NAME=ksmquiz_db
DB_USER=ksmquiz_db_user

If it still shows old values (quizapp, old username) — we need to edit the file.

The .env file on your PC was updated but the old version got uploaded. The file on PC still has old values. Let’s fix directly on server:

bash

nano /var/www/ksmquiz/backend/.env

Find and change these two lines:

DB_NAME=quizapp        → change to: DB_NAME=ksmquiz_db
DB_USER=quizapp_user   → change to: DB_USER=ksmquiz_db_user

Save: Ctrl+O → Enter → Ctrl+X

Verify:

bash

cat /var/www/ksmquiz/backend/.env | grep -E "^DB_NAME|^DB_USER"

Regarding changing project name everywhere in code files — let’s check what needs updating:

bash

# Find all files mentioning old db name or project name
grep -r "quizapp" /var/www/ksmquiz/backend/ --include="*.js" --include="*.json" -l
grep -r "quizapp" /var/www/ksmquiz/frontend/ --include="*.html" -l

.env fixed ✅ Only package.json and package-lock.json mention quizapp — these are just the package name, not database connections. Let’s check:

bash

grep "quizapp" /var/www/ksmquiz/backend/package.json

This is likely just:

json

"name": "quizapp-backend"

This is internal npm package name only — it does not affect how the app runs, database connects, or users see anything. You can update it or leave it — it makes zero functional difference.

If you want to update it for cleanliness:

bash

sed -i 's/"name": "quizapp-backend"/"name": "ksmquiz-backend"/' /var/www/ksmquiz/backend/package.json

package-lock.json updates automatically when you run npm install — no manual change needed.

Complete Record — Fresh Ubuntu 24.04 Setup

What was done and why:

1. System Update

bash

apt update && apt upgrade -y

Downloads latest security patches and software updates. Always do this first on a fresh server. Like Windows Update but for Linux.

2. Install Core Software

bash

apt install -y curl git nginx mariadb-server ufw fail2ban
PackagePurpose
curlDownload files and scripts from internet
gitVersion control — clone projects from GitHub
nginxWeb server — serves websites, acts as reverse proxy
mariadb-serverDatabase server — stores all your app data
ufwUncomplicated Firewall — controls which ports are open
fail2banSecurity tool — automatically bans IPs that try to brute force SSH

3. Install Node.js 20

bash

curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs

Node.js does not come in Ubuntu’s default packages at version 20. We added NodeSource repository (official Node.js source) first, then installed. This gives us the latest stable version instead of an outdated one.

4. Install PM2

bash

npm install -g pm2

PM2 is a Process Manager for Node.js. It keeps your Node.js apps running permanently — auto-restarts if they crash, starts them on server reboot. The -g flag installs it globally so any user can use it.

5. Create User mks

bash

adduser mks
usermod -aG sudo mks

Created a non-root user for daily use. Added to sudo group so mks can run admin commands when needed with sudo. Best practice — never run your apps as root.

6. Reboot

bash

reboot

Required to load the newly installed kernel (6.8.0-117). Like restarting Windows after updates.

7. Configure UFW Firewall

bash

ufw allow 22/tcp   # SSH — so we can connect remotely
ufw allow 80/tcp   # HTTP — websites
ufw allow 443/tcp  # HTTPS — secure websites
ufw enable

UFW blocks all incoming connections by default except what you explicitly allow. We only opened 3 ports:

  • 22 — SSH (remote terminal access)
  • 80 — HTTP (websites, redirects to HTTPS)
  • 443 — HTTPS (secure websites with SSL)

Everything else is blocked — databases, Node.js apps, etc. only accessible internally.

8. Secure MariaDB

bash

mysql_secure_installation

Runs through 4 security steps:

  • Removed anonymous users — no one can log in without a username
  • Disabled remote root login — root can only connect from localhost
  • Removed test database — default test DB anyone could access
  • Reloaded privilege tables — applied all changes immediately

Current Server State

ServiceStatusPortAccessible From
Nginx✅ Running80, 443Public
MariaDB✅ Running3306localhost only
UFW Firewall✅ ActiveBlocking all except 22,80,443
fail2ban✅ RunningMonitoring SSH
PM2✅ InstalledReady to use
Node.js✅ v20.20.2Ready to use
User mks✅ Createdsudo access

Step 9 — Create Database and User in MariaDB

What and Why: Never connect your app to the database as root. Create a dedicated database and a dedicated user with access only to that database. This way if your app is ever compromised, the attacker can only access that one database — not the entire MariaDB system.

Commands run as root:

bash

sudo mysql

Then inside MariaDB:

sql

CREATE DATABASE ksmquiz_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'ksmquiz_db_user'@'localhost' IDENTIFIED BY 'YOUR_PASSWORD';
GRANT ALL PRIVILEGES ON ksmquiz_db.* TO 'ksmquiz_db_user'@'localhost';
FLUSH PRIVILEGES;
SHOW DATABASES;
SELECT user, host FROM mysql.user WHERE user='ksmquiz_db_user';
EXIT;

What each command does:

CommandPurpose
CREATE DATABASE ksmquiz_dbCreates empty database with full UTF8 support including emojis
CREATE USER ... @'localhost'Creates user that can only connect from same server
GRANT ALL PRIVILEGES ON ksmquiz_db.*Gives user full access to only this database
FLUSH PRIVILEGESReloads permission table so changes take effect immediately

Result: Database ksmquiz_db and user ksmquiz_db_user created, localhost only ✅


Step 10 — Restore Database from Backup

What and Why: Upload the compressed backup from PC to VPS then restore it into the new database. We use gunzip piped directly into mysql — no temporary files, saves disk space.

Upload from Windows PowerShell:

powershell

scp "C:\Users\User\Desktop\KSM QUIZ\KSM_QUIZ_BACKUP\quizapp_full_20260516_190913.sql.gz" root@31.220.58.108:~/

Restore on VPS:

bash

gunzip < ~/quizapp_full_20260516_190913.sql.gz | sudo mysql ksmquiz_db

Verify restore:

bash

sudo mysql ksmquiz_db -e "SHOW TABLES;"
sudo mysql ksmquiz_db -e "SELECT COUNT(*) FROM users;"

What the pipe command does:

gunzip decompresses file → | → feeds SQL directly into mysql ksmquiz_db

Result: 10 tables restored, 4 users present ✅


Step 11 — Switch to mks User and Set Up Project Ownership

What and Why: Project files should be owned by mks not root. PM2 runs as mks. Node.js app runs as mks. If root owns the files, the app cannot write logs or temp files — causes permission errors.

Standard ownership model:

WhoOwns What
rootSystem software — Nginx, MariaDB, UFW
mksProject files — /var/www/ksmquiz
www-dataNginx worker process
ksmquiz_db_userDatabase ksmquiz_db only

Commands:

bash

# As root — give mks ownership of /var/www
chown mks:mks /var/www

# Switch to mks
su - mks
whoami

# Create backups folder (as mks — owns home dir)
mkdir -p ~/backups

Result: /var/www owned by mks, switched to mks user ✅


Step 12 — Set Up SSH Key for GitHub on VPS

What and Why: The server needs to authenticate with GitHub to clone and pull code. SSH key on server works same as your PC — no password ever needed. One key per GitHub account.

Commands as mks:

bash

# Generate SSH key
ssh-keygen -t ed25519 -C "KidsScienceMagazine@gmail.com" -f ~/.ssh/id_github_kids

# Create SSH config
cat > ~/.ssh/config << 'EOF'
# GitHub KidsScienceMagazine
Host github-kids
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_github_kids
EOF

# Set correct permissions
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_github_kids
chmod 644 ~/.ssh/id_github_kids.pub
chmod 644 ~/.ssh/config

Why permissions are critical:

PermissionFileReason
700.ssh folderOnly mks can enter
600private keyOnly mks can read — SSH refuses to use if others can read
644public keyEveryone can read — it is meant to be shared
644configEveryone can read — no secrets inside

Added public key to GitHub:

  • Logged into GitHub as KidsScienceMagazine
  • Settings → SSH and GPG keys → New SSH key
  • Title: VPS Ubuntu Server
  • Key type: Authentication Key
  • Pasted contents of id_github_kids.pub

Tested connection:

bash

ssh -T github-kids
# Response: Hi KidsScienceMagazine! You've successfully authenticated

Result: SSH authentication working ✅


Step 13 — Clone Code from GitHub

What and Why: Clone project directly from GitHub into /var/www/ksmquiz. Using SSH URL (git@github-kids) instead of HTTPS — no token or password needed. Files cloned as mks so ownership is correct from the start.

Command:

bash

git clone git@github-kids:KidsScienceMagazine/kidssciencemagazine-quiz.git /var/www/ksmquiz

Why this path: /var/www/ is the standard Linux directory for web projects. Nginx looks here by convention. All projects in one place = easy management.

Result: Code cloned, all files owned by mks ✅


Step 14 — Restore .env File

What and Why: .env contains all secrets — database password, JWT secrets, SMTP credentials. It was never pushed to GitHub (correctly — it is in .gitignore). Must be uploaded manually from your PC backup.

Upload from Windows PowerShell:

powershell

scp "C:\Users\User\Desktop\KSM QUIZ\KSM_QUIZ_BACKUP\quizapp_backend.env.txt" mks@31.220.58.108:/var/www/ksmquiz/backend/.env

Fix permissions immediately:

bash

chmod 600 /var/www/ksmquiz/backend/.env

Why 600 on .env: Contains database passwords and JWT secrets. Only app owner (mks) should read it. Group and others get zero access.

Updated DB_NAME and DB_USER to new names:

bash

nano /var/www/ksmquiz/backend/.env
# Changed DB_NAME=quizapp → DB_NAME=ksmquiz_db
# Changed DB_USER=quizapp_user → DB_USER=ksmquiz_db_user

Result: .env in place with correct values, permissions 600 ✅


Current Server State

ComponentStatusLocation
Ubuntu 24.04 LTS✅ Running
Nginx✅ Running/etc/nginx/
MariaDB✅ Running
UFW✅ ActivePorts 22, 80, 443 only
fail2ban✅ RunningMonitoring SSH
Node.js v20✅ Installed/usr/bin/node
PM2✅ InstalledGlobal
Database ksmquiz_db✅ Restored10 tables, 4 users
Code✅ Cloned/var/www/ksmquiz
.env✅ Restored/var/www/ksmquiz/backend/.env

What Comes Next

Step 15 — npm install (install Node.js dependencies)
Step 16 — Test app starts correctly
Step 17 — Start with PM2 permanently
Step 18 — Configure Nginx for quiz.kidssciencemagazine.com
Step 19 — SSL certificate with Certbot
Step 20 — Final testing

Step 15 — Install Node.js Dependencies

What and Why: node_modules folder was never pushed to GitHub (correctly — it is in .gitignore, too large, platform specific). npm install reads package.json and downloads all required packages fresh. This must be done on every new server.

bash

cd /var/www/ksmquiz/backend
npm install

This will take 30-60 seconds.

Step 15 — Install Node.js Dependencies

What and Why: node_modules folder is never pushed to GitHub — it is in .gitignore because it is too large and platform-specific. npm install reads package.json and downloads all required packages fresh on the new server.

bash

cd /var/www/ksmquiz/backend
npm install

Understanding the output:

  • added 132 packages — all dependencies downloaded ✅
  • 5 vulnerabilities (3 moderate, 2 high) — known security issues in some packages. Run npm audit fix later to fix them. Not urgent for restore.
  • 27 packages looking for funding — just informational notices, ignore

Verify node_modules created:

bash

ls node_modules | head -20

Step 16 — Test App Starts Correctly

What and Why: Before starting permanently with PM2, test the app starts without errors. This catches database connection issues, missing .env values, or code errors immediately.

bash

node -e "require('./server.js')" 2>&1 | head -5

What this does: Loads server.js and shows first 5 lines of output. Expected output:

🚀 Quiz API running on port 3001
   Environment: production
   Frontend: https://quiz.kidssciencemagazine.com

If you see database errors here — check .env DB credentials match what was created in Step 9.

Press Ctrl+C to stop the test after confirming it works.


Step 17 — Start App with PM2

What and Why: PM2 is a Process Manager. It runs your Node.js app permanently in the background — auto-restarts if it crashes, keeps running after you close SSH, starts automatically on server reboot.

bash

cd /var/www/ksmquiz/backend
pm2 start server.js --name ksmquiz

Understanding the command:

PartMeaning
pm2 startStart a new process
server.jsThe entry point file of your app
--name ksmquizGive it a readable name instead of just a number

Check it is running:

bash

pm2 status

Look for status: online and ↺ 0 (zero restarts — means no crashes).

Save PM2 process list:

bash

pm2 save

What pm2 save does: Saves the current list of running processes to /home/mks/.pm2/dump.pm2. When server reboots, PM2 reads this file and restarts all saved processes automatically.

Set PM2 to start on server reboot:

bash

pm2 startup

PM2 will print a command like:

sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u mks --hp /home/mks

Copy and run that exact command. This creates a systemd service that starts PM2 as mks user on every boot.

Useful PM2 commands for future:

bash

pm2 status              # see all running apps
pm2 logs ksmquiz        # see live logs
pm2 logs ksmquiz --lines 50  # see last 50 log lines
pm2 restart ksmquiz     # restart the app
pm2 stop ksmquiz        # stop the app
pm2 delete ksmquiz      # remove from PM2 completely
pm2 monit               # live dashboard showing CPU and memory

Step 18 — Verify App Port

What and Why: Before configuring Nginx, confirm which port the app is listening on. Nginx needs to know where to forward requests.

bash

grep -i port /var/www/ksmquiz/backend/.env
grep -i 'listen\|PORT' /var/www/ksmquiz/backend/server.js | head -5

Result: App listens on port 3001 on 127.0.0.1 (localhost only) ✅

This is correct — app is NOT directly accessible from internet. Only Nginx can reach it internally.


Step 19 — Configure Nginx

What and Why: Nginx acts as a reverse proxy — it receives requests from the internet on port 80/443 and forwards them to your Node.js app on port 3001. It also serves static frontend files directly without going through Node.js.

Create Nginx config file:

bash

sudo nano /etc/nginx/sites-available/ksmquiz

Paste this configuration:

nginx

server {
    listen 80;
    server_name quiz.kidssciencemagazine.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name quiz.kidssciencemagazine.com;

    # Frontend static files
    root /var/www/ksmquiz/frontend;
    index index.html;

    # API requests → Node.js backend
    location /api/ {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }

    # Frontend → serve static HTML
    location / {
        try_files $uri $uri/ /index.html;
    }
}

What each section does:

SectionPurpose
First server blockRedirects all HTTP to HTTPS
rootWhere frontend HTML files are
location /api/Forwards API requests to Node.js on port 3001
proxy_set_headerPasses real client IP and protocol to Node.js
location /Serves frontend static files directly

Enable the site:

bash

sudo ln -s /etc/nginx/sites-available/ksmquiz /etc/nginx/sites-enabled/

What ln -s does: Creates a symbolic link (shortcut) from sites-available to sites-enabled. Nginx only reads sites-enabled. This way you can disable a site by just removing the link without deleting the config.

Test and reload:

bash

sudo nginx -t
sudo systemctl reload nginx

nginx -t — tests config for syntax errors without restarting. Always run this before reload. If it says syntax ok and test successful — safe to reload.


Step 20 — SSL Certificate with Certbot

What and Why: HTTPS encrypts all data between user’s browser and your server. Without SSL, passwords and data are sent in plain text. Certbot gets a free SSL certificate from Let’s Encrypt and configures Nginx automatically.

Install Certbot:

bash

sudo apt install certbot python3-certbot-nginx -y

Get SSL certificate:

bash

sudo certbot --nginx -d quiz.kidssciencemagazine.com

What happens:

  1. Certbot contacts Let’s Encrypt servers
  2. Proves you own the domain by creating a temporary file Nginx serves
  3. Downloads the certificate
  4. Automatically updates your Nginx config with SSL settings

Test auto-renewal:

bash

sudo certbot renew --dry-run

What --dry-run means: Simulates the renewal without actually doing it. If it says simulated renewal succeeded — the auto-renewal will work correctly when certificate expires. Certbot auto-renews every 90 days via a systemd timer installed automatically.

Certificate location:

/etc/letsencrypt/live/quiz.kidssciencemagazine.com/fullchain.pem
/etc/letsencrypt/live/quiz.kidssciencemagazine.com/privkey.pem

Step 21 — Final Testing

Test website loads:

bash

curl -s -o /dev/null -w "%{http_code}" https://quiz.kidssciencemagazine.com

Expected: 200

Test API responds:

bash

curl https://quiz.kidssciencemagazine.com/api/

Expected: JSON response from your app

Test HTTP redirects to HTTPS:

bash

curl -s -o /dev/null -w "%{http_code}" http://quiz.kidssciencemagazine.com

Expected: 301 (redirect to HTTPS)


Complete Server State After Full Restore

ComponentStatusDetails
Ubuntu 24.04 LTSKernel 6.8.0-117
Nginx 1.24✅ RunningPorts 80, 443
MariaDB 10.11✅ RunningPort 3306 localhost only
Node.js v20/usr/bin/node
PM2 v7ksmquiz online, 0 restarts
UFW✅ ActivePorts 22, 80, 443 only
fail2ban✅ RunningSSH protection
Database ksmquiz_db10 tables, 4 users restored
SSL CertificateExpires 2026-08-18, auto-renews
App URLhttps://quiz.kidssciencemagazine.com

One Remaining Thing — PM2 Startup Command

From the output you pasted, PM2 printed this but it was not confirmed as run:

bash

sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u mks --hp /home/mks

Run this now to ensure ksmquiz restarts automatically after server reboot. Then test it:

bash

sudo reboot

Reconnect after 30 seconds and verify:

bash

pm2 status

Should show ksmquiz online without you doing anything.

Backup & Restore — Confirmed Complete ✅

ItemBacked UpRestoredStatus
CodeGitHub (KidsScienceMagazine)/var/www/ksmquiz
DatabasePC → KSM_QUIZ_BACKUP folderksmquiz_db (10 tables, 4 users)
.env filePC → KSM_QUIZ_BACKUP folder/var/www/ksmquiz/backend/.env
SSL CertificateAuto-renewsquiz.kidssciencemagazine.com
PM2 runningpm2 save doneksmquiz online 3 days, 0 restarts

1. Future Management Commands

Daily Operations

bash

# Check app status
pm2 status

# See live logs
pm2 logs ksmquiz

# See last 100 lines of logs
pm2 logs ksmquiz --lines 100

# Restart app (after code changes)
pm2 restart ksmquiz

# Check all services running
sudo systemctl is-active nginx mariadb

# Check disk space
df -h

# Check memory usage
free -h

After Code Changes (Pulling Updates from GitHub)

bash

cd /var/www/ksmquiz
git pull
cd backend
npm install          # only if package.json changed
pm2 restart ksmquiz
pm2 logs ksmquiz     # verify no errors after restart

Nginx Management

bash

sudo nginx -t                    # test config before applying
sudo systemctl reload nginx      # apply changes without downtime
sudo systemctl restart nginx     # full restart if reload fails
sudo nano /etc/nginx/sites-available/ksmquiz  # edit config

Database Management

bash

# Login to database
sudo mysql ksmquiz_db

# Quick query without entering interactive mode
sudo mysql ksmquiz_db -e "SHOW TABLES;"
sudo mysql ksmquiz_db -e "SELECT COUNT(*) FROM users;"

# Manual backup
mysqldump --single-transaction --routines --triggers ksmquiz_db | gzip > ~/backups/ksmquiz_$(date +%Y%m%d_%H%M%S).sql.gz

SSL Certificate

bash

# Check expiry date
sudo certbot certificates

# Test auto-renewal
sudo certbot renew --dry-run

# Force renew if needed
sudo certbot renew --force-renewal

Admin Password Change

Your admin users are stored in admin_users table with bcrypt hashed passwords. The JWT secrets in .env sign authentication tokens.

Change Admin Password in Database

Step 1 — Generate new bcrypt hash:

bash

cd /var/www/ksmquiz/backend
node -e "const b=require('bcryptjs'); console.log(b.hashSync('YOUR_NEW_PASSWORD', 10));"

Copy the hash output.

Step 2 — Update in database:

bash

sudo mysql ksmquiz_db

sql

UPDATE admin_users 
SET password_hash='PASTE_NEW_HASH_HERE' 
WHERE username='admin';

-- Verify it updated
SELECT username, email, last_login FROM admin_users;
EXIT;

Change JWT Secrets in .env

When to change: If you suspect tokens are compromised. Note — changing JWT_SECRET logs out ALL users immediately since their existing tokens become invalid.

Step 1 — Generate new secrets:

bash

node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
# Run twice — one for JWT_SECRET, one for JWT_ADMIN_SECRET

Step 2 — Update .env:

bash

nano /var/www/ksmquiz/backend/.env
# Update JWT_SECRET and JWT_ADMIN_SECRET lines

Step 3 — Restart app to load new values:

bash

pm2 restart ksmquiz
pm2 logs ksmquiz  # verify no errors

Step 4 — Update your PC backup:

powershell

scp mks@31.220.58.108:/var/www/ksmquiz/backend/.env "C:\Users\User\Desktop\KSM QUIZ\KSM_QUIZ_BACKUP\quizapp_backend_updated.env.txt"

2. Full Project Description Prompt for New Chat

Copy this exactly:


I have a Node.js quiz web application called ksmquiz running in production on a Ubuntu 24.04 LTS VPS (Hostinger KVM2, IP: 31.220.58.108). Here is the complete current state:

Live URL: https://quiz.kidssciencemagazine.com

Server Stack:

  • Ubuntu 24.04 LTS
  • Nginx 1.24 — reverse proxy and static file server
  • MariaDB 10.11 — database
  • Node.js v20 — runtime
  • PM2 v7 — process manager
  • UFW firewall — ports 22, 80, 443 open only
  • fail2ban — SSH protection
  • SSL via Certbot (Let’s Encrypt, auto-renews)

Project Structure:

/var/www/ksmquiz/
├── backend/          ← Node.js/Express API
│   ├── server.js     ← entry point, listens on 127.0.0.1:3001
│   ├── db.js         ← MariaDB connection
│   ├── routes/       ← API routes
│   ├── middleware/   ← auth middleware
│   ├── scheduler.js  ← cron jobs
│   └── .env          ← secrets (not in GitHub)
└── frontend/
    ├── index.html    ← main quiz interface
    └── admin.html    ← admin panel

Database:

  • MariaDB, database name: ksmquiz_db
  • User: ksmquiz_db_user (localhost only)
  • 10 tables: admin_users, attempt_answers, categories, otp_codes, questions, quiz_attempts, quiz_questions, quizzes, subjects, users
  • Authentication: bcrypt passwords, JWT tokens
  • Admin users stored in admin_users table with password_hash column

.env key variables:

  • PORT=3001
  • NODE_ENV=production
  • DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
  • JWT_SECRET, JWT_ADMIN_SECRET, JWT_EXPIRES_IN
  • SMTP settings (Brevo)
  • FRONTEND_URL, ADMIN_URL
  • OTP and rate limiting settings

Nginx config: /etc/nginx/sites-available/ksmquiz

  • HTTP redirects to HTTPS
  • /api/ routes proxied to Node.js on port 3001
  • / serves frontend static files from /var/www/ksmquiz/frontend

GitHub repo: github.com/KidsScienceMagazine/kidssciencemagazine-quiz (private) SSH key configured on server for KidsScienceMagazine account.

User: mks (sudo access), all project files owned by mks PM2: app named ksmquiz, running 3+ days, 0 restarts

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

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