🌐 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/JakartaVerify:
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 woidenEdit /etc/hosts
nano /etc/hostsReplace 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_woidenReload Shell
Reload the current shell so the new hostname takes effect without a full reboot:
exec bash🔒 Warp Setup
Steps
- Enable TUN (reboot required)
- Hax — https://hax.co.id/vps-control
- Woiden — https://woiden.id/vps-control
- Reboot VPS
- Setup DNS64
echo -e "nameserver 2606:4700:4700::64\nnameserver 2001:67c:2b0::4\nnameserver 2001:4860:4860::64\n" > /etc/resolv.conf - Check TUN state — output should be:
cat: /dev/net/tun: File descriptor in bad statecat /dev/net/tun - Run installer script
wget -N https://gitlab.com/fscarmen/warp/-/raw/main/menu.sh && bash menu.sh 1 - 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 verboseExample 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.confAdd to config:
set -g mouse onReload 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 -yStep 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/nullStep 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.listStep 4 — Pin nginx.org Packages (higher priority)
echo -e "Package: *\nPin: origin nginx.org\nPin-Priority: 900\n" | tee /etc/apt/preferences.d/99nginxStep 5 — Install
apt update
apt install nginx -yStep 6 — Configure nginx.conf
nano /etc/nginx/nginx.confInside 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-appserver {
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-appserver {
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 nginxPM2 — 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-common2. 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-tidyEdit PHP configuration:
nano /etc/php/8.1/fpm/php.iniChange the following values:
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
memory_limit = 256M
date.timezone = Asia/Jakartasystemctl restart php8.1-fpm
systemctl enable php8.1-fpm3. MariaDB
apt install -y mariadb-server mariadb-client
systemctl start mariadb
systemctl enable mariadb
mysql_secure_installation4. Nginx
apt install -y nginx
systemctl start nginx
systemctl enable nginx5. 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/desa6. Nginx Configuration
nano /etc/nginx/sites-available/opensidserver {
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/letsencrypt2. 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 nginx3. 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/html4. 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-name5. 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-app6. 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;
\q7. Drop MariaDB Database
mysql -u root -pDROP 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.19. 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/mysql10. 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.list11. Reset UFW
# Remove all rules & reset to defaults
ufw reset
# Or just disable
ufw disable12. 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.isoe3b0c44298fc1c149afbf4c8996fb924 *filename.isoAll 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.sha256Verify all entries:
sha256sum -c file.sha256./photo.jpg: OK
./video.mp4: OK
./archive.tar.gz: OKPer-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"
doneSkip: notes.txt.sha256 already exists
Created: photo.jpg.sha256
Created: video.mp4.sha256Verify a Single Checksum File
sha256sum -c photo.jpg.sha256photo.jpg: OKQuick Inline Hash (no file written)
sha256sum -b filename.iso | awk '{ print $1 }'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855💡 The
-bflag 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 autocleanDisk 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 :80File 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-pagerNetwork
ip addr show
curl -4 ifconfig.me
curl -6 ifconfig.me
curl -s https://cloudflare.com/cdn-cgi/traceCompression
# 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/