
August 14, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Moving from traditional VPS or shared hosting to container orchestration is a significant architectural shift, and Kubernetes for Laravel getting started requires understanding both PHP runtime specifics and cluster networking. While many tutorials focus on generic Node.js examples, Laravel applications have unique requirements around writable storage, queue workers, cron scheduling, and OPcache invalidation that directly impact your manifest design. This guide bridges that gap by providing battle-tested configurations specifically validated for Laravel 12.x on PHP 8.4 in production environments during 2026.
How do you structure a production-ready Dockerfile for Laravel on Kubernetes?
The foundation of any successful modern Laravel architecture on Kubernetes is an optimized container image. Unlike simple development setups, production images must minimize attack surface, reduce startup time, and handle immutable infrastructure patterns correctly. I've seen too many teams ship bloated images containing Composer dev dependencies or missing critical PHP extensions, leading to runtime failures that only surface after deployment.
# Dockerfile - Optimized for Laravel 12.x on PHP 8.4
FROM php:8.4-fpm-alpine AS base
RUN apk add --no-cache \
nginx supervisor libpng-dev libjpeg-turbo-dev freetype-dev \
oniguruma-dev libzip-dev redis-dev icu-dev postgresql-dev
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
pdo_mysql pdo_pgsql mbstring zip exif pcntl bcmath opcache redis intl
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
COPY docker/php/www.conf /usr/local/etc/php-fpm.d/www.conf
FROM composer:2.7 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --ignore-platform-reqs
FROM node:22-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json vite.config.js ./
RUN npm ci
COPY resources ./resources
RUN npm run build
FROM base AS runtime
WORKDIR /var/www/html
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
COPY . .
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
COPY docker/nginx/default.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
EXPOSE 8080
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] This multi-stage approach keeps your final image under 200MB while ensuring all compiled assets and production-only Composer packages are present. The supervisord process manager runs both Nginx and PHP-FPM within a single pod, which simplifies networking but requires careful health check configuration. For higher traffic applications, consider separating Nginx into its own sidecar container to allow independent scaling of the web server layer.
What Kubernetes resources does a Laravel application actually need?
A common mistake when approaching Kubernetes for Laravel getting started is treating the application as a single monolithic deployment. In practice, Laravel applications consist of at least three distinct workload types: HTTP request handling, asynchronous queue processing, and scheduled task execution. Each has fundamentally different scaling characteristics, resource profiles, and failure modes. Bundling them together creates operational headaches that compound as traffic grows.
| Resource Type | Purpose | Scaling Strategy | Storage Need | Health Check |
|---|---|---|---|---|
| Deployment (Web) | HTTP requests via PHP-FPM | HPA on CPU/RPS metrics | ReadOnly root + RW uploads | /up endpoint (Laravel 11+) |
| Deployment (Queue) | Background job processing | KEDA on queue depth or manual replicas | Shared session/cache optional | Process liveness only |
| CronJob | Scheduler every minute | Fixed schedule, concurrency forbidden | None typically | Completion timeout |
| PersistentVolumeClaim | User uploads, temp files | N/A - provisioned once | ReadWriteMany required | N/A |
| ConfigMap / Secret | .env values, DB credentials | Updated via rollout restart | N/A | N/A |
The separation between web and queue deployments is non-negotiable for production systems. Queue workers can consume significant memory during large imports or PDF generation, and you don't want that starving your HTTP response handlers. On projects like Nepal Gift Card, where digital product delivery involves async license key generation, isolating queue workers prevented checkout timeouts during peak load. Similarly, running php artisan schedule:run inside your web pods leads to duplicate executions during horizontal scaling; always use a dedicated CronJob resource with concurrencyPolicy: Forbid.
How do you handle persistent storage and file uploads in Kubernetes?
Storage is where most Laravel teams hit their first major wall with Kubernetes. Unlike stateless APIs, Laravel applications frequently write to storage/app/public for user uploads, generated reports, or cached views. Containers are ephemeral by design, meaning any file written to the local filesystem disappears when the pod restarts. For database-driven website development in Nepal projects involving document-heavy legal portals or e-commerce product catalogs, solving this reliably is essential before going live.
You have two viable paths. The first uses a ReadWriteMany (RWX) PersistentVolumeClaim backed by NFS, Amazon EFS, CephFS, or Longhorn. This allows all pods to read and write the same directory simultaneously. The second, often superior path for cloud deployments, configures Laravel's filesystem driver to use S3, Google Cloud Storage, or Azure Blob directly, eliminating shared state entirely. I strongly prefer the object storage approach for new projects because it removes an entire class of distributed filesystem bugs and scales without provisioning larger volumes.
# k8s/pvc.yaml - Only needed if NOT using S3/GCS
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: laravel-storage
spec:
accessModes:
- ReadWriteMany # CRITICAL: Not ReadWriteOnce
storageClassName: efs-sc # or nfs-client, longhorn
resources:
requests:
storage: 20Gi
# In your Deployment spec:
volumes:
- name: storage
persistentVolumeClaim:
claimName: laravel-storage
containers:
- name: laravel
volumeMounts:
- name: storage
mountPath: /var/www/html/storage/app/public
subPath: public
- name: storage
mountPath: /var/www/html/storage/framework/sessions
subPath: sessions If you choose the object storage route, set FILESYSTEM_DISK=s3 in your ConfigMap and ensure your IAM roles or service accounts grant appropriate bucket permissions. Run php artisan storage:link during container build, not at runtime, since the symlink target won't exist on ephemeral containers. For local development parity, use MinIO or LocalStack to emulate S3 behavior without touching production infrastructure.
How should environment variables and secrets be managed securely?
Laravel's .env file pattern doesn't translate directly to Kubernetes. Hardcoding secrets in manifests commits credentials to version control, while mounting entire .env files as volumes prevents granular updates. The correct approach splits configuration into ConfigMaps for non-sensitive values and Secrets for credentials, then injects them as environment variables or mounted files depending on sensitivity. When working as a Laravel developer in Nepal serving international clients, maintaining this separation ensures compliance expectations are met regardless of where the cluster runs.
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: laravel-config
data:
APP_NAME: "My Application"
APP_ENV: "production"
APP_DEBUG: "false"
APP_URL: "https://example.com"
LOG_CHANNEL: "stderr" # Critical for kubectl logs
CACHE_STORE: "redis"
QUEUE_CONNECTION: "redis"
SESSION_DRIVER: "redis"
FILESYSTEM_DISK: "s3"
---
# k8s/secret.yaml (create via CLI, never commit plaintext)
# kubectl create secret generic laravel-secrets \
# --from-literal=APP_KEY='base64:...' \
# --from-literal=DB_PASSWORD='...' \
# --from-literal=REDIS_PASSWORD='...' \
# --from-literal=AWS_SECRET_ACCESS_KEY='...' Always set LOG_CHANNEL=stderr in Kubernetes environments. Laravel's default stack channel writes to files inside the container, which vanish on restart and aren't captured by cluster logging solutions like Fluent Bit or Loki. Writing to standard error ensures logs flow through kubectl and your observability stack. Similarly, prefer Redis over database or file drivers for cache, sessions, and queues; these stateful components perform poorly when backed by shared filesystems or add unnecessary database load.
What deployment strategy prevents downtime during Laravel releases?
Zero-downtime deployments require coordinating three factors: graceful PHP-FPM shutdown, asset versioning, and database migration timing. Laravel's Vite-manifest approach handles frontend assets well when combined with proper cache headers, but backend code changes need careful orchestration. On sister sites sharing Deployer 7 pipelines, we solved this with symlink swaps; Kubernetes achieves equivalent safety through rolling updates with configured surge and unavailability parameters.
# k8s/deployment-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-web
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0 # Zero downtime guarantee
template:
spec:
terminationGracePeriodSeconds: 30
containers:
- name: laravel
image: registry.example.com/laravel-app:v1.2.4
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: laravel-config
- secretRef:
name: laravel-secrets
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
readinessProbe:
httpGet:
path: /up
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /up
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi The preStop hook sleeping five seconds is crucial. Kubernetes removes the pod from service endpoints and sends SIGTERM nearly simultaneously, but kube-proxy rules propagate asynchronously. Without this delay, in-flight requests may still route to a terminating pod, causing connection resets. Laravel 11+'s built-in /up endpoint provides ideal health checks; for older versions, create a simple controller returning 200 OK. Database migrations should run in an initContainer or separate Job resource before the main deployment rolls out, preventing race conditions where new code queries columns that don't yet exist.
Getting Started with Kubernetes for Laravel: Next Steps
Successfully adopting Kubernetes for Laravel getting started means accepting that operational complexity increases even as scalability improves. Begin with a managed Kubernetes offering like DigitalOcean DOKS, AWS EKS, or Google GKE rather than self-managing etcd and control planes; the monthly cost difference (~Rs 8,000–15,000 NPR / $60–110 USD) buys enormous reliability and frees you to focus on application concerns. Validate your Docker builds locally with Docker Compose before pushing to a cluster, and invest early in CI/CD pipelines that automate image tagging and manifest updates.
Monitor aggressively from day one using Prometheus and Grafana or managed alternatives. Track PHP-FPM active processes, queue lag, p95 latency, and OOM kill rates—these signals reveal problems before users notice. If your team lacks dedicated DevOps capacity, consider whether a well-configured Platform-as-a-Service like Laravel Vapor or Render might deliver similar benefits with less overhead. Kubernetes earns its keep at scale, but premature orchestration distracts from shipping features that grow your business.
Ready to architect your Laravel infrastructure properly? Reach out to discuss your deployment needs and get a tailored assessment of whether Kubernetes, serverless, or traditional hosting best serves your current stage and growth trajectory.

