Deploying My Portfolio Website: From Zero to Production with VPS, Staging, and GitHub Actions
A complete guide to deploying a Next.js portfolio website on a VPS (Tencent Cloud Lighthouse) with staging environment, PM2, Nginx, SSL, and automated CI/CD via GitHub Actions.
Intro
Gw Adit, frontend dev yang stack utama Next.js + Tailwind + shadcn/ui. Tahun lalu gw bikin portfolio pake Next.js App Router, DAW-inspired design (timeline, tracks, clips), sama feature-based folder structure. Tapi selama ini portfolio cuma live di Vercel. Gak ada control penuh ke infrastructure.
Jadi gw mutusin: Deploy ke VPS sendiri. Gw pake Tencent Cloud Lighthouse karena low-cost, performa solid buat workload kecil. Setup staging environment (staging subdomain), otomasi CI/CD via GitHub Actions, plus monitoring (Prometheus + Grafana + Uptime Kuma) sama Telegram notifications buat health checks sama deploy alerts.
Artikel ini dokumen lengkapnya, dari nol (beli VPS) sampai production live, sama staging pipeline yang reliable. Jadi kalo lo juga mau deploy portfolio ke VPS, bisa follow step-by-step ini.
๐ฆ Tech Stack & Prerequisites
Core:
- Frontend: Next.js 15 (App Router), React, Tailwind CSS, shadcn/ui
- Backend/Server: Node.js 22 LTS, PM2 buat process management, Nginx sebagai reverse proxy
- Infrastructure: Tencent Cloud Lighthouse (Ubuntu 24.04 LTS) dengan Cloudflare DNS
- CI/CD: GitHub Actions pake appleboy/ssh-action sama telegram-action
- Monitoring: Prometheus + Node Exporter + Nginx Exporter + Alertmanager (ngirim alert ke Telegram), plus Uptime Kuma buat monitor uptime sama bisa trigger service restart
Domain & SSL:
- Domain pake Cloudflare, contoh:
YOUR_DOMAIN.com - Production:
YOUR_DOMAIN.comโ port 3000 (Next.js) - Staging:
staging.YOUR_DOMAIN.comโ port 3002 (Next.js dev mode) - SSL: Let's Encrypt pake DNS-01 challenge (Cloudflare plugin) karena HTTP-01 gabisa karena Basic Auth di nginx
Notifications:
- Telegram bot (
@YOUR_BOT_USERNAME) ke chat_idYOUR_CHAT_ID - Notif untuk deploy success/failure, health checks, sama rollback alerts
โ๏ธ 1. Tencent Cloud Lighthouse Setup
Why Lighthouse? Lighthouse pilihannya karena harganya murah, cuma ~$2-3/month buat 2 vCPU, 2 GB RAM, plus 40 GB SSD. Performanya solid untuk workload kecil kayak portfolio. Lokasi Singapore, latency ke Jakarta oke. Include bandwidth 500GB per bulan, more than enough.
Step 1: Buy Lighthouse Instance
- Login ke Tencent Cloud Console โ Cloud Virtual Machine (CVM) โ Lighthouse
- Create instance:
- OS: Ubuntu 24.04 LTS
- Region: Singapore
- Instance type: Lighthouse (2 vCPU, 2 GB RAM, 40 GB SSD)
- System disk: 40 GB SSD (included)
- Network: Assign public IP, enable port 22 (SSH), 80/443 (HTTP/HTTPS), plus monitoring ports (9090, 4000, 8384, 9113, 9652)
- Security group: Allow inbound 22 (SSH), 80 (HTTP), 443 (HTTPS), and monitoring ports
- Create โ generate SSH key or password (disarankan SSH key)
Step 2: SSH Setup & Initial Security
bashLoading...Loading syntax highlighting...
Security tweaks:
- Change SSH port (optional, 22 โ custom port misal 2222)
- Disable password auth, pake SSH key only
- Install
ufwataufail2ban(opsional)
๐ง 2. VPS Provisioning: System Packages & Tools
Login sebagai user YOUR_USERNAME, install dependencies:
bashLoading...Loading syntax highlighting...
Buat folder structure untuk apps:
bashLoading...Loading syntax highlighting...
๐ง 3. Next.js App: Build & Run Configuration
Project setup (local/dev):
bashLoading...Loading syntax highlighting...
PM2 config: File ecosystem.config.js di root project:
javascriptLoading...Loading syntax highlighting...
Start both:
bashLoading...Loading syntax highlighting...
๐ 4. Nginx Reverse Proxy + SSL
Why Nginx?
- Port 80/443 โ single entry point (better security + SSL termination)
- Rate limiting, security headers, gzip compression
- Multiple apps via subpath/subdomain (nanti bisa scale)
Nginx config untuk production (/etc/nginx/sites-available/portfolio.conf):
nginxLoading...Loading syntax highlighting...
Enable & test:
bashLoading...Loading syntax highlighting...
๐ชช 5. SSL Certificate with Certbot (DNS-01 Challenge)
Karena production domain pake Cloudflare, dan nginx sudah pake Basic Auth (monitoring subdomains), HTTP-01 challenge gagal. Solusi: pake DNS-01 challenge via Cloudflare API.
Setup Cloudflare API token:
- Cloudflare Dashboard โ My Profile โ API Tokens
- Create Token โ Edit zone DNS โ Zone:
YOUR_DOMAIN.com - Copy token
Issue cert untuk production:
bashLoading...Loading syntax highlighting...
~/.secrets/cloudflare.ini:
dns_cloudflare_email = your-email@example.com
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
Issue cert untuk staging (staging subdomain):
bashLoading...Loading syntax highlighting...
Auto-renew (crontab):
bashLoading...Loading syntax highlighting...
๐ 6. GitHub Actions CI/CD Pipeline
Workflow Production (deploy.yml)
Path: .github/workflows/deploy.yml
yamlLoading...Loading syntax highlighting...
Workflow Staging (deploy-dev.yml)
yamlLoading...Loading syntax highlighting...
GitHub Secrets:
VPS_HOST:YOUR_VPS_IPVPS_USER:YOUR_USERNAMEVPS_SSH_KEY: Private SSH key (RSA 4096) untuk akses VPSTELEGRAM_BOT_TOKEN: Token bot TelegramTELEGRAM_CHAT_ID:YOUR_CHAT_ID
๐ 7. Deploy Script: Production & Staging
Production Deploy Script (/home/YOUR_USERNAME/apps/next-portfolio-blog/deploy.sh)
bashLoading...Loading syntax highlighting...
Staging Deploy Script (/home/YOUR_USERNAME/apps/next-portfolio-blog-dev/deploy.sh)
bashLoading...Loading syntax highlighting...
Key differences prod vs staging:
- Prod:
npm ci --omit=dev --ignore-scripts(faster, no dev deps) - Staging:
npm ci(full dev deps for hot reload, dev mode) - Prod: Next.js start (static build) vs Staging:
npm run dev(dev server) - Port: 3000 (prod) vs 3002 (staging)
- PM2 process name:
portfolio-blogvsportfolio-blog-dev
๐ 8. Monitoring Stack (Prometheus + Grafana + Uptime Kuma)
Architecture:
VPS (YOUR_VPS_IP)
โโโ Prometheus (9090) โ scrapes metrics
โโโ Alertmanager (9093) โ alerts โ Telegram
โโโ Grafana (4000, /grafana/) โ dashboards (Basic Auth)
โโโ Node Exporter (9100) โ system metrics
โโโ Nginx Exporter (9113) โ nginx metrics
โโโ PM2 Exporter (9652) โ app process metrics
โโโ Uptime Kuma (3001) โ uptime monitoring, service restart capabilities
Deploy monitoring? Bisa pake docker-compose atau manual services. Prometheus juga bisa jalan sebagai systemd service atau via PM2.
Uptime Kuma Setup: Tool ini penting buat monitor uptime sama bisa trigger restart service otomatis. Cara setup:
- Install via npm:
npm install -g uptime-kuma(atau pake Docker:docker run -d --restart=always -p 3001:3001 -v /path/to/data:/app/data louislam/uptime-kuma) - Akses:
http://YOUR_VPS_IP:3001 - Tambah monitors:
- HTTP(s) ke
https://YOUR_DOMAIN.com(production) - HTTP(s) ke
https://staging.YOUR_DOMAIN.com(staging) - TCP ke
YOUR_VPS_IP:3000(Next.js prod) - TCP ke
YOUR_VPS_IP:3002(Next.js dev) - Ping ke
YOUR_VPS_IP
- HTTP(s) ke
- Service restart: Uptime Kuma bisa setup webhook โ GitHub Actions atau custom script yang otomatis restart PM2 services kalo downtime terdeteksi.
- Status page: Bisa enable public status page di Uptime Kuma kalo perlu share uptime ke client.
Prometheus config snippet (/home/YOUR_USERNAME/dashboard/monitoring/prometheus.yml):
yamlLoading...Loading syntax highlighting...
Grafana access: http://YOUR_VPS_IP:4000 (Basic Auth: YOUR_GRAFANA_USER (default: admin))
Alertmanager rules โ Telegram jika:
- CPU > 85% for 5m
- Memory < 10% free
- HTTP 5xx from nginx exporter
- PM2 process down
(Rinci setup monitoring ini di dokumentasi terpisah, tapi intinya semua metrics dikumpul dan visualisasi di Grafana, plus uptime tracking via Uptime Kuma.)
๐ 9. Rollback & Recovery Strategy
Git-based rollback (built into deploy script):
- Setiap deploy: simpan commit hash ke
$ROLLBACK_DIR/last_known_good.commit - Kalau build/health check gagal โ
git reset --hard $CURRENT_COMMIT+ rebuild + reload PM2 - Rollback juga terkirim Telegram notification
Manual rollback script (rollback.sh untuk production, rollback-dev.sh untuk staging):
bashLoading...Loading syntax highlighting...
Database/backup? Portfolio ini statis (Markdown content, no DB). Tapi kalo ada data (misal transactions ledger via Zenstack), setup backup routine (cron + remote storage).
๐๏ธ 10. Directory Structure on VPS
/home/YOUR_USERNAME/
โโโ apps/
โ โโโ next-portfolio-blog/ # production (branch: main)
โ โ โโโ .git/
โ โ โโโ .next/ # build output
โ โ โโโ node_modules/
โ โ โโโ ecosystem.config.js # PM2 config
โ โ โโโ deploy.sh # production deploy script
โ โ
โ โโโ next-portfolio-blog-dev/ # staging (branch: dev)
โ โโโ .git/
โ โโโ .next/
โ โโโ node_modules/
โ โโโ ecosystem.config.js
โ โโโ deploy.sh # staging deploy script
โ
โโโ dashboard/
โ โโโ monitoring/ # Prometheus, Grafana, exporters configs
โ
โโโ portfolio-deploy.log # centralized deploy logs
โโโ portfolio-rollback/ # last commit hash (prod)
โโโ portfolio-rollback-dev/ # last commit hash (staging)
๐ 11. Feature-Based Folder Structure (Codebase Convention)
Untuk maintainability, repo pake feature-based structure (bukan page-based):
/src
โโโ features/
โ โโโ landing-page/ # homepage route (/)
โ โ โโโ sections/ # ALL landing page sections grouped
โ โ โ โโโ hero/
โ โ โ โโโ about/
โ โ โ โโโ work/
โ โ โ โโโ contact/
โ โ โ โโโ index.ts # export all sections
โ โ โโโ page.tsx # landing page composable
โ โ โโโ layout.tsx
โ โ
โ โโโ blog/ # /blog route
โ โ โโโ page.tsx
โ โ โโโ components/
โ โ
โ โโโ projects/ # /projects/[slug] route
โ โโโ page.tsx
โ โโโ ProjectDetail/
โ
โโโ components/
โ โโโ ui/ # shadcn/ui components (reusable)
โ โโโ layout/ # Navbar, Footer, etc.
โ โโโ theme/ # ThemeProvider, DarkMode
โ
โโโ lib/ # utilities (only if shared by 2+ features)
โ โโโ utils.ts
โ โโโ constants.ts
โ
โโโ content/ # Markdown content (blog posts, projects)
โโโ public/ # static assets
โโโ styles/ # global CSS
Rule of thumb:
- Each feature = route/page group
sections/only exist di feature yang punya many sections (like landing-page)_shared/untuk utils yang dipakai >2 feature (bukan dilib/)- Data local ke feature, jangan global state kecuali truly shared
โ 12. Checklist & Verification
Before claiming "deploy done", verify:
- VPS processos PM2 running (
pm2 list, both portfolio-blog & portfolio-blog-dev) - Nginx config valid (
sudo nginx -t) - SSL certs active (
sudo certbot certificates) - Domain DNS mengarah ke VPS IP (Cloudflare A record)
- HTTP health check:
curl -I https://YOUR_DOMAIN.comโ 200 OK - Staging health:
curl -I https://staging.YOUR_DOMAIN.comโ 200 OK - PM2 resurrect on boot:
pm2 startup+pm2 save - GitHub Actions triggers successfully (check Actions tab)
- Telegram notifications terkirim (sukses/gagal)
- Monitoring accessible (Grafana: 4000, Prometheus: 9090)
- Rollback script tested manually (fasilitas recovery)
๐ฏ Conclusion
Dengan setup ini, portfolio auto-deploy setiap push ke:
mainโ production (YOUR_DOMAIN.com)devโ staging (staging.YOUR_DOMAIN.com)
Kenapa setup ini worth it?
- Isolation: Staging terpisah dari production. Break things tanpa takut bikin production down.
- Speed: Staging jalan di dev mode (
npm run dev), gak perlu build step, hot reload langsung. - Safety: Auto rollback kalo build atau health check gagal, plus Telegram alerts.
- Observability: Prometheus + Grafana dashboards lengkap, ditambah Uptime Kuma buat tracking uptime.
- Cost-effective: Cuma perlu satu Lighthouse VPS, ~$2-3/bulan.
- Full control: Bisa custom apapun, dari security headers sampe performance tuning.
Next improvements yang bisa ditambahkan:
- Blue-green deployment (dual directories, swap symlink). Zero downtime
- Preview deployments per PR (temporary subdomain
pr-123.staging.YOUR_DOMAIN.com) - Cache optimization (nginx caching, Next.js Image Optimization pake Cloudinary)
- CDN integration (Cloudflare Pages untuk static assets)
๐ Resources & References
- Tencent Cloud CVM Documentation
- Next.js Deployment
- PM2 Documentation
- Certbot DNS-01 with Cloudflare
- appleboy/ssh-action
- Prometheus + Grafana Stack
Written by Adit โ Frontend Developer, Jakarta Selatan.
Portfolio: https://YOUR_DOMAIN.com | GitHub: @YOUR_GITHUB_USERNAME
Related Posts
Why I Rebuilt My Portfolio
My old portfolio was a mess. Here's how I rebuilt it with Next.js 15, Tailwind v4, and a feature-based architecture.
Read moreVPS Monitoring Stack โ Grafana, Prometheus & Blackbox Exporter
How I built a full-stack monitoring system on Ubuntu VPS with Grafana, Prometheus, Blackbox Exporter, and automated uptime tracking with Telegram alerts.
Read moreBuilding a Habit Tracker with Next.js 16, Tailwind v4 & shadcn/ui
A deep dive into building a mobile-first habit tracker with modern Next.js, shadcn/ui components, framer-motion animations, and a localStorage-first architecture for offline-first personal tracking.
Read more