Nginx + PHP-FPM untuk Laravel: Server Block yang Benar

 


Di artikel pertama seri ini, server Anda sudah dibereskan: user deploy, login kunci SSH, firewall menyala, dan swap terpasang. Sekarang giliran bagian yang paling sering bikin frustrasi saat pertama deploy Laravel: membuka domain, lalu yang muncul malah 404, 502, atau — lebih menyebalkan — daftar berkas yang bisa diunduh. Hampir selalu, penyebabnya bukan kode Laravel Anda. Penyebabnya adalah server block Nginx yang mengarah ke tempat yang salah, atau PHP-FPM yang tidak terhubung dengan benar.

Artikel ini memasang Nginx dan PHP-FPM dengan cara yang selaras dengan dokumentasi deployment Laravel: root mengarah ke direktori public, semua permintaan yang bukan berkas statis diteruskan ke index.php, dan berkas tersembunyi seperti .env tidak bisa diakses dari luar. Di ujungnya, HTTPS dari Let's Encrypt ikut menyala. Artikel ini mengasumsikan fondasi artikel pertama sudah beres dan domain Anda sudah mengarah ke IP server — tanpa DNS yang mengarah benar, Certbot tidak bisa memverifikasi kepemilikan domain di langkah terakhir.

Pilih versi PHP dulu, sebelum menginstal apa pun

Dokumentasi deployment Laravel mensyaratkan PHP minimal 8.2 dan ekstensi Ctype, cURL, DOM, Fileinfo, Filter, Hash, Mbstring, OpenSSL, PCRE, PDO, Session, Tokenizer, dan XML. Di Ubuntu 24.04 LTS, repositori bawaan menyediakan PHP 8.3 — melewati lantai itu tanpa perlu repositori pihak ketiga. Itu alasan artikel ini memakai php8.3 di semua nama paket dan jalur. Kalau Anda memakai Ubuntu versi lain, ganti angka 8.3 dengan versi PHP yang tersedia di sana, tapi jangan turun di bawah 8.2: Composer akan menolak menginstal Laravel yang mensyaratkan versi lebih baru, dan galatnya sering baru muncul di tengah proses deploy.

Sebelum instalasi, perbarui paket dulu — sama seperti artikel pertama. Selain menjaga daftar paket tetap segar, langkah ini memastikan Anda mendapat paket PHP 8.3 yang sudah memuat perbaikan keamanan USN-8948-1 (Oktober 2026) untuk kerentanan PHP2Shell, bukan versi yang masih rentan:

apt update && apt upgrade -y

Langkah 1: Instal Nginx, PHP-FPM, dan ekstensi Laravel

Satu perintah memasang semuanya. Paket php8.3-fpm adalah proses PHP yang akan menerima permintaan dari Nginx; php8.3-cli adalah PHP baris perintah untuk Artisan dan Composer nanti; sisanya adalah ekstensi yang dibutuhkan Laravel plus beberapa yang hampir selalu dipakai aplikasi nyata (ZIP untuk manipulasi arsip, BCMath untuk perhitungan presisi, Intl untuk pemformatan lokal, dan driver database):

apt install -y nginx php8.3-fpm php8.3-cli php8.3-common php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip php8.3-bcmath php8.3-intl php8.3-mysql php8.3-sqlite3

Paket php8.3-mysql dan php8.3-sqlite3 boleh dihapus salah satunya kalau Anda sudah yakin hanya memakai satu jenis database — tapi memasang keduanya di awal lebih murah daripada mengulang deploy karena driver yang hilang. Verifikasi hasil instalasi:

php -v
php -m | grep -E 'mbstring|curl|xml|zip|bcmath|intl'
systemctl status php8.3-fpm --no-pager

Hasil yang diharapkan: php -v menampilkan PHP 8.3.x, daftar ekstensi memuat nama-nama di atas, dan status FPM active (running). Kalau FPM tidak jalan, baca dulu journalctl -u php8.3-fpm --no-pager -n 50 sebelum mengubah konfigurasi apa pun — galat di awal biasanya soal socket atau permission, bukan soal aplikasi.

Periksa juga di mana socket FPM mendengarkan, karena jalur ini harus sama persis dengan yang Anda tulis di server block nanti:

ls /run/php/

