Deploy a PHP Service to Kubernetes

Khimananda Oli 8 min read Programming and Languages
Deploy a PHP Service to Kubernetes

By Khimananda Oli | Last reviewed: August 2026

Moving from traditional VPS hosting to containers introduces specific challenges when you deploy a PHP service to Kubernetes. Unlike stateless Go or Node.js microservices, PHP applications typically require a two-process architecture (Nginx and PHP-FPM), persistent storage for uploads, and careful environment variable injection. This guide provides the exact configuration patterns I use in production to ensure reliability, security, and performance for Laravel and Symfony workloads on modern clusters.

How do you architect a container to deploy a PHP service to Kubernetes?

The most common failure mode when teams first deploy a PHP service to Kubernetes is treating the application like a single binary. PHP requires a web server (Nginx/Apache) to handle static assets and proxy dynamic requests to the PHP-FPM process manager. In a containerized environment, you have two primary architectural choices: separate containers per pod or a unified supervisor-managed container.

For most teams, especially those transitioning from traditional LEMP stack setups, the unified approach reduces operational complexity. Running Nginx and PHP-FPM in the same pod eliminates network latency between processes and simplifies scaling logic. However, this requires a proper init system or supervisor to manage both processes gracefully.

Kubernetes Pod Architecture for PHPIngress ControllerTLS Termination + RoutingApplication PodNginxPort 8080PHP-FPMUnix Socket / Port 9000Shared Volume (PVC)/var/www/html/storage & uploads
Single-pod architecture for deploying PHP services with shared storage and internal process communication

Multi-stage Dockerfile for production

Avoid using the official php:apache image for production. It lacks optimization and includes unnecessary modules. Instead, use a multi-stage build that compiles only required extensions and configures Nginx as a reverse proxy. This pattern aligns with best practices covered in containerizing Laravel apps.

# Stage 1: Install dependencies
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --ignore-platform-reqs

