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:
| Component | Version | Status |
|---|---|---|
| Ubuntu 24.04 LTS | noble | ✅ |
| Nginx | 1.24 | ✅ |
| MariaDB | 10.11.14 | ✅ |
| Node.js | v20.20.2 | ✅ |
| npm | 10.8.2 | ✅ |
| PM2 | 7.0.1 | ✅ |
| fail2ban | 1.0.2 | ✅ |
| ufw | 0.36.2 | ✅ |
| user mks | created | ✅ |
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;"
| Part | Meaning |
|---|---|
sudo | Run as root/administrator |
mysql | Open MariaDB client |
-e "..." | Execute this SQL query directly without opening interactive shell |
SELECT user, host, plugin | Show 3 columns: username, where they can connect from, how they authenticate |
FROM mysql.user | From 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:
| Task | Who Does It | Why |
|---|---|---|
| Install MariaDB | root | System level |
| Create databases | root | Admin level |
| Create DB users | root | Admin level |
| App connects to DB | ksmquiz_db_user | App level — limited access |
| Run Node.js app | mks | User 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.
| What | Status |
|---|---|
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:
- Upload it from PC to VPS
- Restore it into the new
ksmquiz_dbdatabase
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
| Part | Meaning |
|---|---|
gunzip < | Decompress the file and send its contents forward |
| | Pipe — send output of left command into right command |
sudo mysql ksmquiz_db | Feed 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! 🎉
| What | Status |
|---|---|
| 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
mksvia PM2 mkscannot 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:
| Who | Owns What | Why |
|---|---|---|
root | System software — Nginx, MariaDB, UFW | Admin level |
mks | Project files — /var/www/ksmquiz | App level |
www-data | Nginx worker process | Web server level |
ksmquiz_db_user | Database ksmquiz_db only | DB 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:
| Option | Method | Best For |
|---|---|---|
| HTTPS with token | Store token in git credentials | Simple, works everywhere |
| SSH keys | Same as your PC setup | More 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.
| Permission | Meaning |
|---|---|
700 on folder | Only owner can read/write/enter |
600 on private key | Only owner can read/write — no one else |
644 on public key | Owner 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 ✅
| File | Owner | Permissions | Status |
|---|---|---|---|
.ssh/ folder | mks:mks | 700 (drwx——) | ✅ |
config | mks:mks | 644 (rw-r–r–) | ✅ |
id_github_kids | mks:mks | 600 (rw——-) | ✅ |
id_github_kids.pub | mks:mks | 644 (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:
- Log into GitHub as KidsScienceMagazine
- Click profile photo → Settings
- Left sidebar → SSH and GPG keys
- Click New SSH key
- Title:
VPS Ubuntu Server - Key type:
Authentication Key - Paste the key
- 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
mksandrootcan 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
| Package | Purpose |
|---|---|
curl | Download files and scripts from internet |
git | Version control — clone projects from GitHub |
nginx | Web server — serves websites, acts as reverse proxy |
mariadb-server | Database server — stores all your app data |
ufw | Uncomplicated Firewall — controls which ports are open |
fail2ban | Security 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
| Service | Status | Port | Accessible From |
|---|---|---|---|
| Nginx | ✅ Running | 80, 443 | Public |
| MariaDB | ✅ Running | 3306 | localhost only |
| UFW Firewall | ✅ Active | — | Blocking all except 22,80,443 |
| fail2ban | ✅ Running | — | Monitoring SSH |
| PM2 | ✅ Installed | — | Ready to use |
| Node.js | ✅ v20.20.2 | — | Ready to use |
| User mks | ✅ Created | — | sudo 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:
| Command | Purpose |
|---|---|
CREATE DATABASE ksmquiz_db | Creates 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 PRIVILEGES | Reloads 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:
| Who | Owns What |
|---|---|
| root | System software — Nginx, MariaDB, UFW |
| mks | Project files — /var/www/ksmquiz |
| www-data | Nginx worker process |
| ksmquiz_db_user | Database 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:
| Permission | File | Reason |
|---|---|---|
| 700 | .ssh folder | Only mks can enter |
| 600 | private key | Only mks can read — SSH refuses to use if others can read |
| 644 | public key | Everyone can read — it is meant to be shared |
| 644 | config | Everyone 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
| Component | Status | Location |
|---|---|---|
| Ubuntu 24.04 LTS | ✅ Running | — |
| Nginx | ✅ Running | /etc/nginx/ |
| MariaDB | ✅ Running | — |
| UFW | ✅ Active | Ports 22, 80, 443 only |
| fail2ban | ✅ Running | Monitoring SSH |
| Node.js v20 | ✅ Installed | /usr/bin/node |
| PM2 | ✅ Installed | Global |
| Database ksmquiz_db | ✅ Restored | 10 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. Runnpm audit fixlater 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:
| Part | Meaning |
|---|---|
pm2 start | Start a new process |
server.js | The entry point file of your app |
--name ksmquiz | Give 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:
| Section | Purpose |
|---|---|
| First server block | Redirects all HTTP to HTTPS |
root | Where frontend HTML files are |
location /api/ | Forwards API requests to Node.js on port 3001 |
proxy_set_header | Passes 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:
- Certbot contacts Let’s Encrypt servers
- Proves you own the domain by creating a temporary file Nginx serves
- Downloads the certificate
- 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
| Component | Status | Details |
|---|---|---|
| Ubuntu 24.04 LTS | ✅ | Kernel 6.8.0-117 |
| Nginx 1.24 | ✅ Running | Ports 80, 443 |
| MariaDB 10.11 | ✅ Running | Port 3306 localhost only |
| Node.js v20 | ✅ | /usr/bin/node |
| PM2 v7 | ✅ | ksmquiz online, 0 restarts |
| UFW | ✅ Active | Ports 22, 80, 443 only |
| fail2ban | ✅ Running | SSH protection |
| Database ksmquiz_db | ✅ | 10 tables, 4 users restored |
| SSL Certificate | ✅ | Expires 2026-08-18, auto-renews |
| App URL | ✅ | https://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 ✅
| Item | Backed Up | Restored | Status |
|---|---|---|---|
| Code | GitHub (KidsScienceMagazine) | /var/www/ksmquiz | ✅ |
| Database | PC → KSM_QUIZ_BACKUP folder | ksmquiz_db (10 tables, 4 users) | ✅ |
| .env file | PC → KSM_QUIZ_BACKUP folder | /var/www/ksmquiz/backend/.env | ✅ |
| SSL Certificate | Auto-renews | quiz.kidssciencemagazine.com | ✅ |
| PM2 running | pm2 save done | ksmquiz 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_userstable withpassword_hashcolumn
.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