VPS Cheatsheet

VPS Cheatsheet

Quick reference for VPS setup on Debian/Ubuntu — Cloudflare WARP, IPv6, Nginx, Node.js, Python, OpenSID, yt-dlp, SHA256.

Debian 11/12Ubuntu 20–24WARPIPv6NginxNode.jsPythonOpenSIDyt-dlpSHA256

🌐 Locale Setup

locale-gen en_US.UTF-8
dpkg-reconfigure locales
update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8

⏰ Set Timezone

Set the system timezone to Asia/Jakarta (WIB — Western Indonesia Time, UTC+7). Do this early so logs and cron jobs use the correct time.

timedatectl set-timezone Asia/Jakarta

Verify:

timedatectl
               Local time: Wed 2024-01-01 08:00:00 WIB
           Universal time: Wed 2024-01-01 01:00:00 UTC
                 RTC time: Wed 2024-01-01 01:00:00
                Time zone: Asia/Jakarta (WIB, +0700)
System clock synchronized: yes
              NTP service: active

🖥️ Fix Hostname & Sudo

Some VPS providers assign auto-generated hostnames that cause sudo: unable to resolve host warnings on every command. Fix by setting a clean hostname and updating /etc/hosts to match.

Set Hostname

hostnamectl set-hostname woiden

Edit /etc/hosts

nano /etc/hosts

Replace the contents with (substitute woiden and the IPv6 line with your actual values):

127.0.0.1       localhost woiden woiden.localhost.localdomain
::1             localhost ip6-localhost ip6-loopback woiden woiden.localhost.localdomain
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters

# Auto-generated hostname. Please do not remove this comment.
[IPv6] 6150364651_woiden.localhost.localdomain 6150364651_woiden

Reload Shell

Reload the current shell so the new hostname takes effect without a full reboot:

exec bash

🔒 Warp Setup

Steps

  1. Enable TUN (reboot required)
  2. Haxhttps://hax.co.id/vps-control
  3. Woidenhttps://woiden.id/vps-control
  4. Reboot VPS
  5. Setup DNS64
    echo -e "nameserver 2606:4700:4700::64\nnameserver 2001:67c:2b0::4\nnameserver 2001:4860:4860::64\n" > /etc/resolv.conf
  6. Check TUN state — output should be: cat: /dev/net/tun: File descriptor in bad state
    cat /dev/net/tun
  7. Run installer script
    wget -N https://gitlab.com/fscarmen/warp/-/raw/main/menu.sh && bash menu.sh 1
  8. Verify — ensure IP is shown in output
    curl -4 ipinfo.io

🛠️ Troubleshoot Warp (fscarmen error)

# Check status
systemctl status wg-quick@warp

# Bring up/down manually
wg-quick up warp
wg show warp
wg-quick down warp

# Change endpoint
sed -i 's/^Endpoint = .*/Endpoint = [2606:4700:d0::a29f:c001]:2408/' /etc/wireguard/warp.conf

# Reset DNS
echo -e "nameserver 2606:4700:4700::64\nnameserver 2001:67c:2b0::4\nnameserver 2001:4860:4860::64" > /etc/resolv.conf

# Restart & verify
wg-quick up warp
sleep 5
wg show warp
curl -4 ipinfo.io

🛡️ UFW Firewall

⚠️ Make sure to allow SSH before enabling UFW to avoid locking yourself out.

Install & Basic Configuration

apt install ufw -y

# Reset to defaults (deny all in, allow all out)
ufw default deny incoming
ufw default allow outgoing

# Allow SSH (port 22) — REQUIRED before enabling!
ufw allow ssh
# or explicitly:
ufw allow 22/tcp

# Allow HTTP & HTTPS for Nginx
ufw allow 'Nginx Full'
# equivalent to:
# ufw allow 80/tcp
# ufw allow 443/tcp

# Enable UFW
ufw enable

# Check status
ufw status verbose

Example Status Output

Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
Nginx Full                 ALLOW IN    Anywhere
22/tcp (v6)                ALLOW IN    Anywhere (v6)
Nginx Full (v6)            ALLOW IN    Anywhere (v6)

Other UFW Commands

# List rules with numbers
ufw status numbered

# Delete rule by number
ufw delete 2

# Delete rule by definition
ufw delete allow 80/tcp

# Temporarily disable UFW (keeps rules)
ufw disable

# Reset all rules
ufw reset