Hasil yang diharapkan: ada berkas bernama php8.3-fpm.sock. Di Ubuntu, jalur kanonisnya /run/php/php8.3-fpm.sock (dokumentasi Laravel mencontohkan /var/run/php/php8.2-fpm.sock — /var/run adalah symlink ke /run, jadi keduanya menunjuk tempat yang sama, tapi menulis jalur /run/php/ secara eksplisit membuat Anda tidak perlu menebak).

Langkah 2: Atur FPM pool untuk satu aplikasi

Konfigurasi pool FPM tinggal di /etc/php/8.3/fpm/pool.d/www.conf. Untuk satu aplikasi di satu server, pool bawaan www sudah cukup — yang perlu Anda selaraskan adalah tiga hal: siapa yang menjalankan proses, socket apa yang dipakai, dan berapa banyak worker yang diizinkan. Buka berkasnya:

nano /etc/php/8.3/fpm/pool.d/www.conf

Pastikan baris-baris berikut bernilai seperti ini (sebagian sudah menjadi bawaan Ubuntu, periksa saja jangan sampai berbeda):

user = www-data
group = www-data
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6

Penjelasan singkat dari tiap pilihan: pm = dynamic membuat FPM menambah dan mengurangi worker sesuai beban, jadi server tidak menganggur dengan 20 worker saat sepi dan tidak kehabisan worker saat ramai. Angka pm.max_children = 20 adalah titik awal yang waras untuk VPS 4 GB — tiap worker PHP Laravel biasanya memakan 30–60 MB, jadi 20 worker berarti sekitar 0,6–1,2 GB pada beban puncak, masih menyisakan ruang untuk Nginx, database, dan queue worker. Kalau nanti worker sering mentok, naikkan angka ini sambil memantau RAM; kalau RAM yang mentok duluan, turunkan angkanya.

Satu keputusan yang sengaja tidak diubah di sini: proses FPM tetap berjalan sebagai www-data, bukan sebagai deploy. Alasannya menyangkut permission direktori aplikasi yang akan dibahas di artikel deploy berikutnya: berkas aplikasi dimiliki deploy:www-data, dan hanya direktori storage serta bootstrap/cache yang diberi izin tulis untuk grup. Dengan begitu, proses web bisa menulis ke tempat yang memang perlu ditulis, tapi tidak bisa mengubah kode aplikasi.

Setelah mengedit, uji dan muat ulang FPM:

php-fpm8.3 -t
systemctl reload php8.3-fpm

Hasil yang diharapkan: uji konfigurasi menampilkan pesan sukses dan reload selesai tanpa galat. Perintah php-fpm8.3 -t di sini memakai biner khusus versi — jangan tertukar dengan php -t yang tidak ada.

Langkah 3: Tulis server block yang root-nya ke public

Ini inti artikelnya, dan ini juga tempat kesalahan paling mahal bersembunyi. Dokumentasi Laravel menegaskan: web server harus mengarah ke direktori public aplikasi, dan index.php di dalamnya adalah front controller untuk semua permintaan HTTP yang masuk. Jangan pernah mengarahkan root ke folder proyek, dan jangan pernah memindahkan index.php keluar dari public — keduanya membuka akses publik ke berkas sensitif seperti .env.

Buat berkas server block di /etc/nginx/sites-available/laravel.conf. Ganti aplikasi.example.com dengan domain Anda dan /var/www/aplikasi dengan jalur proyek Anda:

nano /etc/nginx/sites-available/laravel.conf

