Kubernetes Cho Laravel: Từ Docker Đến Production K8s

· 14 min read

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:run mỗ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/lock trướ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--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:

  1. Deployment (web) — Laravel app với 3 probes, graceful shutdown
  2. Deployment (queue) — workers riêng, --max-time prevent memory leaks
  3. CronJob (scheduler)concurrencyPolicy: Forbid, mỗi phút
  4. Service + Ingress — TLS, rate limiting, routing
  5. HPA — auto-scale với behavior controls, tránh flapping
  6. PDB — protect availability khi maintenance
  7. Network Policy — firewall level pod
  8. 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.

Bình luận