🖼️ Tmux

apt install tmux

# Edit config
nano ~/.tmux.conf

Add to config:

set -g mouse on

Reload config (inside tmux):

Ctrl+b : source-file ~/.tmux.conf

📦 Install Base Packages

apt install python3-dotenv python3-venv python3-pip postgresql curl nano -y

# Node.js 20.x
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install nodejs -y

🔄 Update Nginx

Install the latest stable Nginx from the official nginx.org repository instead of the default Ubuntu/Debian package, which tends to lag several versions behind.

Step 1 — Install Prerequisites

apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring -y

Step 2 — Add Official Signing Key

curl -sL https://nginx.org/keys/nginx_signing.key | gpg --dearmor | tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null

Step 3 — Add nginx.org Repository

echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" | tee /etc/apt/sources.list.d/nginx.list

Step 4 — Pin nginx.org Packages (higher priority)

echo -e "Package: *\nPin: origin nginx.org\nPin-Priority: 900\n" | tee /etc/apt/preferences.d/99nginx

Step 5 — Install

apt update
apt install nginx -y

Step 6 — Configure nginx.conf

nano /etc/nginx/nginx.conf

Inside the http { } block, add the sites-enabled include line if not already present:

include /etc/nginx/sites-enabled/*;

Change the user directive at the top:

user www-data;   # was: user nginx;

Step 7 — Test & Restart

nginx -t
systemctl restart nginx

🐘 PostgreSQL

Create User & Database

-- Connect to psql
sudo -u postgres psql

-- Create user and database
CREATE USER myproject_user WITH PASSWORD 'your_secret_password';
CREATE DATABASE myproject_db OWNER myproject_user ENCODING 'UTF8';
GRANT ALL PRIVILEGES ON DATABASE myproject_db TO myproject_user;

-- For PostgreSQL v15+, also run:
\c myproject_db
GRANT ALL ON SCHEMA public TO myproject_user;

Drop Database

-- Terminate all active connections
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = 'myproject_db' AND pid <> pg_backend_pid();

-- Drop database & user
DROP DATABASE myproject_db;
DROP USER myproject_user;

Connect to Database

psql -h localhost -U myproject_user -d myproject_db
# or via IPv6:
psql -h ::1 -U myproject_user -d myproject_db

🐬 MariaDB

Create User & Database

CREATE DATABASE opensid CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'opensid_user'@'localhost' IDENTIFIED BY 'StrongPassword_123!';
GRANT ALL PRIVILEGES ON opensid.* TO 'opensid_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Connect to Database

mysql -u opensid_user -p opensid

🔀 Nginx — Reverse Proxy (Next.js / Node / Python)

Used for modern apps running on a specific port (e.g. 3000, 8000, 5000). Nginx acts as a public gateway that forwards requests to the app process behind it.

Architecture

Internet → Nginx (:80/:443) → App Process (localhost:PORT)

▶ Next.js / Node.js

Assumes Next.js runs on port 3000 (via npm run start or PM2).

nano /etc/nginx/sites-available/nextjs-app
server {
    listen [::]:80;       # IPv6
    listen 80;            # IPv4 (remove if not available)
    server_name yourdomain.com www.yourdomain.com;

    # Redirect all HTTP to HTTPS (enable after SSL is set up)
    # return 301 https://$host$request_uri;

    location / {
        proxy_pass         http://127.0.0.1:3000;
        proxy_http_version 1.1;

        # WebSocket support (required for Next.js HMR / Socket.io)
        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;
        proxy_read_timeout 86400;
    }
}

After SSL (HTTPS) — full config:

# Redirect HTTP → HTTPS
server {
    listen [::]:80;
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen [::]:443 ssl;
    listen 443 ssl;
    server_name yourdomain.com www.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    client_max_body_size 50M;

    location / {
        proxy_pass         http://127.0.0.1:3000;
        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;
        proxy_read_timeout 86400;
    }
}

▶ Python (FastAPI / Flask / Gunicorn)

Assumes Python app runs on port 8000.

nano /etc/nginx/sites-available/python-app
server {
    listen [::]:80;
    listen 80;
    server_name api.yourdomain.com;

    location / {
        proxy_pass         http://127.0.0.1:8000;
        proxy_http_version 1.1;
        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_read_timeout 120;
    }
}

💡 For FastAPI with Gunicorn: gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 127.0.0.1:8000

▶ Multiple Apps on Subdomains / Paths

# app1 on subdomain
server {
    listen [::]:80;
    server_name app1.yourdomain.com;
    location / { proxy_pass http://127.0.0.1:3000; ... }
}

# app2 on another subdomain
server {
    listen [::]:80;
    server_name app2.yourdomain.com;
    location / { proxy_pass http://127.0.0.1:4000; ... }
}
# Or multiple apps on different paths in one domain
server {
    listen [::]:80;
    server_name yourdomain.com;

    location /app1/ {
        proxy_pass http://127.0.0.1:3000/;
        ...
    }

    location /api/ {
        proxy_pass http://127.0.0.1:8000/;
        ...
    }
}

Enable Config & Reload

# Enable site
ln -s /etc/nginx/sites-available/nextjs-app /etc/nginx/sites-enabled/

# Test config
nginx -t

# Reload (zero downtime)
systemctl reload nginx

PM2 — Process Manager for Node.js / Next.js

npm install -g pm2

# Start app
pm2 start npm --name "nextjs-app" -- start
# or with custom port:
PORT=3000 pm2 start npm --name "nextjs-app" -- start

# Auto-start on reboot
pm2 startup
pm2 save

# Common commands
pm2 list
pm2 logs nextjs-app
pm2 restart nextjs-app
pm2 stop nextjs-app
pm2 delete nextjs-app

🏘️ Install OpenSID

1. Update & Base Packages

apt update && apt upgrade -y
apt install -y curl wget git unzip software-properties-common

2. PHP 8.1

add-apt-repository ppa:ondrej/php -y
apt update
apt install -y \
  php8.1-fpm php8.1-cli php8.1-mysql \
  php8.1-curl php8.1-gd php8.1-mbstring \
  php8.1-xml php8.1-zip php8.1-bcmath \
  php8.1-intl php8.1-soap php8.1-fileinfo \
  php8.1-json php8.1-tidy

Edit PHP configuration:

nano /etc/php/8.1/fpm/php.ini

Change the following values:

upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
memory_limit = 256M
date.timezone = Asia/Jakarta
systemctl restart php8.1-fpm
systemctl enable php8.1-fpm

3. MariaDB

apt install -y mariadb-server mariadb-client
systemctl start mariadb
systemctl enable mariadb
mysql_secure_installation

4. Nginx

apt install -y nginx
systemctl start nginx
systemctl enable nginx

5. OpenSID Source Code

# Copy source code to /var/www/opensid, then set permissions
chown -R www-data:www-data /var/www/opensid
chmod -R 755 /var/www/opensid
chmod -R 775 /var/www/opensid/storage
chmod -R 775 /var/www/opensid/desa

6. Nginx Configuration

nano /etc/nginx/sites-available/opensid
server {
    listen [::]:80 default_server;  # IPv6
    # listen 80;                    # Uncomment if IPv4 is available

    server_name _;  # Replace with domain/IP if available

    root /var/www/opensid;
    index index.php index.html;

    charset utf-8;
    client_max_body_size 64M;

    access_log /var/log/nginx/opensid_access.log;
    error_log  /var/log/nginx/opensid_error.log;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ .php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Block access to sensitive files
    location ~ /.(env|git|htaccess) {
        deny all;
    }

    location ~* .(sql|log|sh)$ {
        deny all;
    }
}
ln -s /etc/nginx/sites-available/opensid /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx

📥 Import OpenSID SQL

From a phpMyAdmin export on shared hosting:

# Clean DEFINER & syntax errors
sed -i 's/DEFINER=[^*]*\*/\*/g; s/DEFINER=[^ ]* / /g' spectral_opensid.sql
sed -i 's/union allselect/union all select/g' spectral_opensid.sql
sed -i 's/1union all select/1 union all select/g' spectral_opensid.sql

