3
Arsitektur Sistem
Hydro berjalan sebagai tiga container: web melayani berkas statis sekaligus meneruskan /api/, api menjalankan Django lewat Gunicorn, dan db menyimpan seluruh data di PostgreSQL 18.1. Gambaran itu sudah terlihat di compose.yaml:3-461. Produksi memakai pembagian container yang sama, tetapi menambahkan jaringan internal dan pembatasan filesystem di compose.prod.yaml:1-212 serta compose.prod.yaml:22-403.
Topologi container
Browser hanya berhubungan dengan container web. Di dalamnya tidak ada Node.js: image akhir berisi Nginx dan isi /src/out hasil npm run build (deploy/web.Dockerfile:1-144). Permintaan ke /api/ diteruskan Nginx ke http://api:8000 di dalam jaringan Compose (deploy/nginx.conf:21-335). Container api menjalankan gunicorn config.wsgi dan menyimpan unggahan di volume media (compose.yaml:16-336), sedangkan db memakai postgres:18.1 dengan healthcheck pg_isready -U hydro -d hydro (compose.yaml:35-467).
graph TD
B["Browser"] -->|"443/TLS"| W
W["web: nginx-unprivileged 8443"]
W -->|"location /api/ proxy_pass"| A
W --> S["berkas statis /src/out"]
A["api: gunicorn config.wsgi 8000"] -->|"psycopg"| D
A --- M[("media:/data/media")]
D["db: postgres:18.1 5432"]
D --- P[("pgdata:/var/lib/postgresql")]
style W fill:#f59e0b,color:#000
style A fill:#2d7d46,color:#fff
style S fill:#8b5cf6,color:#fff
style D fill:#336791,color:#fff
style M fill:#336791,color:#fff
style P fill:#336791,color:#fff
%% fig: Topologi container Hydro
%% desc: Browser masuk ke container web, Nginx menyajikan berkas statis dan meneruskan /api/ ke Gunicorn, yang membaca PostgreSQL serta volume media.Perbedaan kedua berkas compose terletak pada cara container dijalankan, bukan pada pembagian tugasnya.
| Aspek | compose.yaml (lokal) | compose.prod.yaml (produksi) |
|---|---|---|
| Sumber image | build dari deploy/web.Dockerfile dan ./backend | ghcr.io/${GHCR_OWNER:?}/hydro-web:${APP_VERSION:?} dan hydro-api |
Port web | 127.0.0.1:8080:8080 | ${TAILSCALE_IP:?}:443:8443 |
| Jaringan | jaringan default Compose | edge, app, data; app dan data memakai internal: true |
| Restart | tidak diset | restart: unless-stopped |
| Filesystem | dapat ditulis | read_only: true dengan tmpfs: /tmp |
| Urutan start | depends_on: condition: service_healthy | sama, sekali lagi service_healthy |
Di komputer sendiri, container web memakai deploy/nginx.lokal.conf yang dipasang sebagai volume read-only, tanpa TLS dan tanpa location /_next/static/ khusus (compose.yaml:3-148, deploy/nginx.lokal.conf:1-179). Jaringan produksi memisahkan edge untuk trafik masuk, app untuk Nginx ke Gunicorn, dan data untuk Gunicorn ke PostgreSQL; app serta data sama-sama internal: true sehingga hanya bisa diakses dari dalam (compose.prod.yaml:58-6310).
Perjalanan satu request
Muat halaman dan panggilan data menempuh jalur berbeda. Nginx menyajikan index.html dari root /usr/share/nginx/html dengan Cache-Control "no-cache" dan try_files $uri $uri/index.html /404.html (deploy/nginx.conf:35-4411). Setelah halaman hidup, kode frontend memanggil fetch dengan credentials: "same-origin" dan menyertakan X-CSRFToken dari cookie csrftoken pada metode selain GET (frontend/src/lib/api.ts:31-4112). Karena origin-nya sama, panggilan itu mendarat di location /api/ milik Nginx.
sequenceDiagram
participant B as Browser
participant N as web Nginx
participant G as api Gunicorn
participant J as Django + DRF
participant P as db PostgreSQL
B->>N: GET /produk/
N-->>B: index.html dengan Cache-Control no-cache
B->>N: fetch /api/produk/ credentials same-origin
N->>G: proxy_pass http://api:8000
G->>J: config.wsgi
J->>J: MIDDLEWARE Security, Session, CSRF, Auth
J->>P: query ORM
P-->>J: rows
J-->>G: JSONRenderer
G-->>N: 200 application/json
N-->>B: header keamanan dari keamanan.conf
Note over B,N: metode tulis mengirim X-CSRFToken dari cookie csrftoken
%% fig: Siklus satu halaman dan satu panggilan API
%% desc: Nginx menyajikan halaman statis, lalu meneruskan panggilan /api/ ke Gunicorn, middleware Django, dan PostgreSQL.Semua endpoint Django berada di bawah prefiks /api/, termasuk router SimpleRouter dan api/_allauth/ (backend/config/urls.py:38-4713). Django memproses request melewati MIDDLEWARE yang berurutan: SecurityMiddleware, SessionMiddleware, CommonMiddleware, CsrfViewMiddleware, AuthenticationMiddleware, lalu AccountMiddleware, AuditlogMiddleware, dan AxesMiddleware (backend/config/settings.py:47-5814). Koneksi database dibuka dengan CONN_MAX_AGE 60 detik (backend/config/settings.py:77-8715).
⚠️ Django hanya tahu request itu HTTPS karena header, bukan karena socket. Nginx memasang
X-Forwarded-Proto https, danSECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")dipakai saatDEBUGmati (deploy/nginx.conf:21-335, backend/config/settings.py:140-14516). Kalau header itu hilang,SECURE_SSL_REDIRECTakan membalas redirect.
Peta rute Nginx
Empat blok location menentukan seluruh perilaku lapisan tepi. Setiap blok memuat ulang /etc/nginx/keamanan.conf, lalu menambahkan Cache-Control sendiri (deploy/keamanan.conf:1-717).
location | Perilaku | Rujukan |
|---|---|---|
/api/kesehatan/ | return 404, hanya untuk healthcheck internal | deploy/nginx.conf:16-1918 |
/api/ | proxy_pass http://api:8000, proxy_read_timeout 60s, header keamanan dari Django disembunyikan | deploy/nginx.conf:21-335 |
/_next/static/ | Cache-Control "public, max-age=31536000, immutable" | deploy/nginx.conf:35-4411 |
/ | Cache-Control "no-cache", try_files $uri $uri/index.html /404.html | deploy/nginx.conf:35-4411 |
Blok server menetapkan listen 8443 ssl dengan http2 on, server_tokens off, dan client_max_body_size 6m, sehingga unggahan melewati Nginx sebelum diperiksa Django (deploy/nginx.conf:1-1419).
Berkas statis dan jalur build
Konfigurasi Next.js bercabang menurut NODE_ENV. Di produksi dipakai output: "export", trailingSlash: true, dan images: { unoptimized: true }, sehingga hasil build berupa direktori statis tanpa server Next.js (frontend/next.config.ts:7-3620). Di development, rewrites() mengarahkan /api/:path* ke API_DEV yang default-nya http://127.0.0.1:8000, jadi next dev tetap bisa memanggil Django tanpa Nginx (frontend/next.config.ts:7-3620).
| Batas | Nilai | Rujukan |
|---|---|---|
client_max_body_size | 6m | deploy/nginx.conf:1-1419 |
proxy_read_timeout | 60s | deploy/nginx.conf:21-335 |
| Gunicorn di compose lokal | --workers 2 --access-logfile - | compose.yaml:16-336 |
| Gunicorn image produksi | --workers 3 --timeout 60 | backend/Dockerfile:20-24 |
CONN_MAX_AGE | 60 detik | backend/config/settings.py:77-8715 |
Masa cache /_next/static/ | max-age=31536000, immutable | deploy/nginx.conf:35-4411 |
Foto produk dan healthcheck
Foto produk tidak disajikan sebagai berkas statis. MEDIA_ROOT diarahkan ke /data/media (backend/Dockerfile:20-24, backend/config/settings.py:111-11221), volume yang sama dipasang ke container api (compose.prod.yaml:22-403), lalu view foto mengembalikan FileResponse dari berkas itu (backend/produk/views.py:98-10322). Karena Nginx tidak punya location /media/, byte foto tetap melewati /api/ dan aturan izin Django.
Healthcheck bekerja dari dalam container: python healthcheck.py memanggil http://127.0.0.1:8000/api/kesehatan/ dengan header Host dari DJANGO_ALLOWED_HOSTS (backend/Dockerfile:20-24, backend/healthcheck.py:1-1323). Jalur itu sengaja ditutup dari luar oleh return 404 di Nginx, dan dikecualikan dari redirect HTTPS lewat SECURE_REDIRECT_EXEMPT = [r"^api/kesehatan/$"] (deploy/nginx.conf:16-1918, backend/config/settings.py:140-14516).
Jebakan konfigurasi
⚠️ Port yang dipublikasikan Docker dapat melewati aturan UFW. Itu sebabnya produksi mengikat
webke${TAILSCALE_IP:?}:443:8443, bukan ke0.0.0.0(compose.prod.yaml:1-212).
⚠️ Di luar
DEBUG,SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]sengaja gagal saat start kalau variabel itu kosong, danALLOWED_HOSTSmenjadi daftar kosong bilaDJANGO_ALLOWED_HOSTStidak diisi (backend/config/settings.py:15-2424).
⚠️
apidanwebberjalan denganread_only: truesertacap_drop: [ALL]. Setiap penulisan harus menuju volume (media) atautmpfs/tmp, bukan disk container (compose.prod.yaml:22-403).
Data yang mengalir lewat PostgreSQL, termasuk batch dan mutasi stok yang menopang alur FEFO, dijelaskan di Model Domain dan Constraint Database dan Alur Stok, Batch, dan Opname. Sisi klien yang memakai rute /api/ ini dibahas di Frontend Next.js dan Lapisan Data, daftar endpoint-nya di Endpoint API dan Skema OpenAPI, dan prosedur rilis ketiga container di Deploy, Backup, dan Operasi VPS.