POWER
MENU
Kindle
ยท16 min readยทโ€”

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.

deploymentnextjsvpsdevopsportfoliogithub-actionsstaging

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_id YOUR_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

  1. Login ke Tencent Cloud Console โ†’ Cloud Virtual Machine (CVM) โ†’ Lighthouse
  2. 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
  3. Create โ†’ generate SSH key or password (disarankan SSH key)

Step 2: SSH Setup & Initial Security

bash
Loading...
Loading syntax highlighting...

Security tweaks:

  • Change SSH port (optional, 22 โ†’ custom port misal 2222)
  • Disable password auth, pake SSH key only
  • Install ufw atau fail2ban (opsional)

๐Ÿง 2. VPS Provisioning: System Packages & Tools

Login sebagai user YOUR_USERNAME, install dependencies:

bash
Loading...
Loading syntax highlighting...

Buat folder structure untuk apps:

bash
Loading...
Loading syntax highlighting...

๐Ÿ”ง 3. Next.js App: Build & Run Configuration

Project setup (local/dev):

bash
Loading...
Loading syntax highlighting...

PM2 config: File ecosystem.config.js di root project:

javascript
Loading...
Loading syntax highlighting...

Start both:

bash
Loading...
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):

nginx
Loading...
Loading syntax highlighting...

Enable & test:

bash
Loading...
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:

  1. Cloudflare Dashboard โ†’ My Profile โ†’ API Tokens
  2. Create Token โ†’ Edit zone DNS โ†’ Zone: YOUR_DOMAIN.com
  3. Copy token

Issue cert untuk production:

bash
Loading...
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):

bash
Loading...
Loading syntax highlighting...

Auto-renew (crontab):

bash
Loading...
Loading syntax highlighting...

๐Ÿš€ 6. GitHub Actions CI/CD Pipeline

Workflow Production (deploy.yml)

Path: .github/workflows/deploy.yml

yaml
Loading...
Loading syntax highlighting...

Workflow Staging (deploy-dev.yml)

yaml
Loading...
Loading syntax highlighting...

GitHub Secrets:

  • VPS_HOST: YOUR_VPS_IP
  • VPS_USER: YOUR_USERNAME
  • VPS_SSH_KEY: Private SSH key (RSA 4096) untuk akses VPS
  • TELEGRAM_BOT_TOKEN: Token bot Telegram
  • TELEGRAM_CHAT_ID: YOUR_CHAT_ID

๐Ÿ“œ 7. Deploy Script: Production & Staging

Production Deploy Script (/home/YOUR_USERNAME/apps/next-portfolio-blog/deploy.sh)

bash
Loading...
Loading syntax highlighting...

Staging Deploy Script (/home/YOUR_USERNAME/apps/next-portfolio-blog-dev/deploy.sh)

bash
Loading...
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-blog vs portfolio-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
  • 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):

yaml
Loading...
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):

bash
Loading...
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 di lib/)
  • 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


Written by Adit โ€” Frontend Developer, Jakarta Selatan.
Portfolio: https://YOUR_DOMAIN.com | GitHub: @YOUR_GITHUB_USERNAME

Related Posts