# Drop & recreate database
mysql -u root -p -e "DROP DATABASE opensid; \
  CREATE DATABASE opensid CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; \
  GRANT ALL PRIVILEGES ON opensid.* TO 'opensid_user'@'localhost'; \
  FLUSH PRIVILEGES;"

# Import SQL
mysql -u root -p --init-command="SET FOREIGN_KEY_CHECKS=0;" opensid < spectral_opensid.sql

🔐 Certbot (SSL)

apt install certbot python3-certbot-nginx -y

# Issue certificate
certbot --nginx -d yourdomain.com -d www.yourdomain.com

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

🗑️ Uninstall Web & Cleanup

Safe uninstall order: Certbot → Nginx config → Nginx → App → Database → PM2.

1. Remove SSL Certificate (Certbot)

# List all installed certificates
certbot certificates

# Delete specific certificate
certbot delete --cert-name yourdomain.com

# Remove certbot and all certificate data
apt purge certbot python3-certbot-nginx -y
rm -rf /etc/letsencrypt
rm -rf /var/log/letsencrypt

2. Remove Nginx Site Config

# Disable site
rm /etc/nginx/sites-enabled/site-name

# Remove config file
rm /etc/nginx/sites-available/site-name

# Test & reload
nginx -t && systemctl reload nginx