# Stage 2: Production image
FROM php:8.4-fpm-alpine AS base
RUN apk add --no-cache nginx supervisor \
    && docker-php-ext-install pdo_mysql opcache bcmath \
    && rm -rf /var/cache/apk/*

COPY --from=vendor /app/vendor /var/www/html/vendor
COPY . /var/www/html
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/

RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
EXPOSE 8080
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

This Dockerfile produces an immutable artifact. Dependencies are cached in a separate layer, and the final image contains only runtime requirements. Always run composer install without --dev flags to reduce attack surface and image size.

What Kubernetes manifests are needed to deploy a PHP service?

Once your image is built and pushed to a registry, you need three core resources: a Deployment, a Service, and an Ingress. The Deployment manages pod replicas and rollout strategy; the Service provides stable internal DNS; and the Ingress handles external traffic routing with TLS.

Deployment with health checks and resource limits

PHP-FPM workers consume memory proportional to request complexity. Without explicit resource limits and requests, pods will be evicted unpredictably during traffic spikes. Configure liveness probes against a lightweight health endpoint rather than the homepage to avoid false positives during cache warming.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: php-app
  template:
    metadata:
      labels:
        app: php-app
    spec:
      containers:
      - name: php-app
        image: registry.example.com/php-app:v1.2.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: "250m"
            memory: "256Mi"
          limits:
            cpu: "1000m"
            memory: "512Mi"
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 15
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
        envFrom:
        - secretRef:
            name: php-app-secrets
        volumeMounts:
        - name: storage
          mountPath: /var/www/html/storage/app/public
      volumes:
      - name: storage
        persistentVolumeClaim:
          claimName: php-app-storage

Note the separation of liveness and readiness probes. The readiness probe gates traffic during deployments, while the liveness probe restarts hung processes. For Laravel applications, create dedicated routes at /healthz and /readyz that check database connectivity and cache availability without triggering heavy middleware.

Ingress configuration with TLS

External access requires an Ingress resource. Most managed clusters (EKS, GKE, AKS) provide default controllers, but verify compatibility with your chosen Ingress controller. Always enforce HTTPS redirects and specify TLS certificates managed by cert-manager or cloud-native integrations.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: php-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
  - hosts:
    - app.example.com
    secretName: php-app-tls
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: php-app
            port:
              number: 80
Request Lifecycle & Health Check FlowClientIngressNginxPHP-FPMDB/CacheHealth Probe Sequence1. Kubelet → /readyz (gates traffic)2. Kubelet → /healthz (restarts pod if failed)3. Probes bypass auth middleware4. Return 200 OK within 2s timeout
Request routing and health probe sequence for PHP deployments on Kubernetes

How should secrets and environment variables be managed securely?

Never bake .env files into Docker images or commit them to version control. When you deploy a PHP service to Kubernetes, use native Secrets objects injected as environment variables or mounted files. For SOC 2 or ISO 27001 compliance, integrate with external secret stores like HashiCorp Vault or AWS Secrets Manager via the External Secrets Operator.

Creating and referencing secrets

Store sensitive values like APP_KEY, database passwords, and API tokens in Kubernetes Secrets. Encode values in base64 or use stringData for plaintext input that Kubernetes encodes automatically.

apiVersion: v1
kind: Secret
metadata:
  name: php-app-secrets
type: Opaque
stringData:
  APP_KEY: "base64:your-app-key-here"
  DB_PASSWORD: "super-secure-password"
  REDIS_PASSWORD: "redis-auth-token"
  MAIL_USERNAME: "[email protected]"

Reference these in your Deployment using envFrom for bulk injection or individual env.valueFrom entries for selective mapping. Rotate secrets regularly and audit access logs. For deeper guidance, review Kubernetes secrets management done right.

What are common pitfalls when running PHP on Kubernetes?

PHP's synchronous execution model and reliance on filesystem state create unique challenges in distributed environments. Understanding these failure modes prevents production incidents during peak traffic.

PitfallSymptomSolution
Missing persistent storageUploaded files disappear after pod restartUse PVC with ReadWriteMany or object storage (S3)
Opcache misconfigurationCode changes not reflected; high CPU usageEnable opcache.validate_timestamps=0 in prod; use graceful reload
Session stored locallyUsers logged out randomly across podsConfigure Redis/database session driver; never use file driver
No graceful shutdown502 errors during deploymentsAdd preStop hook; configure PHP-FPM process_control_timeout
Insufficient FPM workersRequest queuing; high latency under loadTune pm.max_children based on memory limits; monitor with Prometheus

Handling sessions and file uploads

Kubernetes pods are ephemeral. Local filesystem writes for sessions, caches, or user uploads will be lost during scaling events or node drains. Configure your application to use external stores:

  • Sessions: Use Redis or database drivers. Never rely on file session storage in multi-pod deployments.
  • Uploads: Offload to S3/GCS/R2 via SDK, or mount a shared PVC using NFS/EFS/Ceph. For Nepal-based deployments with limited bandwidth, consider regional object storage endpoints to reduce latency.
  • Caches: Use Redis/Memcached for route, config, and view caches. Avoid file-based caching entirely.

Graceful shutdown and zero-downtime deploys

PHP-FPM does not handle SIGTERM gracefully by default. During rolling updates, in-flight requests may be terminated abruptly, causing 502 errors. Add a preStop hook to allow active connections to complete:

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 5; kill -QUIT $(cat /run/php-fpm.pid)"]

The sleep allows the load balancer to remove the pod from endpoints before PHP-FPM begins shutting down. Combine this with progressive delivery strategies for critical production services.

State Management: Anti-pattern vs Best Practice❌ Anti-pattern: Local StatePod Asessions/ (local)Pod Bsessions/ (local)User hits Pod A → Session createdNext request → Pod B → Logged out!Pod restart → All uploads lost✅ Best Practice: External StatePod APod BRedis / S3Shared sessions & uploadsAny pod can serve any requestState survives pod restarts
Why external state stores are mandatory when you deploy a PHP service to Kubernetes

Deploy a PHP Service to Kubernetes: Next Steps

Successfully running PHP on Kubernetes requires attention to process management, state externalization, and observability. Start with the unified Nginx+PHP-FPM container pattern, enforce resource boundaries, and validate health probes before promoting to production. Monitor FPM worker saturation and request latency using Prometheus metrics exported via php-fpm-exporter or OpenTelemetry instrumentation.

If your team needs help designing compliant, scalable PHP infrastructure or migrating legacy applications to Kubernetes, reach out for a consultation. I assist organizations in Nepal and globally with audit-ready deployments, performance tuning, and secure CI/CD pipelines tailored to PHP workloads.

Frequently Asked Questions

Use PHP-FPM with Nginx as a sidecar or separate container. This separates concerns, allows independent scaling of web and application layers, and follows current production standards for PHP workloads on Kubernetes clusters running stable v1.35 or later.

Never use local filesystem sessions. Configure Redis or Memcached via environment variables in your Deployment manifest. This ensures session persistence across pod restarts and horizontal scaling events without sticky sessions or shared volume complexity.

No. Kubernetes requires containerized workloads. You must build a Docker image containing your Laravel code, dependencies, and PHP-FPM configuration before creating Deployment and Service resources for cluster scheduling.

Start with 256Mi memory request and 512Mi limit, plus 250m CPU request and 500m limit. Monitor actual usage with Prometheus and adjust based on pm.max_children settings and average request memory consumption to prevent OOM kills.

Use an initContainer or a Kubernetes Job that runs php artisan migrate before the main deployment proceeds. This prevents race conditions where multiple pods attempt migrations simultaneously during rolling updates.

Exit code 137 indicates OOMKilled status. Your pod exceeded its memory limit. Increase the memory limit in your resource specifications or optimize PHP-FPM pm.max_children to match available container memory allocation.

Create an Ingress resource using NGINX Ingress Controller or Traefik. Define host-based routing rules pointing to your PHP Service. Avoid NodePort or LoadBalancer services directly for production HTTP traffic management.

Implement a dedicated /health endpoint returning HTTP 200 with minimal overhead. Configure livenessProbe and readinessProbe in your Deployment spec to use this endpoint, ensuring Kubernetes only routes traffic to healthy PHP-FPM processes.

Store non-sensitive config in ConfigMaps and secrets in Secrets objects. Mount them as environment variables or files. Never bake environment values into container images; use kustomize overlays or Helm values for per-environment customization.

Yes, initially. Kubernetes adds control plane overhead and requires at least three nodes for high availability. Costs justify when you need auto-scaling, zero-downtime deployments, or multi-service orchestration beyond simple PHP hosting.

Check if PHP-FPM is listening on the expected socket or port. Verify Nginx proxy_pass configuration matches. Inspect pod logs with kubectl logs and test connectivity using kubectl exec to curl localhost from inside the container.

Both work. Helm suits teams managing multiple environments with templated charts. Kustomize fits simpler workflows using base manifests with overlay patches. Choose based on team familiarity and whether you need parameterized releases versus patch-based configuration.

Configure S3, GCS, or Azure Blob Storage as your upload destination using Flysystem or similar libraries. Never store uploads on ephemeral pod filesystems. Use presigned URLs for direct browser-to-storage uploads to reduce PHP memory pressure.

Use PHP 8.4 or later. Older versions lack performance improvements and security patches. Ensure your base image uses official php-fpm Alpine or Debian variants with minimal attack surface and up-to-date system libraries.

Set terminationGracePeriodSeconds to 30 or higher in your Deployment spec. Configure PHP-FPM process_control_timeout and implement SIGTERM handling. This allows in-flight requests to complete before Kubernetes forcefully terminates the pod during scale-down operations.