Kubernetes Cho Laravel: Từ Docker Đến Production K8s
Bạn đã container hóa Laravel app với Docker. Giờ cần chạy ở quy mô lớn — nhiều instances, auto-scaling, zero-downtime deploys, và self-healing. Đó là Kubernetes (K8s).
Bài viết này đi từ Dockerfile đến production cluster. Mỗi component có giải thích tại sao cấu hình vậy, không chỉ "chạy được".
Kiến Trúc Tổng Quan
┌─────────────┐
│ Ingress │ (TLS termination, routing)
└──────┬──────┘
│
┌──────┴──────┐
│ Service │ (Load balancing)
└──────┬──────┘
┌──────────────┼──────────────┐
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Pod (web) │ │ Pod (web) │ │ Pod (web) │
└───────────┘ └───────────┘ └───────────┘
┌──────────────┐
│ Queue Workers │ (Separate Deployment)
└──────────────┘
┌──────────────┐
│ Scheduler │ (CronJob, mỗi phút)
└──────────────┘
Components:
- Ingress: entry point, TLS, routing rules
- Service: internal load balancer, service discovery
- Deployment (web): Laravel app pods, horizontal scaling
- Deployment (queue): queue workers, riêng biệt khỏi web
- CronJob:
schedule:runmỗi phút - HPA: auto-scale dựa trên CPU/memory
Bước 1: Docker Image (Multi-Stage)
# ---- Stage 1: Composer dependencies ----
FROM composer:2 AS composer
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative
# ---- Stage 2: Frontend assets ----
FROM node:20-alpine AS assets
WORKDIR /var/www/html
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- Stage 3: Production image ----
FROM php:8.3-fpm-alpine AS production
RUN apk add --no-cache nginx supervisor \
&& docker-php-ext-install pdo_mysql opcache bcmath
# OPcache config cho production
RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini && \
echo "opcache.memory_consumption=256" >> /usr/local/etc/php/conf.d/opcache.ini && \
echo "opcache.max_accelerated_files=20000" >> /usr/local/etc/php/conf.d/opcache.ini && \
echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/opcache.ini
WORKDIR /var/www/html
# Copy built assets và vendor
COPY --from=composer /var/www/html/vendor ./vendor
COPY --from=assets /var/www/html/public/build ./public/build
COPY . .
# Cache config/routes/views cho performance
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache \
&& php artisan event:cache
# Non-root user — security best practice
RUN adduser -D -u 1000 laravel && \
chown -R laravel:laravel /var/www/html/storage /var/www/html/bootstrap/cache
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
Supervisord Config
; docker/supervisord.conf
[supervisord]
nodaemon=true
user=root
[program:nginx]
command=nginx -g "daemon off;"
autostart=true
autorestart=true
[program:php-fpm]
command=php-fpm --nodaemonize
autostart=true
autorestart=true
Tại sao multi-stage?
- Stage 1: Composer install — chỉ copy
composer.json/locktrước, tận dụng Docker layer cache - Stage 2: npm build — tách biệt, chỉ chạy khi frontend thay đổi
- Stage 3: Production — image nhỏ gọn, không có dev dependencies, node_modules, hay source maps
opcache.validate_timestamps=0: production không cần check file changes → faster
Bước 2: Namespace & Configuration
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: laravel-app
labels:
app: laravel
Luôn dùng namespace riêng cho mỗi app — tránh collision, dễ quản lý RBAC, dễ cleanup.
ConfigMap
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: laravel-config
namespace: laravel-app
data:
APP_ENV: "production"
APP_DEBUG: "false"
APP_URL: "https://myapp.com"
LOG_CHANNEL: "stderr" # K8s collects stdout/stderr tự động
LOG_LEVEL: "warning"
CACHE_DRIVER: "redis"
SESSION_DRIVER: "redis"
QUEUE_CONNECTION: "redis"
REDIS_HOST: "redis-master.laravel-app.svc.cluster.local"
DB_HOST: "mysql.laravel-app.svc.cluster.local"
DB_DATABASE: "laravel"
LOG_CHANNEL=stderr là quan trọng: K8s tự collect container logs từ stdout/stderr. Không cần write file. kubectl logs hoạt động ngay.
Secrets
# k8s/secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: laravel-secrets
namespace: laravel-app
type: Opaque
stringData:
APP_KEY: "base64:your-app-key-here"
DB_USERNAME: "laravel_user"
DB_PASSWORD: "your-secure-db-password"
REDIS_PASSWORD: "your-redis-password"
Production: Đừng commit secrets vào Git. Dùng Sealed Secrets, External Secrets Operator, hoặc Vault để inject secrets an toàn. YAML ở trên chỉ cho development/demo.
Bước 3: Deployment (Web)
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-app
namespace: laravel-app
labels:
app: laravel
component: web
spec:
replicas: 3
selector:
matchLabels:
app: laravel
component: web
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Tạo 1 pod mới trước
maxUnavailable: 0 # KHÔNG giảm pod cũ cho đến khi pod mới ready
template:
metadata:
labels:
app: laravel
component: web
spec:
terminationGracePeriodSeconds: 60
# Init container: chạy migrations trước khi web pods start
initContainers:
- name: migrate
image: your-registry/laravel-app:1.0.0
command: ["php", "artisan", "migrate", "--force"]
envFrom:
- configMapRef:
name: laravel-config
- secretRef:
name: laravel-secrets
containers:
- name: laravel
image: your-registry/laravel-app:1.0.0
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: laravel-config
- secretRef:
name: laravel-secrets
# Resource management
resources:
requests:
cpu: "100m" # Minimum guaranteed
memory: "128Mi"
limits:
cpu: "500m" # Maximum allowed
memory: "512Mi" # OOMKilled nếu vượt
# Readiness: pod nhận traffic chưa?
readinessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
# Liveness: pod còn sống không?
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 15
periodSeconds: 20
failureThreshold: 3
# Startup: cho phép slow start (migrations, cache warm)
startupProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 30 # 30 × 5s = 150s max startup time
# Graceful shutdown
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "nginx -s quit && sleep 10"]
Giải Thích 3 Loại Probe
| Probe | Hỏi gì? | Khi fail |
|---|---|---|
| startupProbe | App đã boot xong chưa? | Chờ tiếp (không restart) |
| readinessProbe | App sẵn sàng nhận traffic? | Loại khỏi Service (không nhận request) |
| livenessProbe | App có bị stuck/deadlock? | Restart pod |
Thứ tự: startupProbe chạy trước. Khi pass → readiness + liveness bắt đầu chạy định kỳ.
Tại sao maxUnavailable: 0? Zero-downtime deploy: K8s tạo pod mới trước, đợi readinessProbe pass, rồi mới terminate pod cũ. Không bao giờ có thời điểm thiếu pod.
Tại sao preStop sleep? Khi pod bị terminate, K8s cần thời gian update iptables/endpoints. Sleep 10s đảm bảo in-flight requests hoàn thành trước khi pod thực sự shutdown.
Health Check Endpoint
// routes/web.php
Route::get('/health', function () {
$checks = [];
// Database
try {
DB::connection()->getPdo();
$checks['database'] = 'ok';
} catch (\Exception $e) {
$checks['database'] = 'failed';
}
// Redis
try {
Redis::ping();
$checks['redis'] = 'ok';
} catch (\Exception $e) {
$checks['redis'] = 'failed';
}
// Storage
$checks['storage'] = is_writable(storage_path()) ? 'ok' : 'failed';
$healthy = !in_array('failed', $checks);
return response()->json([
'status' => $healthy ? 'healthy' : 'unhealthy',
'checks' => $checks,
'timestamp' => now()->toIso8601String(),
], $healthy ? 200 : 503);
});
Lưu ý: health check endpoint KHÔNG nên nặng. Đừng query count hay run complex logic. Chỉ verify connections hoạt động.
Bước 4: Service & Ingress
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: laravel-service
namespace: laravel-app
spec:
selector:
app: laravel
component: web
ports:
- port: 80
targetPort: 80
type: ClusterIP # Internal only, Ingress handles external
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: laravel-ingress
namespace: laravel-app
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/proxy-body-size: "50m" # Upload limit
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
nginx.ingress.kubernetes.io/rate-limit-connections: "10" # Rate limiting
nginx.ingress.kubernetes.io/rate-limit-rps: "20"
spec:
ingressClassName: nginx
tls:
- hosts:
- myapp.com
- www.myapp.com
secretName: laravel-tls
rules:
- host: myapp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: laravel-service
port:
number: 80
Ingress annotations giúp config Nginx Ingress Controller trực tiếp — rate limiting, upload size, timeouts — mà không cần sửa Nginx config trong container.
Bước 5: Queue Workers
# k8s/queue-worker.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-queue-worker
namespace: laravel-app
labels:
app: laravel
component: queue
spec:
replicas: 2
selector:
matchLabels:
app: laravel
component: queue
template:
metadata:
labels:
app: laravel
component: queue
spec:
terminationGracePeriodSeconds: 120 # Cho phép jobs hoàn thành
containers:
- name: queue-worker
image: your-registry/laravel-app:1.0.0
command:
- php
- artisan
- queue:work
- redis
- --sleep=3
- --tries=3
- --max-time=3600 # Restart worker mỗi giờ (tránh memory leaks)
- --max-jobs=1000 # Restart sau 1000 jobs
- --memory=256 # Restart nếu vượt 256MB
envFrom:
- configMapRef:
name: laravel-config
- secretRef:
name: laravel-secrets
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "1000m" # Workers cần CPU hơn web pods
memory: "512Mi"
# Workers không có HTTP — dùng exec probe
livenessProbe:
exec:
command:
- php
- -r
- "exit(0);" # Verify PHP process hoạt động
periodSeconds: 30
# Graceful shutdown: SIGTERM → worker finish current job → exit
# terminationGracePeriodSeconds=120 cho đủ thời gian
Tại sao --max-time và --max-jobs? PHP không design cho long-running processes. Memory leaks tích lũy theo thời gian. Restart định kỳ là best practice. K8s tự tạo pod mới.
Tại sao terminationGracePeriodSeconds: 120? Khi deploy mới, K8s gửi SIGTERM cho old pods. Laravel queue worker catch SIGTERM, finish current job, rồi exit. 120s cho đủ thời gian cho jobs chạy lâu.
Queue Workers Cho Nhiều Queues
# High-priority queue
- name: queue-high
command: ["php", "artisan", "queue:work", "redis", "--queue=high", "--sleep=1"]
# Default queue
- name: queue-default
command: ["php", "artisan", "queue:work", "redis", "--queue=default", "--sleep=3"]
# Low-priority queue (emails, reports)
- name: queue-low
command: ["php", "artisan", "queue:work", "redis", "--queue=low", "--sleep=10"]
Tách Deployment riêng cho mỗi priority → scale independently. High-priority 4 replicas, low-priority 1 replica.
Bước 6: Scheduler (CronJob)
# k8s/scheduler.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: laravel-scheduler
namespace: laravel-app
spec:
schedule: "* * * * *" # Mỗi phút
concurrencyPolicy: Forbid # Không run 2 instances cùng lúc
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 1 # Retry 1 lần nếu fail
activeDeadlineSeconds: 120 # Kill nếu chạy quá 2 phút
template:
spec:
containers:
- name: scheduler
image: your-registry/laravel-app:1.0.0
command: ["php", "artisan", "schedule:run"]
envFrom:
- configMapRef:
name: laravel-config
- secretRef:
name: laravel-secrets
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
restartPolicy: OnFailure
concurrencyPolicy: Forbid cực kỳ quan trọng. Nếu schedule:run mất >1 phút (do slow task), K8s sẽ KHÔNG tạo instance mới — tránh duplicate execution.
Bước 7: Auto-Scaling
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: laravel-hpa
namespace: laravel-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: laravel-app
minReplicas: 3
maxReplicas: 20
behavior:
scaleUp:
stabilizationWindowSeconds: 60 # Chờ 60s trước khi scale up thêm
policies:
- type: Pods
value: 2 # Thêm tối đa 2 pods mỗi lần
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300 # Chờ 5 phút trước khi scale down
policies:
- type: Pods
value: 1 # Giảm 1 pod mỗi lần
periodSeconds: 120
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
Behavior section kiểm soát tốc độ scale:
- Scale up: aggressive hơn (60s window, 2 pods/lần) — respond nhanh khi traffic tăng
- Scale down: conservative (300s window, 1 pod/lần) — tránh flapping khi traffic dao động
Không có behavior, HPA có thể scale from 3→20→3 trong vài phút nếu traffic spike ngắn — rất tốn resource.
Pod Disruption Budget (PDB)
# k8s/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: laravel-pdb
namespace: laravel-app
spec:
minAvailable: 2 # Luôn có ít nhất 2 pods running
selector:
matchLabels:
app: laravel
component: web
PDB bảo vệ availability khi voluntary disruptions: node drain, cluster upgrade, spot instance reclaim. K8s sẽ KHÔNG evict pod nếu vi phạm PDB.
Network Policy
# k8s/network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: laravel-network-policy
namespace: laravel-app
spec:
podSelector:
matchLabels:
app: laravel
policyTypes:
- Ingress
- Egress
ingress:
# Chỉ cho phép traffic từ Ingress controller
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- port: 80
egress:
# Cho phép kết nối MySQL, Redis
- to:
- podSelector:
matchLabels:
app: mysql
ports:
- port: 3306
- to:
- podSelector:
matchLabels:
app: redis
ports:
- port: 6379
# Cho phép DNS resolution
- to:
- namespaceSelector: {}
ports:
- port: 53
protocol: TCP
- port: 53
protocol: UDP
Network Policy = firewall ở pod level. Mặc định K8s cho phép tất cả traffic giữa pods — không an toàn. Policy này giới hạn: web pods chỉ nhận traffic từ Ingress, chỉ kết nối MySQL + Redis.
Kubectl Commands Hữu Dụng
# === Logs & Debugging ===
kubectl logs -f deployment/laravel-app -n laravel-app # Stream logs
kubectl logs deployment/laravel-app -n laravel-app --previous # Logs của pod bị crash
kubectl logs -l component=queue -n laravel-app --tail=100 # Queue worker logs
# === Interact ===
kubectl exec -it deployment/laravel-app -n laravel-app -- sh
kubectl exec -it deployment/laravel-app -n laravel-app -- php artisan migrate:status
kubectl exec -it deployment/laravel-app -n laravel-app -- php artisan tinker
# === Scaling ===
kubectl scale deployment laravel-app --replicas=5 -n laravel-app # Manual scale
kubectl get hpa -n laravel-app -w # Watch HPA decisions
# === Deployments ===
kubectl rollout status deployment/laravel-app -n laravel-app # Watch deploy progress
kubectl rollout undo deployment/laravel-app -n laravel-app # Rollback to previous
kubectl rollout history deployment/laravel-app -n laravel-app # See revision history
# === Monitoring ===
kubectl top pods -n laravel-app # CPU/Memory usage
kubectl get events -n laravel-app --sort-by='.lastTimestamp' # Recent events
kubectl describe pod <pod-name> -n laravel-app # Pod details + events
CI/CD Pipeline (GitHub Actions)
# .github/workflows/deploy.yml
name: Deploy to K8s
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build & push Docker image
run: |
docker build -t ${{ secrets.REGISTRY }}/laravel-app:${{ github.sha }} .
docker push ${{ secrets.REGISTRY }}/laravel-app:${{ github.sha }}
- name: Deploy to K8s
run: |
kubectl set image deployment/laravel-app \
laravel=${{ secrets.REGISTRY }}/laravel-app:${{ github.sha }} \
-n laravel-app
kubectl rollout status deployment/laravel-app \
-n laravel-app \
--timeout=300s
Dùng git SHA làm image tag — mỗi commit = unique image. Không dùng latest tag — K8s có thể không pull image mới nếu tag không đổi.
Checklist Production
✅ Docker
□ Multi-stage build (nhỏ, nhanh)
□ Non-root user
□ OPcache enabled + validate_timestamps=0
□ Config/route/view cached
✅ Kubernetes
□ Namespace riêng
□ 3 probes: startup, readiness, liveness
□ Resource requests + limits
□ RollingUpdate với maxUnavailable=0
□ terminationGracePeriodSeconds phù hợp
□ PDB (Pod Disruption Budget)
✅ Security
□ Secrets KHÔNG trong Git (Sealed Secrets/Vault)
□ Network Policies
□ Non-root container
□ Read-only filesystem (nếu có thể)
✅ Scaling
□ HPA với behavior config
□ Queue workers tách biệt
□ Scheduler với concurrencyPolicy: Forbid
✅ Observability
□ LOG_CHANNEL=stderr
□ Health check endpoint
□ Prometheus metrics (optional)
□ kubectl top monitoring
Kết Luận
Kubernetes cho Laravel theo pattern rõ ràng:
- Deployment (web) — Laravel app với 3 probes, graceful shutdown
- Deployment (queue) — workers riêng,
--max-timeprevent memory leaks - CronJob (scheduler) —
concurrencyPolicy: Forbid, mỗi phút - Service + Ingress — TLS, rate limiting, routing
- HPA — auto-scale với behavior controls, tránh flapping
- PDB — protect availability khi maintenance
- Network Policy — firewall level pod
- Secrets management — Sealed Secrets hoặc Vault, không commit plaintext
K8s có learning curve dốc, nhưng khi configured đúng, nó xử lý scaling, healing, và zero-downtime deploys tự động. Start simple (Deployment + Service + Ingress), thêm complexity khi cần.