3. Uninstall Nginx (Complete)

systemctl stop nginx
apt purge nginx nginx-common nginx-full -y
apt autoremove -y

# Remove all remaining config & log files
rm -rf /etc/nginx
rm -rf /var/log/nginx
rm -rf /var/www/html

4. Remove Node.js / Next.js App

# Stop & remove from PM2
pm2 stop app-name
pm2 delete app-name
pm2 save

# Remove project folder
rm -rf /var/www/app-name
# or in home dir:
rm -rf ~/app-name

5. Remove Python App

# Stop service if using systemd
systemctl stop app-service
systemctl disable app-service
rm /etc/systemd/system/app-service.service
systemctl daemon-reload

# Remove project folder & virtual environment
rm -rf /var/www/python-app

6. Drop PostgreSQL Database

-- Connect to psql
sudo -u postgres psql

-- Terminate active connections
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = 'myproject_db' AND pid <> pg_backend_pid();

-- Drop
DROP DATABASE myproject_db;
DROP USER myproject_user;
\q

7. Drop MariaDB Database

mysql -u root -p
DROP DATABASE database_name;
DROP USER 'db_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

8. Remove PHP

# Stop PHP-FPM
systemctl stop php8.1-fpm
systemctl disable php8.1-fpm

# Uninstall all PHP 8.1 packages
apt purge php8.1* -y
apt autoremove -y

# Remove remaining config
rm -rf /etc/php/8.1

9. Remove MariaDB (Complete)

systemctl stop mariadb
apt purge mariadb-server mariadb-client mariadb-common -y
apt autoremove -y

# Remove all database data (CAUTION!)
rm -rf /var/lib/mysql
rm -rf /etc/mysql

10. Remove PM2 & Node.js

# Remove all PM2 processes
pm2 delete all
pm2 kill

# Remove PM2
npm uninstall -g pm2
rm -rf ~/.pm2

# Remove Node.js
apt purge nodejs -y
apt autoremove -y
rm -rf /etc/apt/sources.list.d/nodesource.list

11. Reset UFW

# Remove all rules & reset to defaults
ufw reset

# Or just disable
ufw disable

12. General Cleanup

# Remove unused packages
apt autoremove -y
apt autoclean

# Check running processes
ps aux | grep -E 'node|python|php|nginx'

# Check open ports
ss -tlnp

🎬 yt-dlp — Download YouTube

Install

apt update && apt upgrade -y
apt install python3 ffmpeg -y
pip install yt-dlp --break-system-packages
# or update if already installed:
pip install -U yt-dlp --break-system-packages

📋 Check Available Formats

Before downloading, list available formats and their IDs:

yt-dlp --js-runtimes node --remote-components ejs:github --list-formats "VIDEO_URL"

Example output:

ID      EXT   RESOLUTION  NOTE
140     m4a   audio only  128k
137     mp4   1920x1080   1080p
136     mp4   1280x720    720p
135     mp4   854x480     480p

💡 Use the format ID (e.g. 137+140) to select a specific quality.

🎥 Download Video (Best Quality)

# Best quality auto (mp4 + m4a → merge to mp4)
yt-dlp --js-runtimes node --remote-components ejs:github \
  -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]" \
  --merge-output-format mp4 \
  "URL"

# With specific format ID (e.g. 137+140)
yt-dlp --js-runtimes node --remote-components ejs:github \
  -f "137+140" \
  --merge-output-format mp4 \
  "URL"

🎵 Download Audio (MP3)

# Best quality auto → convert to mp3
yt-dlp --js-runtimes node --remote-components ejs:github \
  -f "bestaudio[ext=m4a]" \
  --extract-audio \
  --audio-format mp3 \
  "URL"

