~/saidqb cat wiki/stack/server/deploy-react.md
commit by saidqb
Deploy React/Vite (Static Build + Nginx)
Aplikasi React/Vite hasil build itu murni file statis (HTML/CSS/JS) — tidak butuh Node.js jalan di server, cukup web server yang bisa serve file statis. Struktur repo yang cocok: lihat project-app-build-dan-doc.md (folder project/build/ saja yang di-deploy).
Tiap project dapat Linux user sendiri (/home/<appuser>/app), bukan numpuk di satu folder /var/www bersama — kalau server ini dipakai buat banyak project, satu project ke-compromise tidak otomatis bisa baca file project lain.
Contoh command di dokumen ini pakai Ubuntu/Debian (apt). Kalau server-nya distro RHEL-based (Rocky Linux/AlmaLinux/CentOS Stream, pakai dnf), lihat catatan padanan di tiap langkah yang berbeda — hasil build React murni file statis, jadi server tidak butuh Node.js/PHP/Remi sama sekali, cuma Nginx + rsync.
0. Prasyarat sebelum deploy
- Node.js & npm terinstall di mesin yang dipakai untuk build (local/CI — bukan server produksi, server cuma serve file statis). Pakai nvm supaya gampang ganti versi Node per proyek:
- Install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash(lalu buka terminal baru, atausource ~/.bashrc) - Install & pakai Node LTS:
nvm install --lts && nvm use --lts - Cek hasil install:
node -v && npm -v
- Install nvm:
- Nginx sudah terinstall di server:
sudo apt install nginx -y(Rocky Linux/AlmaLinux:sudo dnf install nginx -y) - Akses SSH ke server (user dengan hak
sudo) untuk setup Nginx & folder deploy — disiapkan penyedia server, bukan sesuatu yang di-install - Domain sudah diarahkan (DNS A record) ke IP server — diatur di panel DNS registrar, bukan di server
rsyncterinstall di mesin build & server (biasanya sudah ada default di Linux/macOS; kalau belum:sudo apt install rsync -y, atausudo dnf install rsync -ydi Rocky Linux/AlmaLinux)
1. Buat user khusus untuk app
sudo adduser --disabled-password --gecos "" reactapp--disabled-password — user ini tidak bisa login pakai password (cuma diakses lewat sudo dari user SSH yang sudah masuk). --gecos "" skip pertanyaan nama/nomor telepon dsb yang tidak relevan buat service account. Home directory-nya otomatis jadi /home/reactapp.
ssh user@server 'sudo mkdir -p /home/reactapp/app && sudo chown reactapp:reactapp /home/reactapp/app'2. Build di local/CI
npm install
npm run build # hasilnya di folder dist/ atau build/, tergantung tool3. Salin/deploy hasil build ke server
rsync dijalankan lewat user SSH biasa (bukan reactapp — bikin reactapp bisa login SSH sendiri cuma nambah kerumitan setup key yang tidak perlu untuk static site), lalu kepemilikan file disamakan ke reactapp sesudahnya:
rsync -avz --delete dist/ user@server:/home/reactapp/app/
ssh user@server 'sudo chown -R reactapp:reactapp /home/reactapp/app'4. Izin baca untuk Nginx
Home directory Linux defaultnya tertutup dari user lain — Nginx (jalan sebagai www-data) butuh akses baca ke folder ini biar bisa serve file statisnya:
sudo chmod o+rx /home/reactapp
sudo chmod -R o+rX /home/reactapp/app5. Nginx — serve static + SPA fallback
server {
listen 80;
server_name app.example.com;
root /home/reactapp/app;
index index.html;
location / {
try_files $uri $uri/ /index.html; # penting untuk client-side routing (React Router)
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}try_files ... /index.html wajib ada kalau pakai client-side router (React Router, dsb) — tanpa ini, refresh di URL selain / (misal /about) akan 404 karena Nginx mencari file /about yang memang tidak ada.
sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo ufw allow 'Nginx Full' # buka port 80/443 di firewall — tanpa ini situs tidak bisa diakses dari luar meski Nginx sudah jalan benar, kalau ufw aktif (default di banyak VPS)Rocky Linux/AlmaLinux: tidak ada sites-available/sites-enabled bawaan — taruh langsung file konfigurasinya di /etc/nginx/conf.d/app.conf (tanpa symlink). Firewall default-nya firewalld, bukan ufw:
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reloadSELinux aktif default di RHEL-based — kalau Nginx gagal serve file padahal chmod/chown di Langkah 4 sudah benar, biasanya SELinux context yang salah, bukan permission Linux biasa: sudo restorecon -Rv /home/reactapp/app.
6. Urutan tiap kali deploy update
npm run build
rsync -avz --delete dist/ user@server:/home/reactapp/app/
ssh user@server 'sudo chown -R reactapp:reactapp /home/reactapp/app && sudo chmod -R o+rX /home/reactapp/app'Tidak perlu restart service apa pun — file statis langsung kebaca Nginx begitu di-overwrite. Kalau pakai cache-busting hash di nama file build (default Vite), tidak perlu khawatir user dapat file lama ter-cache browser.
Opsional: HTTPS
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d app.example.comRocky Linux/AlmaLinux: paket certbot-nya dari EPEL (sudo dnf install epel-release -y dulu kalau belum ada):
sudo dnf install certbot python3-certbot-nginx -y
sudo certbot --nginx -d app.example.comKonfigurasi SSL-nya tidak perlu ditulis manual — plugin --nginx otomatis suntik langsung ke server block yang sudah dibuat di Langkah 5 (listen 443 ssl, path sertifikat, redirect HTTP→HTTPS), dan pasang auto-renewal sendiri (sertifikat Let's Encrypt berlaku 90 hari, diperpanjang otomatis sebelum expired). Hasilnya kira-kira begini:
server {
listen 80;
server_name app.example.com;
root /home/reactapp/app;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparam.pem; # managed by Certbot
}
server {
if ($host = app.example.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name app.example.com;
return 404; # managed by Certbot
}Kalau nanti edit config ini lagi, jangan hapus baris # managed by Certbot — dan tetap sudo nginx -t && sudo systemctl reload nginx tiap habis edit.
Kalau domain dipasang di belakang Cloudflare (proxy oranye), lihat catatan cloudflare.md — ada penyesuaian mode SSL/TLS & opsi sertifikat yang perlu diperhatikan.
Opsional: batasi hasil build hanya jalan di domain tertentu
Static build (HTML/CSS/JS) berjalan penuh di browser klien — server cuma kirim file mentah, jadi tidak ada cara di sisi server untuk memverifikasi "domain yang sah". Siapa pun bisa rsync/download folder build lalu deploy ulang ke domain lain, dan proteksi apa pun di JS tetap bisa dibaca & dihapus lewat DevTools oleh orang yang niat teknis. Jadi ini bukan proteksi anti-bypass — cuma bikin hasil clone tampil blank secara default untuk pengunjung biasa.
Pendekatannya: index.html cuma sembunyikan tampilan (tanpa expose domain/logika), lalu bundle JS (sudah ter-minify) yang menentukan apakah dikembalikan terlihat.
index.html, taruh di <head> paling atas — dieksekusi sinkron sebelum browser sempat render body:
<script>document.documentElement.style.visibility = 'hidden'</script>Entry point JS (main.jsx/main.js), sebelum render app:
const ALLOWED_HOST = 'app.example.com' // domain produksi yang sah
const isLocalDev =
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') &&
window.location.port === '5173' // sesuaikan port dev server
if (window.location.hostname !== ALLOWED_HOST && !isLocalDev) {
document.body.innerHTML = ''
} else {
document.documentElement.style.visibility = 'visible'
// render seperti biasa
}Kalau proyeknya prerender tiap route jadi file statis (SSG/SSR-lite via renderToString, dst), pastikan index.html yang dipakai sebagai template prerender sudah versi ini — supaya proteksi ikut ter-bake ke semua dist/<route>/index.html, bukan cuma di /.
Set port dev secara eksplisit di config (mis. server.port + strictPort: true di Vite) kalau dipakai untuk exception isLocalDev — port default bisa auto-geser saat bentrok, dan bikin exception di atas jadi salah tanpa disadari.
Trade-off: kalau JS gagal load (network error, JS disabled di browser), halaman stuck hidden selamanya alih-alih tampil normal — cocok untuk SPA yang memang sudah bergantung penuh ke JS, kurang cocok kalau situsnya perlu tetap terbaca tanpa JS.