Menginstal Django sampai bisa dijalankan lewat python manage.py runserver di laptop sendiri adalah satu hal, membawanya sampai bisa diakses publik lewat domain dengan HTTPS aktif adalah hal lain yang jauh lebih panjang. Jarak di antara keduanya diisi oleh serangkaian package sistem, konfigurasi database, dan beberapa lapisan server yang saling meneruskan request satu sama lain. Panduan ini mengajak kita menyusun jalur itu langkah demi langkah, mulai dari server Ubuntu 26.04 LTS yang masih polos, instalasi Django 6.1 di dalam virtual environment, sampai aplikasi bisa diakses lewat domain sendiri dengan koneksi HTTPS yang aman.
Stack yang kita pakai: Ubuntu 26.04 LTS sebagai sistem operasi, Python 3.14 dengan python3-venv, Django 6.1 sebagai framework, MariaDB 11.8 sebagai database, Nginx sebagai web server, Gunicorn sebagai WSGI server, dan sertifikat SSL gratis dari Let's Encrypt lewat Certbot. Sebelum mulai, pastikan dua hal sudah siap: server Ubuntu 26.04 dengan user non-root yang punya akses sudo, dan nama domain yang sudah diarahkan ke alamat IP server tersebut.
1. Instalasi Dependensi dan Environment
Langkah pertama sebagai Sysadmin adalah memastikan semua package dasar tersedia sebelum menyentuh kode Django sama sekali. Python 3.14 sendiri sudah tersedia bawaan di Ubuntu 26.04 LTS, jadi yang perlu kita siapkan adalah tooling pendukungnya, database server untuk menyimpan data, dan web server untuk menghadapi traffic dari luar.
1.1 Update Repositori Sistem
Jalankan perintah berikut agar daftar package di server sinkron dengan repository Ubuntu terbaru:
sudo apt update && sudo apt upgrade -yPerintah ini penting dijalankan lebih dulu karena package python3-venv atau MariaDB 11.8 yang akan kita install butuh definisi repository yang sudah ter-update. Kalau langkah ini dilewati, instalasi berikutnya bisa gagal atau menarik versi lama.
1.2 Instalasi Paket Sistem
Ubuntu 26.04 LTS sudah membawa Python 3.14 preinstalled sebagai interpreter python3 bawaan sistem, jadi kita tidak perlu lagi menginstalnya secara manual seperti di versi Ubuntu yang lebih lama. Yang masih perlu dipasang cuma dua kelompok package tambahan: module venv beserta header Python untuk kompilasi, dan server database serta web server-nya. Jalankan langkah berikut secara berurutan.
- Install module
venvdan header Python untuk kompilasi package (keduanya tidak ikut ter-install otomatis meskipython3sudah ada):sudo apt install python3-venv python3-dev -y - Install database server dan web server:
sudo apt install mariadb-server nginx -y - Install build tools untuk kompilasi driver MariaDB (dipakai nanti saat
pip install mysqlclient):sudo apt install build-essential libmariadb-dev pkg-config -y
Paket libmariadb-dev dan pkg-config sering terlewat di tutorial yang lebih tua karena versi Python lama masih menyertakan header kompatibel secara longgar. Di versi Python dan MariaDB terbaru, driver mysqlclient butuh header file ini untuk berhasil dikompilasi dari source.
1.3 Verifikasi Instalasi
Cek versi tiap komponen untuk memastikan Python bawaan sistem memang 3.14, module venv sudah bisa dipakai, dan instalasi MariaDB serta Nginx berhasil:
python3 --version
python3 -m venv --help
mariadb --version
nginx -vKalau keempat perintah di atas mengembalikan nomor versi atau help text tanpa error command not found, environment dasar sudah siap dan kita bisa lanjut ke tahap database. python3 -m venv --help khusus menguji apakah module venv dari python3-venv sudah terpasang, karena Python bawaan Ubuntu kadang tidak menyertakan module ini secara default.
2. Setup Database MariaDB
MariaDB butuh sedikit pengamanan bawaan dan sebuah database khusus sebelum Django bisa terhubung ke sana. Bagian ini menyiapkan keduanya. Sebagai catatan kompatibilitas, dokumentasi resmi Django 6.1 mensyaratkan minimal MariaDB 10.11, jadi MariaDB 11.8 LTS yang kita pakai di sini sudah jauh di atas batas minimum tersebut.
2.1 Keamanan Dasar MariaDB
Jalankan script keamanan bawaan MariaDB untuk mengatur password root, menghapus anonymous user, dan menonaktifkan remote login untuk root:
sudo mariadb-secure-installationPerintah mariadb-secure-installation ini adalah nama resmi sejak MariaDB 10.5, menggantikan mysql_secure_installation yang dipakai versi lebih lama. Nama lama itu masih bisa dipanggil lewat symlink untuk kompatibilitas, tapi di MariaDB 11.8 sebaiknya kita pakai nama resminya langsung. Ikuti prompt yang muncul: atur password root yang kuat, jawab Y untuk menghapus anonymous user, menonaktifkan remote root login, menghapus database test, dan me-reload privilege table. Langkah ini mencegah Attacker memanfaatkan konfigurasi default MariaDB yang longgar untuk brute force atau akses tanpa autentikasi.
Verifikasi: pastikan service MariaDB berjalan normal, lalu cek langsung apakah anonymous user dan database test benar-benar sudah hilang:
sudo systemctl status mariadb
sudo mariadb -e "SELECT User, Host FROM mysql.user WHERE User='';"
sudo mariadb -e "SHOW DATABASES LIKE 'test';"Kalau dua query terakhir tidak mengembalikan baris apa pun, anonymous user dan database test sudah bersih. sudo systemctl status mariadb yang menampilkan active (running) menandakan service siap menerima koneksi dari Django nanti.
2.2 Membuat Database dan User
Masuk ke prompt MariaDB sebagai root lewat mariadb, command-line client resmi MariaDB yang sejak versi 10.2 menggantikan nama mysql (nama lama itu tetap ada sebagai symlink, tapi sudah ditandai deprecated di MariaDB versi terbaru):
sudo mariadb -u root -pSetelah masuk, jalankan empat perintah SQL berikut untuk membuat database, membuat user khusus, dan memberi hak akses:
- Buat database dengan character set
utf8mb4agar mendukung karakter Unicode penuh (termasuk emoji):CREATE DATABASE django_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - Buat user baru dengan password yang kuat, jangan pakai akun root untuk koneksi aplikasi:
CREATE USER 'django_user'@'localhost' IDENTIFIED BY 'password_yang_kuat'; - Berikan hak akses penuh user tersebut hanya ke database
django_db:GRANT ALL PRIVILEGES ON django_db.* TO 'django_user'@'localhost'; - Terapkan perubahan hak akses dan keluar:
FLUSH PRIVILEGES; EXIT;
Kenapa harus user terpisah dari root? Kalau kredensial aplikasi bocor lewat file settings.py yang tidak sengaja ter-commit ke repository publik, Attacker hanya bisa menyentuh database django_db, bukan seluruh instance MariaDB. Prinsip least privilege ini jadi lapisan pertahanan kalau lapisan lain gagal.
Verifikasi: login pakai akun django_user yang baru dibuat untuk memastikan password dan hak aksesnya benar:
mariadb -u django_user -p django_db -e "SELECT DATABASE();"Kalau perintah di atas mengembalikan nama django_db tanpa error akses, kredensial sudah benar. Uji juga batasan haknya dengan mariadb -u django_user -p -e "SHOW DATABASES;", hasilnya seharusnya cuma menampilkan django_db (dan information_schema bawaan), bukan seluruh database di server, karena django_user memang cuma diberi privilege ke django_db di langkah sebelumnya.
3. Project Django dan Virtual Environment
Bagian ini menyiapkan struktur project, mengisolasi dependency Python lewat virtual environment, dan mengonfigurasi Django 6.1 agar terhubung ke MariaDB yang baru dibuat.
3.1 Struktur Direktori Project
Buat folder khusus untuk project di luar direktori home user agar rapi dan mudah diberi hak akses ke group www-data nanti:
sudo mkdir -p /var/www/myproject
sudo chown $USER:$USER /var/www/myproject
cd /var/www/myproject3.2 Inisialisasi Virtual Environment
Virtual environment adalah folder terisolasi yang menyimpan package Python khusus untuk satu project, terpisah dari package sistem. Ini mencegah konflik versi kalau di server yang sama nanti ada project Django lain dengan kebutuhan versi berbeda.
- Buat virtual environment baru bernama
venv:python3 -m venv venv - Aktifkan virtual environment tersebut:
source venv/bin/activate
Prompt terminal akan berubah menampilkan awalan (venv) setelah aktivasi berhasil. Semua perintah pip dan python setelah ini akan berjalan di dalam environment terisolasi tersebut.
Perlu diperhatikan, kita tidak perlu menginstal pip secara terpisah lewat apt sebelum langkah ini. Package python3-venv yang sudah dipasang di langkah 1.2 sudah menyertakan module ensurepip, yang otomatis mem-bootstrap pip ke dalam venv begitu perintah python3.14 -m venv venv dijalankan. Kalau python3-venv di langkah 1.2 terlewat, pembuatan venv di atas akan gagal dengan pesan ensurepip is not available, jadi kalau pesan itu muncul, kembali ke langkah 1.2 dulu.
Verifikasi: pastikan python dan pip yang aktif sekarang benar-benar mengarah ke binary di dalam venv, bukan ke Python sistem:
which python
python --version
pip --versionPath yang muncul dari which python harus diawali /var/www/myproject/venv/bin/ dan versinya Python 3.14.x. Kalau masih menunjuk ke /usr/bin/python3, berarti aktivasi venv belum berhasil dan package yang diinstall di langkah berikutnya akan tercampur ke Python sistem. pip --version sekaligus membuktikan pip sudah ter-bootstrap otomatis lewat ensurepip tadi, path-nya juga harus mengarah ke folder venv yang sama.
3.3 Instalasi Paket Python
Install Django, Gunicorn, dan driver MariaDB lewat pip:
pip install django==6.1 gunicorn mysqlclientmysqlclient adalah driver resmi yang direkomendasikan dokumentasi Django untuk terhubung ke MySQL maupun MariaDB. Driver ini dikompilasi dari C dan butuh libmariadb-dev serta build-essential yang sudah kita install di langkah 1.2, itu sebabnya urutan instalasi package sistem penting dilakukan lebih dulu.
Verifikasi: pastikan ketiga package benar-benar terpasang dan bisa di-import tanpa error. Kegagalan kompilasi mysqlclient biasanya baru ketahuan di titik ini, bukan saat pip install selesai tanpa pesan error yang jelas:
python -c "import django, gunicorn, MySQLdb; print(django.get_version())"Kalau perintah ini mencetak 6.1.x tanpa ModuleNotFoundError, ketiga package sudah siap dipakai. MySQLdb adalah nama modul Python dari package mysqlclient, jadi kalau baris ini gagal dengan pesan ImportError terkait libmariadb, kemungkinan besar libmariadb-dev di langkah 1.2 belum terpasang saat pip install mysqlclient dijalankan, dan perlu di-install ulang setelah dependensi sistemnya lengkap.
Simpan daftar package beserta versi persisnya ke requirements.txt begitu instalasi berhasil:
pip freeze > requirements.txtpip freeze mencatat versi eksak semua package yang terpasang di venv ini, termasuk dependency tidak langsung yang ikut terpasang bersama Django dan mysqlclient. File requirements.txt ini yang sebaiknya ikut ter-commit ke repository (bukan folder venv itu sendiri), supaya kalau server perlu di-rebuild dari nol, atau Developer lain perlu menyiapkan environment yang identik, cukup jalankan pip install -r requirements.txt tanpa harus menebak-nebak versi package mana yang dipakai. Setiap kali menambah atau meng-update package baru lewat pip install, jalankan ulang pip freeze > requirements.txt untuk menjaga file ini tetap sinkron.
Dapatkan Kredit DigitalOcean $25
Klaim Kredit $253.4 Membuat Project Sample
Buat project Django baru di direktori saat ini (tanda titik di akhir perintah penting, artinya "di folder ini juga", bukan membuat sub-folder baru):
django-admin startproject myproject .startproject cuma membuat urls.py dengan satu route bawaan ke /admin/, tanpa view apa pun untuk / (root domain). Selama DEBUG = True, kekosongan ini tersamarkan oleh halaman demo "The install worked successfully!" milik Django, tapi begitu DEBUG dimatikan nanti di langkah 6.4, halaman demo itu otomatis hilang dan / cuma akan mengembalikan 404 Not Found polos. Supaya root domain tetap menampilkan sesuatu yang berarti di production, tambahkan satu view homepage sederhana sekarang. Buka myproject/urls.py dan ganti isinya menjadi:
from django.contrib import admin
from django.http import HttpResponse
from django.urls import path
def homepage(request):
return HttpResponse("<h1>myproject</h1><p>Django 6.1 berjalan normal di server ini.</p>")
urlpatterns = [
path('admin/', admin.site.urls),
path('', homepage, name='homepage'),
]View homepage di atas sengaja dibuat sesederhana mungkin lewat HttpResponse langsung, tanpa file template terpisah, supaya tutorial ini tetap fokus ke proses deployment, bukan ke pembuatan front-end. Yang penting, path '' (root) sekarang punya view yang benar-benar terdaftar, sehingga responsnya tidak lagi bergantung pada nilai DEBUG. Developer bebas mengganti homepage ini nanti dengan view sungguhan yang me-render template, atau bahkan meng-include urls.py dari app lain.
3.5 Konfigurasi settings.py
Buka myproject/settings.py dan sesuaikan tiga bagian berikut agar siap production.
- Tambahkan domain atau IP server ke
ALLOWED_HOSTS:ALLOWED_HOSTS = ['example.com', 'www.example.com', '203.0.113.10'] - Ganti konfigurasi
DATABASESdari SQLite bawaan ke MariaDB, sesuai kredensial yang dibuat di langkah 2.2:DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'django_db', 'USER': 'django_user', 'PASSWORD': 'password_yang_kuat', 'HOST': 'localhost', 'PORT': '3306', } } - Tambahkan konfigurasi direktori file statis dan media di bagian bawah
settings.py:STATIC_URL = 'static/' STATIC_ROOT = BASE_DIR / 'staticfiles' MEDIA_URL = 'media/' MEDIA_ROOT = BASE_DIR / 'media'
ALLOWED_HOSTS wajib diisi karena Django menolak request dengan header Host yang tidak terdaftar di situ, ini mencegah serangan HTTP Host header yang bisa dipakai Attacker untuk cache poisoning atau password reset link palsu. STATIC_ROOT juga wajib diisi karena nanti Nginx yang akan melayani file statis langsung dari folder ini, bukan lewat Django, supaya request lebih cepat dan Gunicorn tidak terbebani.
Folder staticfiles nanti otomatis dibuat oleh perintah collectstatic di langkah 3.6, tapi folder media tidak dibuat otomatis oleh Django, ia baru muncul kalau ada user yang meng-upload file lewat aplikasi. Karena Nginx sudah dikonfigurasi mengarah langsung ke folder ini lewat alias di langkah 5.2, buat foldernya sekarang supaya tidak muncul error "No such file or directory" saat pengecekan izin di langkah 8.5:
mkdir -p /var/www/myproject/mediaContoh di atas menuliskan password database dan SECRET_KEY langsung di settings.py supaya alurnya mudah diikuti, tapi untuk server production sungguhan sebaiknya nilai sensitif ini tidak ikut ter-commit ke repository. Cara yang lebih aman adalah membaca nilainya dari environment variable, misalnya lewat os.environ.get('DB_PASSWORD'), lalu menyimpan nilai aslinya di file terpisah seperti .env yang ditambahkan ke .gitignore. Kalau settings.py pernah ter-push ke repository publik dengan kredensial asli di dalamnya, anggap kredensial itu bocor dan segera ganti password database serta SECRET_KEY-nya.
Verifikasi: sebelum menjalankan migrasi, validasi dulu settings.py lewat system check framework bawaan Django:
python manage.py checkSejak Django 6.1, perintah check ini sudah otomatis menyertakan pengecekan koneksi database ke semua alias di DATABASES (bukan cuma syntax settings.py seperti versi Django lebih lama), jadi kalau ada typo di password atau HOST MariaDB, kesalahan itu akan ketahuan di sini, bukan menunggu error samar saat migrate dijalankan. Keluaran System check identified no issues (0 silenced) menandakan konfigurasi sudah siap lanjut ke langkah berikutnya.
3.6 Inisialisasi Aplikasi
Jalankan tiga perintah berikut secara berurutan untuk menyiapkan skema database, mengumpulkan file statis, dan membuat akun admin:
- Migrasi skema database ke MariaDB:
python manage.py migrate - Kumpulkan semua file statis ke folder
STATIC_ROOT:python manage.py collectstatic - Buat akun superuser untuk masuk ke Django admin:
python manage.py createsuperuser
Verifikasi: pastikan migrate benar-benar membuat tabel di MariaDB dan collectstatic benar-benar mengisi folder staticfiles, dua hal yang nanti langsung dipakai Nginx di langkah 5:
mariadb -u django_user -p django_db -e "SHOW TABLES;"
ls /var/www/myproject/staticfiles | headKalau SHOW TABLES; menampilkan daftar tabel bawaan Django seperti django_migrations dan auth_user, migrasi berhasil. Kalau folder staticfiles kosong, collectstatic kemungkinan gagal menemukan file statis atau STATIC_ROOT di settings.py salah ketik.
3.7 Pengujian Awal
Sebelum masuk ke konfigurasi Gunicorn dan Nginx, uji dulu apakah Django dan koneksi database sudah benar lewat development server bawaan:
python manage.py runserver 0.0.0.0:8000Akses http://alamat-ip-server:8000 dari browser. Kalau view homepage yang dibuat di langkah 3.4 muncul ("myproject" beserta pesan "Django 6.1 berjalan normal di server ini.") tanpa error koneksi database, berarti langkah 1 sampai 3 sudah benar. Matikan development server dengan Ctrl+C setelah pengujian selesai, karena server ini tidak cocok dipakai untuk production.
4. Setup Gunicorn
Django punya development server bawaan, tapi server itu single-threaded dan tidak dirancang menahan beban traffic production. Di sinilah Gunicorn masuk sebagai WSGI server yang menjembatani Nginx dengan kode Python aplikasi.
4.1 Testing Gunicorn Manual
Uji dulu apakah Gunicorn bisa menjalankan aplikasi Django tanpa error:
gunicorn --bind 0.0.0.0:8000 myproject.wsgiFormat myproject.wsgi merujuk ke objek application di dalam file myproject/wsgi.py yang otomatis dibuat Django saat startproject. Kalau perintah ini berhasil dan halaman Django bisa diakses di port 8000, artinya Gunicorn sudah bisa menjalankan aplikasi. Matikan dengan Ctrl+C, lalu lanjut ke konfigurasi permanen lewat systemd.
4.2 Membuat Systemd Socket dan Service
Menjalankan Gunicorn manual lewat terminal tidak praktis untuk production, karena proses akan mati begitu terminal ditutup. Solusinya, kita daftarkan Gunicorn sebagai service systemd yang aktif otomatis saat server boot dan otomatis restart kalau crash.
Nama unit sengaja diberi prefix myproject-, bukan sekadar gunicorn. Kalau nanti di server yang sama Sysadmin men-deploy aplikasi Django lain, misalnya blog, dua unit bernama gunicorn.service akan saling bentrok karena systemd hanya mengenal satu nama unit per file. Dengan prefix nama project, tiap aplikasi punya socket, service, dan file log sendiri, sehingga bisa dikelola (dicek statusnya, di-restart, dimatikan) secara independen satu sama lain.
- Buat direktori khusus untuk menyimpan log Gunicorn milik project ini, lalu serahkan kepemilikannya ke user deploy dan group
www-data:sudo mkdir -p /var/log/gunicorn/myproject sudo chown $USER:www-data /var/log/gunicorn/myproject - Buat file
/etc/systemd/system/myproject-gunicorn.socketyang mendefinisikan lokasi Unix socket:[Unit] Description=gunicorn socket for myproject [Socket] ListenStream=/run/myproject-gunicorn.sock SocketUser=$USER SocketGroup=www-data SocketMode=0660 [Install] WantedBy=sockets.targetGanti
$USERdengan nama user non-root yang sama seperti di filemyproject-gunicorn.service. Untuk socket activation, file socket dibuat langsung oleh systemd, bukan oleh Gunicorn, jadi kepemilikannya diatur lewat[Socket], bukan lewatUser/Groupdi[Service]. TanpaSocketMode, systemd memakai default0666yang berarti socket file bisa dibaca dan ditulis siapa saja di server, celah yang bisa dimanfaatkan user lain di mesin yang sama, termasuk user milik aplikasi lain kalau server dipakai bersama.SocketMode=0660membatasi akses hanya untuk pemilik dan groupwww-data, persis group yang dipakai Nginx untuk menghubungi Gunicorn. - Buat file
/etc/systemd/system/myproject-gunicorn.serviceyang mendefinisikan cara Gunicorn dijalankan:[Unit] Description=gunicorn daemon for myproject Requires=myproject-gunicorn.socket After=network.target [Service] User=$USER Group=www-data WorkingDirectory=/var/www/myproject ExecStart=/var/www/myproject/venv/bin/gunicorn \ --access-logfile /var/log/gunicorn/myproject/access.log \ --error-logfile /var/log/gunicorn/myproject/error.log \ --workers 3 \ --bind unix:/run/myproject-gunicorn.sock \ myproject.wsgi:application Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.targetGanti
$USERdengan nama user non-root yang dipakai Sysadmin, jangan biarkan literal$USERtertulis di file karena systemd tidak membaca environment variable shell.--access-logfiledan--error-logfilemengarahkan log request dan log error Gunicorn ke file eksplisit di direktori yang dibuat langkah pertama, bukan ke stdout yang tercampur ke journal systemd bersama aplikasi lain di server yang sama. Ini memudahkan Sysadmin memisahkan log tiap aplikasi saat men-debug, dan memudahkan rotasi log lewatlogrotateper aplikasi.Restart=on-failuremembuat proses Gunicorn otomatis dinyalakan ulang lima detik kemudian kalau service-nya berhenti karena error, tanpa baris ini systemd hanya membiarkan service mati begitu crash terjadi.
Nama file socket dan service sengaja dibuat sama (myproject-gunicorn) supaya systemd otomatis mengaitkan keduanya lewat mekanisme socket activation: systemd yang membuat dan menjaga socket file tetap ada, lalu baru menyalakan proses Gunicorn begitu ada request pertama yang masuk. Gunicorn dijalankan lewat Unix socket (bukan TCP port) karena komunikasi antar-proses di mesin yang sama lewat socket file sedikit lebih cepat dan tidak membuka port tambahan yang perlu diamankan dari akses luar.
4.3 Start dan Enable Gunicorn
Jalankan tiga perintah berikut untuk mengaktifkan service yang baru dibuat:
- Muat ulang konfigurasi systemd daemon agar file baru terbaca:
sudo systemctl daemon-reload - Mulai dan aktifkan socket Gunicorn agar berjalan otomatis saat boot:
sudo systemctl start myproject-gunicorn.socket sudo systemctl enable myproject-gunicorn.socket - Verifikasi socket file berhasil dibuat:
sudo systemctl status myproject-gunicorn.socket ls -l /run/myproject-gunicorn.sock
Kalau /run/myproject-gunicorn.sock muncul di hasil ls, socket activation berjalan dengan benar. Gunicorn service sendiri baru akan aktif secara otomatis begitu ada request pertama yang masuk lewat socket ini, biasanya dipicu oleh Nginx yang kita konfigurasi di langkah berikutnya.
Verifikasi: sebelum melibatkan Nginx, uji dulu apakah Gunicorn benar-benar merespons lewat socket tersebut, supaya kalau nanti ada masalah, kita sudah tahu Gunicorn bukan penyebabnya:
curl --unix-socket /run/myproject-gunicorn.sock http://localhost/Perintah curl di atas mengirim request HTTP langsung lewat Unix socket, meniru apa yang nanti dilakukan Nginx. Kalau output-nya adalah HTML halaman Django (bukan pesan curl: (7) Failed to connect atau Connection refused), berarti Gunicorn sudah siap menerima traffic dari Nginx. Cek juga systemctl status myproject-gunicorn.service untuk memastikan service-nya otomatis berpindah dari inactive ke active (running) setelah request pertama ini, sebagai bukti socket activation bekerja seperti yang dijelaskan di langkah 4.2.
5. Setup Nginx Reverse Proxy
Gunicorn sengaja tidak dihadapkan langsung ke internet. Nginx berdiri di depan sebagai reverse proxy, menerima semua request dari Visitor, melayani file statis langsung dari disk, dan meneruskan sisanya ke Gunicorn lewat socket.
5.1 Membuat Nginx Server Block
Buat file konfigurasi baru untuk project:
sudo nano /etc/nginx/sites-available/myproject5.2 Konfigurasi Routing
Isi file tersebut dengan konfigurasi berikut, sesuaikan server_name dengan domain milik Developer atau Sysadmin:
server {
listen 80;
server_name example.com www.example.com;
location /static/ {
alias /var/www/myproject/staticfiles/;
}
location /media/ {
alias /var/www/myproject/media/;
}
location / {
include proxy_params;
proxy_pass http://unix:/run/myproject-gunicorn.sock;
}
}Location block /static/ dan /media/ sengaja diletakkan sebelum location / supaya Nginx melayani request ke file statis langsung dari filesystem, tanpa perlu diteruskan ke Gunicorn dan Django. Ini alasan performa: Nginx jauh lebih efisien menyajikan file statis dibanding meminta Python memprosesnya. Sisa traffic yang tidak cocok dengan dua path itu diteruskan (proxy pass) ke myproject-gunicorn.sock lewat directive proxy_pass.
Perhatikan directive yang dipakai di dua location block statis itu adalah alias, bukan root. Bedanya penting: root menempelkan seluruh path di URL (termasuk /static/) ke direktori dasar, sedangkan alias menggantikan bagian path yang cocok dengan location. Kalau root /var/www/myproject; yang dipakai, request ke /static/app.css akan dicari Nginx di /var/www/myproject/static/app.css, padahal STATIC_ROOT di settings.py (langkah 3.5) diarahkan ke folder staticfiles, bukan static. Salah pilih directive di sini adalah penyebab umum file CSS dan JavaScript gagal load (muncul error 404) meski collectstatic sudah berhasil dijalankan.
5.3 Aktivasi Nginx
- Buat symlink konfigurasi ke folder
sites-enabledagar aktif:sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled - Uji syntax konfigurasi sebelum diterapkan:
sudo nginx -t - Restart Nginx agar konfigurasi baru berlaku:
sudo systemctl restart nginx
Kalau sudo nginx -t mengembalikan pesan syntax is ok dan test is successful, konfigurasi aman untuk diterapkan. Jangan pernah restart Nginx tanpa melewati pengujian ini dulu, karena satu typo di file konfigurasi bisa membuat Nginx gagal start dan seluruh traffic ke server terputus.
Verifikasi: uji rantai lengkap Nginx sampai Django lewat HTTP biasa (belum HTTPS, karena SSL baru dipasang di langkah 6). Dari server sendiri maupun dari komputer lain yang sudah bisa menjangkau domain atau IP-nya:
curl -I http://example.com/
curl -I http://example.com/static/admin/css/base.cssRequest pertama seharusnya mengembalikan HTTP/1.1 200 OK berupa view homepage sederhana yang dibuat di langkah 3.4 ("myproject" beserta pesan "Django 6.1 berjalan normal di server ini."), tanda Nginx berhasil meneruskan request ke Gunicorn dan Django lewat socket. Karena view ini sudah terdaftar permanen di urls.py (bukan halaman demo bawaan Django yang cuma tampil saat DEBUG = True), respons 200 ini akan tetap konsisten meski nanti DEBUG dimatikan di langkah 6.4. Request kedua menguji jalur file statis: kalau ikut mengembalikan 200 OK, berarti location block /static/ dan alias di langkah 5.2 sudah benar mengarah ke folder staticfiles. Kalau salah satu masih gagal, cek dulu sudo systemctl status nginx lalu lanjut ke bagian troubleshooting di bab 8.
5.4 Firewall UFW dan Port
Konfigurasi Nginx di atas percuma kalau firewall di level sistem operasi masih menutup port 80 dan 443. Banyak image server Ubuntu dari provider cloud sudah mengaktifkan Uncomplicated Firewall (UFW) secara default dengan hanya mengizinkan port SSH, sehingga Visitor tidak bisa mengakses aplikasi lewat HTTP maupun HTTPS meski Nginx sudah berjalan normal.
- Cek dulu status firewall sebelum mengubah apa pun:
sudo ufw status - Kalau statusnya
active, izinkan profile Nginx yang mencakup port 80 dan 443 sekaligus, dan pastikan port SSH tetap diizinkan supaya Sysadmin tidak terkunci dari server sendiri:sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' - Terapkan ulang aturan dan verifikasi port 80/443 sudah terbuka:
sudo ufw reload sudo ufw status
Kalau sudo ufw status di langkah pertama menampilkan inactive, artinya semua port memang masih terbuka dan langkah ini boleh dilewati untuk sementara. Tapi kalau Sysadmin berencana mengaktifkan UFW nanti, selalu jalankan sudo ufw allow OpenSSH lebih dulu sebelum sudo ufw enable, karena mengaktifkan firewall tanpa mengizinkan SSH lebih dulu akan memutus koneksi remote yang sedang dipakai untuk mengelola server.
6. Setup SSL Let's Encrypt
Aplikasi sudah bisa diakses lewat HTTP di titik ini, tapi traffic antara Visitor dan server masih belum terenkripsi. Certbot menyelesaikan ini dengan menerbitkan sertifikat SSL gratis dari Let's Encrypt dan otomatis mengonfigurasi ulang server block Nginx untuk HTTPS.
6.1 Instalasi Certbot
sudo apt install certbot python3-certbot-nginx -ypython3-certbot-nginx adalah plugin yang memungkinkan Certbot membaca dan mengubah otomatis konfigurasi server block Nginx yang sudah kita buat di langkah 5, jadi Sysadmin tidak perlu mengedit ulang file konfigurasi secara manual.
Verifikasi: pastikan Certbot dan plugin-nya terpasang dengan benar sebelum lanjut menerbitkan sertifikat:
certbot --version6.2 Generate Sertifikat SSL
sudo certbot --nginx -d example.com -d www.example.comCertbot akan menanyakan alamat email untuk notifikasi keamanan, meminta persetujuan Terms of Service Let's Encrypt, lalu menawarkan opsi mengalihkan otomatis semua traffic HTTP ke HTTPS. Pilih opsi redirect itu, karena tanpa redirect, Visitor yang mengetik http:// tanpa s tetap akan terhubung lewat koneksi tidak terenkripsi.
Verifikasi: cek detail sertifikat yang baru diterbitkan, lalu uji akses HTTPS dan pastikan HTTP lama sudah ikut dialihkan:
sudo certbot certificates
curl -I https://example.com/
curl -I http://example.com/sudo certbot certificates menampilkan domain yang ter-cover, lokasi file sertifikat, dan tanggal expired-nya (seharusnya sekitar 90 hari dari sekarang). curl -I https://example.com/ yang berhasil mengembalikan HTTP/2 200 tanpa keluhan sertifikat menandakan HTTPS sudah aktif. curl -I http://example.com/ (tanpa s) seharusnya mengembalikan status 301 dengan header Location menunjuk ke versi https://, bukti opsi redirect otomatis yang dipilih di langkah sebelumnya benar-benar aktif.
6.3 Testing Auto-Renewal
Sertifikat Let's Encrypt hanya berlaku 90 hari. Certbot otomatis memasang timer systemd untuk perpanjangan, tapi ada baiknya kita uji dulu apakah mekanisme ini berjalan tanpa benar-benar memperbarui sertifikat:
sudo certbot renew --dry-runKalau perintah ini selesai tanpa error, proses perpanjangan otomatis di background sudah bisa diandalkan, dan Sysadmin tidak perlu memperbarui sertifikat secara manual tiap tiga bulan.
6.4 Finalisasi settings.py Production
Selama langkah 3.7, 4.1, dan 5.3, DEBUG sengaja masih bernilai True (nilai default yang dibuat startproject) supaya kalau ada error, halaman traceback Django langsung tampil dan memudahkan proses pengujian. Sekarang HTTPS sudah aktif dan seluruh rantai terbukti berjalan, saatnya mematikan mode debug itu dan menambahkan beberapa opsi keamanan yang cuma relevan setelah SSL terpasang. Buka lagi myproject/settings.py dan ubah atau tambahkan baris berikut:
DEBUG = False
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = ['https://example.com', 'https://www.example.com']DEBUG = False wajib diaktifkan sebelum aplikasi benar-benar dipakai Visitor, karena DEBUG = True di production membocorkan detail internal aplikasi (potongan kode, environment variable, struktur database) ke siapa pun yang memicu error, termasuk Attacker yang sengaja mencari celah lewat traceback tersebut.
SECURE_PROXY_SSL_HEADER ini krusial dan sering terlewat: Nginx yang menangani enkripsi HTTPS, sedangkan request yang diteruskan ke Gunicorn lewat Unix socket selalu berupa HTTP biasa. Tanpa baris ini, Django menganggap semua request tidak aman (request.is_secure() selalu False) meski Visitor mengakses lewat https://, dan bisa memicu redirect loop atau cookie yang gagal terkirim. Konfigurasi ini aman dipakai karena file /etc/nginx/proxy_params yang di-include di langkah 5.2 sudah otomatis mengirim header X-Forwarded-Proto sesuai skema asli (HTTP atau HTTPS) yang dipakai Visitor, jadi Django bisa memercayainya.
SESSION_COOKIE_SECURE dan CSRF_COOKIE_SECURE memastikan cookie session dan CSRF Django cuma dikirim lewat koneksi HTTPS, tidak pernah lewat HTTP biasa yang bisa disadap Attacker di jaringan yang sama. CSRF_TRUSTED_ORIGINS perlu diisi dengan domain lengkap beserta skema https://-nya, karena tanpa ini Django bisa menolak submit form (termasuk login ke Django admin) dengan pesan CSRF verification failed saat aplikasi diakses lewat reverse proxy seperti Nginx.
Perhatikan kita tidak mengaktifkan SECURE_SSL_REDIRECT di Django. Redirect dari HTTP ke HTTPS sudah ditangani Nginx lewat opsi yang dipilih saat menjalankan Certbot di langkah 6.2, jadi mengaktifkannya lagi di Django cuma menambah satu lapis redirect yang tidak perlu.
Verifikasi: restart service Gunicorn supaya settings.py yang baru diubah terbaca ulang, lalu uji / dan /admin/ sekaligus, plus satu path yang sengaja tidak ada untuk memastikan traceback Python sudah tidak lagi bocor ke Visitor:
sudo systemctl restart myproject-gunicorn.service
curl -I https://example.com/
curl -I https://example.com/admin/
curl https://example.com/halaman-yang-tidak-ada/curl -I https://example.com/ harus tetap mengembalikan 200 OK lewat view homepage yang dibuat di langkah 3.4, tidak berubah jadi 404 meski DEBUG sudah False, karena view itu terdaftar permanen di urls.py, bukan bergantung pada halaman demo Django yang cuma tampil saat DEBUG = True. curl -I https://example.com/admin/ seharusnya mengembalikan 302 Found yang mengarahkan ke halaman login Django admin. Path yang sengaja tidak ada seharusnya menampilkan halaman 404 polos ala Django (bukan lagi halaman traceback berwarna dengan detail kode), bukti DEBUG = False sudah aktif. Kalau salah satu request di atas gagal atau muncul error 500/502, cek sudo tail -f /var/log/gunicorn/myproject/error.log seperti dibahas di bagian troubleshooting bab 8.
7. Arsitektur dan Alur Request
Setelah semua komponen terpasang, ada baiknya kita lihat bagaimana kelima layer ini saling terhubung saat satu request masuk dari Visitor sampai data diambil dari database, dan bagaimana posisi tiap komponen dalam stack secara keseluruhan.
7.1 Alur Request End-to-End
Diagram berikut menunjukkan perjalanan satu HTTP request dari browser Visitor sampai kembali sebagai response, termasuk jalur khusus untuk file statis yang tidak menyentuh Django sama sekali.
7.2 Lapisan Tech Stack
Diagram kedua menunjukkan susunan layer yang sama, tapi dari sudut pandang tanggung jawab tiap komponen dalam stack, dari request paling luar sampai base sistem operasi paling bawah.
Dua diagram ini penting dipahami saat troubleshooting: kalau ada error, langkah pertama adalah mencari tahu di layer mana request berhenti atau gagal, baru mengecek log di layer tersebut. Bagian berikutnya membahas persis cara melakukan itu.
8. Troubleshooting Log Tiap Layer
Error 502 Bad Gateway, error 500, atau aplikasi yang sama sekali tidak bisa diakses, hampir selalu bisa dilacak lewat log di salah satu layer berikut. Urutan pengecekan berikut mengikuti arah request, dari yang paling dekat dengan kode aplikasi sampai paling dekat dengan infrastructure.
8.1 Log Django
Kalau Gunicorn dijalankan manual lewat terminal (langkah 4.1), error traceback Python langsung tampil di console. Untuk production lewat systemd dengan DEBUG = False (sudah diaktifkan di langkah 6.4), traceback semacam ini tidak lagi tampil ke Visitor, jadi satu-satunya tempat melihatnya adalah error.log di bawah ini.
8.2 Log Gunicorn
Karena kita sudah mengarahkan --access-logfile dan --error-logfile ke file eksplisit di langkah 4.2, traceback Python dan riwayat request Gunicorn ada di sana, bukan di journal systemd:
sudo tail -f /var/log/gunicorn/myproject/error.log
sudo tail -f /var/log/gunicorn/myproject/access.logJournal systemd tetap berguna untuk melihat kejadian di level service itu sendiri, misalnya kapan Gunicorn gagal start, kapan Restart=on-failure memicu restart, atau error sebelum Gunicorn sempat membuka file log-nya sendiri:
sudo journalctl -u myproject-gunicorn
sudo journalctl -u myproject-gunicorn.socketKalau Nginx melempar error 502, penyebab paling umum ada di sini: service Gunicorn gagal start karena syntax error di kode (cek error.log atau journal), atau hak akses user/group pada socket file tidak sesuai dengan yang diharapkan Nginx. Cek juga kepemilikan socket file:
ls -l /run/myproject-gunicorn.sock8.3 Log Nginx
Dua file log Nginx punya fungsi berbeda. access.log mencatat setiap request yang masuk (berguna melihat apakah request Visitor benar-benar sampai ke Nginx), sedangkan error.log mencatat kegagalan Nginx sendiri, termasuk saat gagal terhubung ke socket Gunicorn atau saat file statis tidak ditemukan.
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.logPesan seperti connect() to unix:/run/myproject-gunicorn.sock failed di error.log berarti masalah bukan di Nginx, melainkan Gunicorn yang tidak aktif atau socket path-nya salah ketik di konfigurasi server block.
8.4 Log MariaDB
Kalau Django gagal terhubung ke database, biasanya error muncul lebih dulu di log Gunicorn/Django sebagai exception Python. Untuk melihat sisi database-nya, misalnya saat MariaDB menolak koneksi atau crash, cek dulu lewat journal systemd, bukan lewat file log:
sudo journalctl -u mariadb -ePackage mariadb-server di Ubuntu 26.04 secara default mengirim log error ke syslog/journald, bukan ke file /var/log/mysql/error.log. File itu cuma muncul kalau log_error secara eksplisit diarahkan ke sebuah path di konfigurasi. Kalau Sysadmin mencoba sudo tail -f /var/log/mysql/error.log dan mendapat pesan No such file or directory, itu bukan tanda ada yang salah, cuma tanda MariaDB memang belum dikonfigurasi menulis log ke file dan journalctl di atas adalah cara yang benar untuk versi default ini.
Kalau Sysadmin tetap ingin log tersimpan sebagai file terpisah (misalnya supaya lebih mudah di-rotate dengan logrotate atau diproses tool monitoring lain), tambahkan baris berikut ke /etc/mysql/mariadb.conf.d/50-server.cnf di bagian [mariadbd], lalu restart service-nya:
log_error = /var/log/mysql/error.logsudo systemctl restart mariadb
sudo tail -f /var/log/mysql/error.logSetelah restart, MariaDB akan membuat file error.log itu sendiri di direktori /var/log/mysql/ yang kepemilikannya sudah diatur package mariadb-server, dan barulah perintah tail -f di atas bisa dipakai.
8.5 Izin File dan Firewall
Beberapa penyebab ini tidak muncul di log aplikasi sama sekali, tapi paling sering bikin Sysadmin bingung karena semua service terlihat running normal.
- Error 403 Forbidden dari Nginx saat mengakses file statis: biasanya karena user
www-datatidak punya izin baca ke folderstaticfilesataumedia. Kedua folder ini seharusnya sudah dibuat masing-masing di langkah 3.5 (media) dan 3.6 (staticfileslewatcollectstatic);mkdir -pdi bawah ini aman dijalankan ulang kalau salah satu ternyata belum ada atau sempat terhapus. Cek dan perbaiki izin dengan:mkdir -p /var/www/myproject/staticfiles /var/www/myproject/media sudo chmod -R o+rX /var/www/myproject/staticfiles /var/www/myproject/media - Server tidak bisa diakses sama sekali dari luar padahal
systemctl status nginxmenunjukkan service aktif: kemungkinan besar firewall UFW masih menutup port 80/443, seperti dibahas di langkah 5.4. Cek dengansudo ufw status, dan pastikan juga security group atau firewall di sisi provider cloud (kalau ada) juga sudah mengizinkan port yang sama, karena UFW hanya mengatur firewall di dalam OS, bukan firewall jaringan di luar server. - Root domain (
/) menampilkan404 Not Foundpolos, padahal semua service aktif dancurl -Ike/admin/berhasil: karena view homepage sudah didaftarkan permanen diurls.pysejak langkah 3.4,/seharusnya tidak pernah404lagi, terlepas dari nilaiDEBUG. Kalau gejala ini muncul, cek dulu isimyproject/urls.pyapakah barispath('', homepage, name='homepage')masih ada dan tidak sengaja terhapus atau tertimpa saat deploy ulang. Kalau baris itu memang hilang, kembalikan sesuai langkah 3.4, lalu restart Gunicorn supaya perubahan terbaca ulang.
9. Update Kode Setelah Deploy
Deployment bukan aktivitas sekali jalan. Setelah aplikasi live, Developer akan terus menambah fitur atau memperbaiki bug, dan kode baru itu perlu sampai ke server dengan cara yang benar. Bab ini merangkum tindakan apa saja yang perlu dilakukan Sysadmin setiap kali ada perubahan project, supaya tidak ada langkah yang terlewat, seperti lupa restart Gunicorn sehingga kode lama masih yang berjalan padahal file di server sudah ter-update.
9.1 Kapan Perlu Restart Gunicorn
Gunicorn me-load kode Django ke memori cuma sekali, tepat saat worker process-nya pertama kali start. Ini beda dari development server (runserver) yang otomatis mendeteksi perubahan file dan me-reload sendiri. Konsekuensinya, tidak semua jenis perubahan butuh perlakuan yang sama:
- Perubahan pada file Python apa pun (
views.py,models.py,urls.py,settings.py, atau module lain yang di-import aplikasi) wajib diikuti restart Gunicorn. Tanpa ini, Visitor tetap akan mendapat perilaku kode versi lama walau file di server sudah berubah. - Perubahan pada template HTML tidak perlu restart. Django membaca ulang isi file template dari disk setiap kali ada request, kecuali secara eksplisit mengaktifkan cached template loader, yang tidak dipakai di konfigurasi tutorial ini.
- Perubahan pada file statis (CSS, JavaScript, gambar) tidak perlu restart Gunicorn, tapi tetap perlu
collectstaticulang (bahasan lengkap di langkah 9.2), karena Nginx menyajikan file-file ini langsung dari folderstaticfiles, bukan lewat Django.
Restart standar cukup dengan perintah yang sama seperti langkah 4.3:
sudo systemctl restart myproject-gunicorn.servicePerintah ini mematikan seluruh worker lama lalu menyalakan yang baru, jadi ada jeda singkat (biasanya kurang dari satu detik) di mana request yang masuk akan mengantre di socket sampai worker baru siap, bukan langsung gagal, berkat socket activation yang dibahas di langkah 4.2. Untuk aplikasi dengan traffic tinggi yang jeda sekecil apa pun terasa mengganggu, Gunicorn punya cara reload worker lebih halus lewat sinyal SIGHUP, tanpa perlu menambah directive apa pun ke myproject-gunicorn.service:
sudo systemctl kill -s HUP myproject-gunicorn.serviceSinyal HUP ini memberi tahu proses arbiter Gunicorn untuk memuat ulang worker satu per satu secara bergantian, sehingga selalu ada worker lain yang tetap melayani request selagi worker yang di-reload bergantian. Untuk project skala kecil sampai menengah seperti di tutorial ini, jeda dari systemctl restart biasa umumnya tidak masalah, dan lebih mudah diingat.
Kalau yang berubah bukan kode aplikasi, tapi file unit systemd itu sendiri (myproject-gunicorn.socket atau myproject-gunicorn.service), restart service saja tidak cukup. Muat ulang dulu konfigurasi systemd daemon-nya seperti langkah 4.3:
sudo systemctl daemon-reload
sudo systemctl restart myproject-gunicorn.service9.2 Migrate dan Collectstatic Ulang
Dua perintah dari langkah 3.6 ini bukan cuma dijalankan sekali di awal, keduanya perlu diulang setiap kali perubahan kode menyentuh bagian yang relevan.
- Kalau ada perubahan di
models.py(menambah field, membuat model baru, dan sejenisnya) yang sudah dibuatkan file migration-nya lewatpython manage.py makemigrations(biasanya dilakukan di komputer development, bukan di server), terapkan migration itu ke database production:python manage.py migrateJalankan ini sebelum restart Gunicorn, supaya tidak ada jeda waktu di mana kode baru sudah aktif tapi skema database-nya belum sesuai, yang bisa memicu error
OperationalErroratauProgrammingErrordari MariaDB. - Kalau ada perubahan pada file statis (menambah CSS/JS baru, mengganti logo, meng-update package front-end), kumpulkan ulang ke
STATIC_ROOT:python manage.py collectstatic --noinputFlag
--noinputmelewati prompt konfirmasi "this will overwrite existing files" yang biasanya muncul kalaustaticfilessudah pernah diisi sebelumnya, berguna kalau perintah ini nanti dijalankan otomatis lewat script deploy tanpa ada orang yang menekanYsecara manual.
9.3 Alur Update Kode Lengkap
Menggabungkan semua poin di atas, urutan lengkap yang aman diikuti Sysadmin setiap kali ada perubahan kode yang perlu di-deploy ke server adalah:
- Masuk ke direktori project dan aktifkan virtual environment:
cd /var/www/myproject source venv/bin/activate - Salin kode terbaru ke server, misalnya lewat
git pullkalau project dikelola dengan Git, ataursync/scpkalau belum:git pull origin main - Update dependency kalau
requirements.txtikut berubah (lihat lagi langkah 3.3):pip install -r requirements.txt - Terapkan migration database kalau ada perubahan model:
python manage.py migrate - Kumpulkan ulang file statis kalau ada yang berubah:
python manage.py collectstatic --noinput - Restart Gunicorn supaya kode Python yang baru ter-load:
sudo systemctl restart myproject-gunicorn.service
Kalau perubahan kali ini juga menyentuh konfigurasi Nginx (bukan cuma kode Django), tambahkan satu langkah lagi sebelum selesai: uji dulu syntax-nya, baru reload (bukan restart) supaya koneksi Visitor yang sedang berlangsung tidak terputus:
sudo nginx -t
sudo systemctl reload nginxVerifikasi: tutup alur ini dengan pengujian singkat, sama seperti langkah 6.4, untuk memastikan tidak ada yang rusak akibat perubahan barusan:
curl -I https://example.com/
curl -I https://example.com/admin/Kalau keduanya masih mengembalikan status yang sama seperti sebelum update (200 OK dan 302 Found), deploy kode baru ini berhasil tanpa merusak apa pun. Kalau ada yang gagal, langsung ke bagian troubleshooting di bab 8, mulai dari sudo tail -f /var/log/gunicorn/myproject/error.log untuk melihat penyebabnya.
10. Kesimpulan
Sampai titik ini, aplikasi Django sudah berjalan lewat lima layer yang saling melengkapi: Nginx menghadapi Visitor dan melayani file statis, Gunicorn menjembatani ke kode Python lewat socket yang aksesnya dibatasi, Django memproses logic aplikasi, MariaDB menyimpan data lewat user khusus yang haknya dibatasi, dan Let's Encrypt menjaga seluruh komunikasi tetap terenkripsi lewat HTTPS, dengan UFW mengunci port yang tidak perlu terbuka ke publik. Kalau ada masalah di kemudian hari, urutan troubleshooting di bagian 8 selalu jadi titik awal yang baik, mulai dari log paling dekat dengan kode sampai paling dekat dengan infrastructure. Setiap kali ada perubahan kode baru, alur di bagian 9 memastikan Visitor selalu mendapat versi aplikasi yang terbaru dan konsisten.