# With specific format ID (e.g. 140)
yt-dlp --js-runtimes node --remote-components ejs:github \
  -f "140" \
  --extract-audio \
  --audio-format mp3 \
  "URL"

🌐 Download 360° Video

Step 1 — Check available formats:

yt-dlp --js-runtimes node --remote-components ejs:github \
  --list-formats "VIDEO_360_URL"

Step 2 — Download & merge:

# Best quality auto
yt-dlp --js-runtimes node --remote-components ejs:github \
  -f "bestvideo+bestaudio[ext=m4a]" \
  --merge-output-format mp4 \
  --embed-metadata \
  "VIDEO_360_URL"

# With specific format ID
yt-dlp --js-runtimes node --remote-components ejs:github \
  -f "VIDEO_ID+AUDIO_ID" \
  --merge-output-format mp4 \
  --embed-metadata \
  "VIDEO_360_URL"

Step 3 — Inject 360° metadata (spatial media):

git clone https://github.com/google/spatial-media.git
cd spatial-media

# Inject metadata so it's recognized as 360° by YouTube/VR players
python3 spatialmedia -i input.mp4 output_360.mp4

⚠️ Without metadata injection, the 360° video will not play in spherical mode on platforms like YouTube or Meta Quest.

💡 yt-dlp Tips

# Download with custom filename
yt-dlp ... -o "%(title)s.%(ext)s" "URL"

# Download entire playlist
yt-dlp ... -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]" \
  --merge-output-format mp4 \
  -o "%(playlist_index)s - %(title)s.%(ext)s" \
  "PLAYLIST_URL"

# Limit download speed (e.g. 2MB/s)
yt-dlp ... --limit-rate 2M "URL"

# Resume interrupted download
yt-dlp ... -c "URL"

#️⃣ Generate SHA256

Commands to generate SHA-256 checksums for verifying file integrity.

Single File

sha256sum -b filename.iso
e3b0c44298fc1c149afbf4c8996fb924  *filename.iso

All Files → One Combined Checksum File

Recursively hash every file in the current directory tree (excluding the output file itself) into a single file.sha256:

find . -type f ! -name 'file.sha256' -print0 \
  | xargs -0 sha256sum -b > file.sha256

Verify all entries:

sha256sum -c file.sha256
./photo.jpg: OK
./video.mp4: OK
./archive.tar.gz: OK

Per-File Checksum (skip existing)

Hash only top-level files, writing each hash to its own <filename>.sha256. Skips files that already have a checksum:

find . -maxdepth 1 -type f ! -name "*.sha256" -print0 \
  | while IFS= read -r -d '' file; do
    filename=$(basename "$file")
    sha_file="${filename}.sha256"
    if [ -f "$sha_file" ]; then
        echo "Skip: $sha_file already exists"
        continue
    fi
    sha256sum -b "$file" > "$sha_file"
    echo "Created: $sha_file"
done
Skip: notes.txt.sha256 already exists
Created: photo.jpg.sha256
Created: video.mp4.sha256

Verify a Single Checksum File

sha256sum -c photo.jpg.sha256
photo.jpg: OK

Quick Inline Hash (no file written)

sha256sum -b filename.iso | awk '{ print $1 }'
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

💡 The -b flag reads in binary mode — recommended for non-text files (images, ISOs, archives) to avoid line-ending differences across environments.

🧹 Utilities

Update & Upgrade

apt update && apt upgrade -y
apt autoremove -y && apt autoclean

Disk Usage

df -h
du -sh /* 2>/dev/null
du -sh /var/log/*

Process Monitoring

htop
ps aux | grep nginx
kill -9 <PID>

Ports & Connections

ss -tlnp
ss -tlnp6
# Check specific port
ss -tlnp | grep :80

File Search

# Find files by name
find / -name "*.conf" 2>/dev/null

# Search inside files
grep -rn "listen 80" /etc/nginx/

Logs

# Follow Nginx error log
tail -f /var/log/nginx/error.log

# Systemd service logs
journalctl -u nginx -f
journalctl -u nginx -n 100 --no-pager

Network

ip addr show
curl -4 ifconfig.me
curl -6 ifconfig.me
curl -s https://cloudflare.com/cdn-cgi/trace

Compression

# Create
tar -czvf archive.tar.gz /path/to/dir
zip -r archive.zip dir/

# Extract
tar -xzvf archive.tar.gz -C /target/
unzip archive.zip -d /target/