server {
    listen 80;
    listen [::]:80;
    server_name aplikasi.example.com www.aplikasi.example.com;
    root /var/www/aplikasi/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;
    charset utf-8;

    client_max_body_size 32M;

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

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Mari bedah baris-baris yang paling menentukan. root /var/www/aplikasi/public adalah keputusan keamanannya: yang bisa diakses publik hanyalah isi public, bukan seluruh proyek. try_files $uri $uri/ /index.php?$query_string adalah keputusan routing-nya: Nginx melayani berkas statis langsung (CSS, gambar, JS hasil build), dan segala hal lain diteruskan ke index.php beserta query string-nya — persis seperti yang diharapkan router Laravel. Tanpa baris ini, semua rute selain halaman depan akan 404.

Di blok PHP, try_files $uri =404 adalah pengaman penting: berkas PHP yang tidak ada tidak boleh diteruskan ke FPM, karena itu membuka celah eksekusi berkas sembarangan. fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name memberi tahu FPM berkas mana yang harus dieksekusi, dengan jalur yang sudah di-resolve (ini penting kalau jalur proyek Anda nanti memakai symlink, seperti strategi deploy berbasis release di artikel kelima seri ini). Terakhir, blok location ~ /\.(?!well-known).* menolak semua permintaan ke berkas tersembunyi — .env, .git, .htaccess — kecuali /.well-known/, yang harus tetap bisa diakses karena Certbot memakainya untuk verifikasi domain di langkah terakhir.

Nilai client_max_body_size 32M mengatur batas ukuran upload di sisi Nginx. Angka ini harus selaras dengan pengaturan PHP di /etc/php/8.3/fpm/php.ini — buka berkas itu dan samakan:

upload_max_filesize = 32M
post_max_size = 32M

Kalau keduanya tidak selaras, yang berlaku adalah nilai yang lebih kecil, dan pengguna akan mendapat galat 413 (di Nginx) atau upload yang diam-diam gagal (di PHP) tanpa pesan yang jelas. Setelah mengubah php.ini, muat ulang FPM lagi: systemctl reload php8.3-fpm.

Aktifkan situs dan uji konfigurasinya:

ln -s /etc/nginx/sites-available/laravel.conf /etc/nginx/sites-enabled/
rm /etc/nginx/sites-enabled/default
nginx -t
systemctl reload nginx

Menghapus default itu penting: kalau tidak, Nginx masih menjawab domain apa pun dengan halaman selamat datang bawaan, dan Anda akan kebingungan kenapa domain Anda menampilkan halaman yang salah padahal server block sudah benar. Hasil yang diharapkan dari nginx -t: syntax is ok dan test is successful.

Di titik ini, aplikasi Laravel Anda mungkin belum ada di /var/www/aplikasi — dan itu tidak apa-apa. Untuk membuktikan Nginx dan FPM sudah terhubung, buat dulu berkas sementara /var/www/aplikasi/public/index.php berisi <?php phpinfo(); dan buka domain Anda di browser. Kalau halaman info PHP muncul, jalurnya benar. Hapus berkas itu segera setelah pengujian — halaman phpinfo() membocorkan detail versi dan konfigurasi server ke siapa pun yang membukanya.

Langkah 4: Nyalakan HTTPS dengan Certbot

Setelah situs menjawab lewat HTTP dan DNS domain sudah mengarah ke server, pasang Certbot beserta plugin Nginx-nya:

apt install -y certbot python3-certbot-nginx
certbot --version

Lalu minta sertifikatnya:

certbot --nginx -d aplikasi.example.com -d www.aplikasi.example.com

Certbot akan menanyakan alamat email (untuk pemberitahuan perpanjangan) dan apakah HTTP harus dialihkan ke HTTPS — pilih pengalihan (redirect) supaya semua trafik lama ikut terenkripsi. Setelah selesai, Certbot sudah mengubah server block Anda sendiri: menambahkan blok listen 443 ssl beserta jalur sertifikat di /etc/letsencrypt/live/aplikasi.example.com/. Jangan mengedit blok hasil Certbot itu secara manual kecuali Anda tahu persis yang diubah; kalau server block perlu diubah lagi, jalankan certbot --nginx ulang atau edit dengan hati-hati lalu nginx -t.

Buka port HTTPS di firewall — selama ini UFW hanya membuka SSH, dan Nginx yang terinstal belakangan belum pernah diizinkan:

ufw allow 'Nginx Full'
ufw status verbose

Hasil yang diharapkan: aturan Nginx Full tercantum sebagai ALLOW, dan https://aplikasi.example.com terbuka dengan gembok valid. Terakhir, uji perpanjangan otomatisnya — sertifikat Let's Encrypt hanya berlaku 90 hari, dan Ubuntu menjalankan pemeriksaan perpanjangan lewat systemd timer, jadi Anda tidak perlu membuat cron sendiri:

certbot renew --dry-run
systemctl is-active certbot.timer

Hasil yang diharapkan: simulasi perpanjangan selesai tanpa galat dan timer dilaporkan active. Kalau simulasi gagal, jangan menunggu 90 hari untuk sadar — periksa systemctl status certbot.timer dan log di /var/log/letsencrypt/ sekarang juga.


Kalau muncul galat, baca tabel ini dulu

GejalaKemungkinan penyebabPerintah cek
502 Bad GatewayFPM tidak jalan, atau socket di server block tidak cocok dengan socket FPMsystemctl status php8.3-fpm, ls /run/php/, tail -n 50 /var/log/nginx/error.log
404 di semua rute kecuali halaman depantry_files tidak meneruskan ke index.php, atau root bukan publicPeriksa root dan blok location / di /etc/nginx/sites-available/laravel.conf
Berkas PHP terunduh, bukan dieksekusiBlok location ~ \.php$ hilang atau FPM tidak terhubungnginx -T | grep -A 8 'location ~'
413 Request Entity Too LargeUpload melebihi client_max_body_size atau upload_max_filesizeSamakan keduanya di server block dan /etc/php/8.3/fpm/php.ini, lalu reload Nginx dan FPM
403 ForbiddenPermission direktori tidak mengizinkan www-data membaca publicnamei -l /var/www/aplikasi/public/index.php, pastikan grup dan izin baca beres
Halaman kosong / 500 setelah deployAPP_DEBUG masih true di produksi, atau storage tidak bisa ditulistail -n 100 /var/www/aplikasi/storage/logs/laravel.log, periksa izin storage dan bootstrap/cache
Certbot gagal verifikasiDNS belum mengarah ke server, atau port 80 tertutup firewalldig +short aplikasi.example.com, ufw status, curl -I http://aplikasi.example.com

Aturan umum saat mendiagnosis: log Nginx di /var/log/nginx/error.log menjawab pertanyaan "apakah permintaan sampai ke FPM dan kenapa ditolak", sementara log Laravel di storage/logs/laravel.log menjawab "apakah aplikasi sudah jalan tapi gagal di dalam". Mengecek keduanya sebelum mengubah konfigurasi menghemat satu putaran tebak-tebakan.

Tabel ringkasan

KomponenKeputusanBerkas kunci
PHP8.3 dari repositori Ubuntu 24.04 (memenuhi syarat minimal Laravel 8.2)php8.3-fpm, php8.3-cli, ekstensi mbstring/xml/curl/zip/bcmath/intl/mysql/sqlite3
FPM poolwww, user www-data, socket /run/php/php8.3-fpm.sock, pm = dynamic, maks 20 worker/etc/php/8.3/fpm/pool.d/www.conf
Server blockRoot ke public, try_files ke index.php, blok berkas tersembunyi/etc/nginx/sites-available/laravel.conf
Batas upload32M di Nginx dan PHP, selarasServer block + /etc/php/8.3/fpm/php.ini
HTTPSCertbot plugin Nginx, redirect HTTP ke HTTPS, perpanjangan via timer/etc/letsencrypt/live/aplikasi.example.com/, certbot.timer
FirewallTambah Nginx Full setelah HTTPS menyalaufw allow 'Nginx Full'

Checklist akhir

  • php -v menampilkan PHP 8.3 dan ekstensi Laravel terpasang.
  • systemctl status php8.3-fpm aktif, socket /run/php/php8.3-fpm.sock ada.
  • nginx -t lolos, situs default sudah dihapus dari sites-enabled.
  • Domain menjawab lewat HTTP, dan halaman phpinfo() sementara sudah dihapus setelah pengujian.
  • https:// terbuka dengan sertifikat valid, HTTP dialihkan ke HTTPS.
  • certbot renew --dry-run lolos dan certbot.timer aktif.
  • ufw status menampilkan Nginx Full di samping OpenSSH.
  • Berkas .env tidak bisa dibuka dari browser (uji: curl -I https://aplikasi.example.com/.env harus 403 atau 404, bukan 200).

Artikel berikutnya masuk ke proses deploy itu sendiri: mengambil kode lewat Git, memasang dependensi dengan Composer, menjalankan migrasi dengan aman, dan menautkan storage — semua di atas server block yang baru saja Anda susun ini.

Galat pertama apa yang Anda temui saat pertama kali memasang Nginx untuk Laravel — 502, 404 di semua rute, atau yang lain? Ceritakan di komentar beserta penyebab yang akhirnya Anda temukan, supaya pembaca lain tidak mengulang jalan yang sama.

Baca juga: fondasi server dari artikel sebelumnya di {{URL-ID-29}}, gambaran cara satu VPS melayani banyak layanan lewat Satu VPS Banyak Layanan: Nginx Reverse Proxy, dan pengantar Docker Compose untuk self-hosting di Docker Compose untuk Self-Hosting dari Nol.

Komentar