Men-deploy FrankenPHP ke production dengan Docker dan Kubernetes: membangun image yang benar dengan install-php-extensions, volume caddy_data/caddy_config yang persisten, menjalankan sebagai non-root, serta Deployment, Service, Ingress, dan HPA di Kubernetes.

Setelah 18 episode membangun dan mengamankan aplikasi, kini saatnya men-deploy ke infrastruktur nyata. Episode ini membahas dua lapis deployment yang paling umum untuk FrankenPHP di production 2026: Docker (image yang benar, volume yang persisten, non-root) dan Kubernetes (Deployment, Service, Ingress, dan autoscaling). Deployment yang buruk membuat semua tuning performa episode 17 sia-sia.
Mengapa penting? Image FrankenPHP yang salah (misal lupa volume caddy_data) akan kehilangan sertifikat TLS setiap restart; Deployment K8s tanpa liveness/readiness probe membuat rolling update memakan downtime; dan HPA yang salah membaca metrik membuat cluster membengkak tak terkendali. Detail-deploy menentukan stabilitas jangka panjang.
Image resmi dunglas/frankenphp adalah titik awal terbaik. Untuk aplikasi nyata, kita menambahkan ekstensi PHP dan kode aplikasi:
FROM dunglas/frankenphp:1.12.7-php8.5-bookworm
# Ekstensi yang dibutuhkan aplikasi
RUN install-php-extensions \
pdo_pgsql \
redis \
opcache \
intl
# Setelah ini semua perintah berjalan sebagai user 'frankenphp'
USER frankenphp
WORKDIR /app
COPY --chown=frankenphp:frankenphp . .
EXPOSE 80 443 443/udpinstall-php-extensions adalah skrip bawaan image yang mengkompilasi ekstensi dengan aman — jauh lebih mudah daripada menangani docker-php-ext-install manual.
caddy_data dan caddy_configDua hal tidak boleh hilang saat container restart:
caddy_data — sertifikat TLS, kunci private, OCSP staples. Hilang = re-issue sertifikat (dan Let's Encrypt rate limit).caddy_config — konfigurasi ter-adapt.services:
app:
build: .
ports:
- "80:80"
- "443:443"
- "443:443/udp"
environment:
SERVER_NAME: "kaliansite.com"
GOMEMLIMIT: "2GiB"
volumes:
- caddy_data:/data
- caddy_config:/config
- ./public:/app/public
restart: always
volumes:
caddy_data:
caddy_config:Warning
Tanpa volume caddy_data, setiap docker compose up -d --build menghasilkan sertifikat baru. Ledakan permintaan ke Let's Encrypt bisa berujung pada rate limit yang menghentikan issuance beberapa jam. Persistenkan volume — selalu.
Image FrankenPHP resmi menyediakan user frankenphp (uid/gid 1000). Menjalankan container sebagai root adalah risiko keamanan yang tidak perlu — jika aplikasi berhasil dieksploitasi, penyerang punya akses root di container:
docker run --user frankenphp -p 80:80 -p 443:443 -p 443:443/udp dunglas/frankenphpPastikan volume aplikasi punya permission yang benar untuk uid 1000:
chown -R 1000:1000 ./public ./storageDi Kubernetes, jangan lupa mengaturnya juga:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000apiVersion: apps/v1
kind: Deployment
metadata:
name: frankenphp-app
spec:
replicas: 3
selector:
matchLabels:
app: frankenphp-app
template:
metadata:
labels:
app: frankenphp-app
spec:
containers:
- name: app
image: registry.example.com/frankenphp-app:1.12.7
ports:
- containerPort: 80
env:
- name: SERVER_NAME
value: "kaliansite.com"
livenessProbe:
httpGet: { path: /healthz, port: 80 }
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet: { path: /readyz, port: 80 }
initialDelaySeconds: 5
periodSeconds: 10
volumeMounts:
- { name: data, mountPath: /data }
- { name: config, mountPath: /config }
securityContext:
runAsNonRoot: true
runAsUser: 1000
volumes:
- name: data
persistentVolumeClaim: { claimName: frankenphp-data }
- name: config
persistentVolumeClaim: { claimName: frankenphp-config }/healthz) — restart pod yang macet./readyz) — keluarkan pod dari Service saat belum siap.apiVersion: v1
kind: Service
metadata:
name: frankenphp-app
spec:
selector:
app: frankenphp-app
ports:
- port: 80
targetPort: 80Jika Ingress controller tidak mendukung HTTP/3/HTTP/2 h2c, kalian kehilangan beberapa fitur. Opsi umum:
Untuk pengalaman penuh FrankenPHP (automatic HTTPS, HTTP/3), gunakan LoadBalancer Service yang mengekspos 80/443/443-UDP langsung ke Caddy — Caddy menjadi edge sendiri. Jika sudah punya ingress controller, route biasa via Ingress tetap valid.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: frankenphp-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: frankenphp-app
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60Caution
HPA berbasis CPU bekerja, tapi datang belakangan — pod baru butuh beberapa menit siap, sementara antrean request sudah menumpuk. Untuk lonjakan tajam, pertimbangkan custom metrics berbasis frankenphp_queue_depth (episode 12) atau target GPU/CPU yang lebih agresif.
caddy_data via PVCSertifikat yang persisten (episode 15) berarti caddy_data harus bertahan dari pod lama ke pod baru. Dengan PVC berkapasitas kecil:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: frankenphp-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1GiSemua pod berbagi satu PVC hanya jika storage class mendukung ReadWriteMany. Kalau tidak, pastikan Deployment menggunakan satu PVC yang dibagikan ke semua replica (data sertifikat sama) atau gunakan ReadWriteOnce dengan satu pod utama — pola yang paling umum adalah satu PVC ReadWriteMany (misal NFS/Rook) atau mengabaikan persistence dan menerima re-issue.
latest: deploy tanpa pin versi → update tak terduga. Selalu tag versi penuh (contoh 1.12.7-php8.5-bookworm).resources.requests.cpu.Pada episode 19 ini, kalian telah men-deploy FrankenPHP ke Docker dan Kubernetes.
Inti yang harus dibawa pulang:
install-php-extensions; jalankan sebagai user frankenphp (non-root).caddy_data dan caddy_config — hilangnya sertifikat adalah rate limit Let's Encrypt.queue_depth) saat skala.Di episode 20 selanjutnya kita menaikkan level: cluster, autoscaling & high availability — mendesain FrankenPHP yang stateless agar bisa di-scale keluar dengan aman, rolling deployment, blue-green, dan drain request. Sampai jumpa di episode 20!