• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Dockerizing and Orchestrating Legacy Laravel Systems on Modern Linode Infrastructure

Dockerizing and Orchestrating Legacy Laravel Systems on Modern Linode Infrastructure

Assessing Legacy Laravel Application Dependencies

Before embarking on the containerization journey for a legacy Laravel application, a thorough dependency audit is paramount. Legacy systems often carry implicit dependencies on specific OS packages, PHP extensions, and even subtle environmental variables that were assumed to be present. A failure to identify and replicate these in the Docker image will lead to runtime failures that are notoriously difficult to debug within a containerized environment.

Start by examining the application’s composer.json for its PHP version and required packages. Beyond that, scrutinize the server’s current PHP installation for enabled extensions. Common culprits for legacy apps include redis, memcached, imagick, gd, pdo_mysql, and intl. Additionally, check for system-level libraries required by these extensions, such as libpng-dev, libjpeg-dev, freetype-dev, and libzip-dev.

A practical approach is to run a script on the existing production server that lists all installed PHP extensions and their loaded configurations. This can be achieved with a simple PHP script:

<?php
echo "<h2>PHP Version</h2>";
echo "<p>" . phpversion() . "</p>";

echo "<h2>Loaded PHP Extensions</h2>";
echo "<ul>";
foreach (get_loaded_extensions() as $extension) {
    echo "<li>" . $extension . "</li>";
}
echo "</ul>";

echo "<h2>PHP Info (relevant sections)</h2>";
phpinfo();
?>

Analyze the output of phpinfo(), paying close attention to the “Configure Command” and “extension_dir” to understand how PHP was compiled and where extensions are loaded from. This information is critical for constructing an accurate Dockerfile.

Crafting the Dockerfile for Legacy Laravel

The Dockerfile needs to be meticulously constructed to mirror the production environment. We’ll opt for an official PHP base image, specifying a version that matches the legacy application’s requirements. For this example, let’s assume PHP 7.4.

The following Dockerfile demonstrates how to install necessary OS packages, PHP extensions, and configure PHP. It also includes steps for setting up Composer and copying the application code.

# Use an official PHP runtime as a parent image
FROM php:7.4-fpm

# Set the working directory in the container
WORKDIR /var/www/html

# Install system dependencies for PHP extensions
# Adjust packages based on your audit (e.g., libzip-dev for zip extension)
RUN apt-get update && apt-get install -y \
    git \
    unzip \
    libzip-dev \
    libpng-dev \
    libjpeg-dev \
    freetype-dev \
    libonig-dev \
    libxml2-dev \
    libssl-dev \
    acl \
    vim \
    nano \
    && rm -rf /var/lib/apt/lists/*

# Install PHP extensions
# Add or remove extensions based on your audit
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd \
    && docker-php-ext-install pdo pdo_mysql zip intl opcache exif pcntl bcmath sockets \
    && pecl install redis \
    && docker-php-ext-enable redis \
    && pecl install imagick \
    && docker-php-ext-enable imagick

# Install Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Copy the application code
# Ensure you have a .dockerignore file to exclude unnecessary files (e.g., vendor, node_modules)
COPY . /var/www/html

# Install Composer dependencies
# Use --no-dev for production builds if you have a separate build stage
RUN composer install --no-interaction --prefer-dist --optimize-autoloader

# Set permissions for storage and bootstrap/cache directories
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache \
    && chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache

# Expose port 9000 for PHP-FPM
EXPOSE 9000

# Default command to run PHP-FPM
CMD ["php-fpm"]

A crucial companion to the Dockerfile is the .dockerignore file. This prevents unnecessary files from being copied into the image, reducing build times and image size. Essential entries include:

.git
.gitignore
.env
vendor
node_modules
npm-debug.log
yarn-error.log
storage/logs/*
storage/framework/sessions/*
storage/framework/views/*
storage/framework/cache/*
bootstrap/cache/*

Containerizing the Database and Cache Services

Legacy Laravel applications typically rely on MySQL and often use Redis or Memcached for caching. These services can also be containerized, simplifying deployment and management. We’ll use docker-compose.yml to define these services.

This docker-compose.yml defines three services: app (our Laravel application), db (MySQL), and redis.

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: legacy_laravel_app
    ports:
      - "8000:80" # Map host port 8000 to container port 80 for Nginx/Apache
    volumes:
      - .:/var/www/html # Mount application code for development (remove for production builds)
    depends_on:
      - db
      - redis
    environment:
      # Ensure these match your .env file or are set appropriately
      DB_HOST: db
      DB_PORT: 3306
      DB_DATABASE: legacy_db
      DB_USERNAME: legacy_user
      DB_PASSWORD: legacy_password
      REDIS_HOST: redis
      REDIS_PORT: 6379
      APP_ENV: local # Or production
      APP_DEBUG: true # Set to false for production

  db:
    image: mysql:8.0
    container_name: legacy_laravel_db
    restart: always
    ports:
      - "3306:3306"
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root_password_here # Change this!
      MYSQL_DATABASE: legacy_db
      MYSQL_USER: legacy_user
      MYSQL_PASSWORD: legacy_password

  redis:
    image: redis:6.2
    container_name: legacy_laravel_redis
    restart: always
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  db_data:
  redis_data:

Important Considerations:

  • The volumes for the app service are commented out for production builds. For development, mounting the local code allows for live changes without rebuilding the image. For production, the code should be baked into the image during the build process.
  • Environment variables are crucial. Ensure the .env file within your application is correctly configured or that these variables are managed externally (e.g., via Kubernetes Secrets or Linode NodeBalancers).
  • Database and Redis data persistence is handled by Docker volumes. These should be managed and backed up appropriately.

Web Server Configuration (Nginx)

A separate container for a web server like Nginx is typically used to serve the Laravel application. This container will proxy requests to the PHP-FPM service running in the app container. We’ll define an Nginx service in our docker-compose.yml and create a corresponding Nginx configuration file.

First, create an Nginx configuration file (e.g., nginx/default.conf):

server {
    listen 80;
    server_name localhost; # Or your domain name

    root /var/www/html/public;
    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        # Ensure this matches the PHP-FPM service name and port in docker-compose.yml
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }

    # Optional: Add caching headers for static assets
    location ~* \.(css|js|jpg|jpeg|gif|png|ico|svg|webp)$ {
        expires 1y;
        add_header Cache-Control "public";
    }
}

Now, update your docker-compose.yml to include the Nginx service:

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: legacy_laravel_app
    # No ports exposed here for app service, Nginx will handle external access
    volumes:
      - .:/var/www/html # Mount application code for development (remove for production builds)
    depends_on:
      - db
      - redis
    environment:
      DB_HOST: db
      DB_PORT: 3306
      DB_DATABASE: legacy_db
      DB_USERNAME: legacy_user
      DB_PASSWORD: legacy_password
      REDIS_HOST: redis
      REDIS_PORT: 6379
      APP_ENV: local
      APP_DEBUG: true

  nginx:
    image: nginx:stable-alpine
    container_name: legacy_laravel_nginx
    ports:
      - "80:80" # Map host port 80 to container port 80
      - "443:443" # For HTTPS
    volumes:
      - .:/var/www/html # Mount application code (for development)
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf # Mount Nginx config
      # Add SSL certificates here for production
      # - ./ssl/your_domain.crt:/etc/nginx/ssl/your_domain.crt
      # - ./ssl/your_domain.key:/etc/nginx/ssl/your_domain.key
    depends_on:
      - app
    environment:
      # Nginx environment variables can be set here if needed

  db:
    image: mysql:8.0
    container_name: legacy_laravel_db
    restart: always
    ports:
      - "3306:3306"
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root_password_here
      MYSQL_DATABASE: legacy_db
      MYSQL_USER: legacy_user
      MYSQL_PASSWORD: legacy_password

  redis:
    image: redis:6.2
    container_name: legacy_laravel_redis
    restart: always
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  db_data:
  redis_data:

With this setup, when you run docker-compose up -d, Nginx will listen on port 80, proxying requests to the app service’s PHP-FPM on port 9000.

Deployment to Linode Kubernetes Engine (LKE)

Orchestrating these containers on a cloud platform like Linode requires a Kubernetes cluster. We’ll outline the process of deploying our Dockerized Laravel application to Linode Kubernetes Engine (LKE).

Prerequisites:

  • A running LKE cluster.
  • kubectl configured to communicate with your LKE cluster.
  • Docker images for your application, Nginx, MySQL, and Redis pushed to a container registry (e.g., Docker Hub, Linode Container Registry).

We’ll define Kubernetes manifests (YAML files) for each component.

Kubernetes Deployment for Application and Nginx

A Deployment object manages the application pods, and a Service object exposes them. For Nginx, we’ll use a Deployment and an Ingress resource to manage external access.

# app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: legacy-laravel-app
  labels:
    app: legacy-laravel
spec:
  replicas: 2 # Adjust as needed for scalability
  selector:
    matchLabels:
      app: legacy-laravel
  template:
    metadata:
      labels:
        app: legacy-laravel
    spec:
      containers:
      - name: app
        image: your-docker-registry/legacy-laravel-app:latest # Replace with your image
        ports:
        - containerPort: 9000
        env:
        - name: DB_HOST
          value: "legacy-laravel-db-service" # Kubernetes service name for DB
        - name: DB_PORT
          value: "3306"
        - name: DB_DATABASE
          value: "legacy_db"
        - name: DB_USERNAME
          value: "legacy_user"
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: legacy-laravel-secrets
              key: db-password
        - name: REDIS_HOST
          value: "legacy-laravel-redis-service" # Kubernetes service name for Redis
        - name: REDIS_PORT
          value: "6379"
        - name: APP_ENV
          value: "production"
        - name: APP_DEBUG
          value: "false"
        volumeMounts:
        - name: storage-volume
          mountPath: /var/www/html/storage
        - name: bootstrap-cache-volume
          mountPath: /var/www/html/bootstrap/cache
      volumes:
      - name: storage-volume
        persistentVolumeClaim:
          claimName: legacy-laravel-storage-pvc
      - name: bootstrap-cache-volume
        persistentVolumeClaim:
          claimName: legacy-laravel-bootstrap-cache-pvc

---
# app-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: legacy-laravel-app-service
spec:
  selector:
    app: legacy-laravel
  ports:
    - protocol: TCP
      port: 9000
      targetPort: 9000
  type: ClusterIP # Internal service, accessed by Nginx

---
# nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: legacy-laravel-nginx
  labels:
    app: legacy-laravel-nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: legacy-laravel-nginx
  template:
    metadata:
      labels:
        app: legacy-laravel-nginx
    spec:
      containers:
      - name: nginx
        image: your-docker-registry/legacy-laravel-nginx:latest # Replace with your image
        ports:
        - containerPort: 80
        - containerPort: 443
        volumeMounts:
        - name: nginx-config-volume
          mountPath: /etc/nginx/conf.d/default.conf
          subPath: default.conf # Mount specific file
        # Add volume mounts for SSL certificates if using HTTPS
        # - name: ssl-certs
        #   mountPath: /etc/nginx/ssl
      volumes:
      - name: nginx-config-volume
        configMap:
          name: legacy-laravel-nginx-config
      # - name: ssl-certs
      #   secret:
      #     secretName: legacy-laravel-ssl-certs

---
# nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: legacy-laravel-nginx-service
spec:
  selector:
    app: legacy-laravel-nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      name: http
    - protocol: TCP
      port: 443
      targetPort: 443
      name: https
  type: LoadBalancer # Expose Nginx externally via Linode Load Balancer

---
# nginx-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: legacy-laravel-nginx-config
data:
  default.conf: |
    server {
        listen 80;
        server_name your_domain.com; # Replace with your domain

        root /var/www/html/public;
        index index.php index.html index.htm;

        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }

        location ~ \.php$ {
            fastcgi_pass legacy-laravel-app-service:9000; # Use the app service name
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }

        location ~ /\.ht {
            deny all;
        }
    }

Kubernetes Deployment for Database and Redis

For stateful services like MySQL and Redis, StatefulSets are generally preferred over Deployments to ensure stable network identifiers and persistent storage. We’ll also define PersistentVolumeClaims (PVCs) for data persistence.

# db-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: legacy-laravel-db
spec:
  serviceName: legacy-laravel-db-service # Headless service for stable network IDs
  replicas: 1
  selector:
    matchLabels:
      app: legacy-laravel-db
  template:
    metadata:
      labels:
        app: legacy-laravel-db
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        ports:
        - containerPort: 3306
          name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: legacy-laravel-secrets
              key: db-root-password
        - name: MYSQL_DATABASE
          value: "legacy_db"
        - name: MYSQL_USER
          value: "legacy_user"
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: legacy-laravel-secrets
              key: db-password
        volumeMounts:
        - name: db-data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:
  - metadata:
      name: db-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi # Adjust storage size as needed

---
# db-headless-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: legacy-laravel-db-service
spec:
  selector:
    app: legacy-laravel-db
  ports:
    - protocol: TCP
      port: 3306
      targetPort: 3306
  clusterIP: None # Headless service

---
# redis-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: legacy-laravel-redis
spec:
  serviceName: legacy-laravel-redis-service
  replicas: 1
  selector:
    matchLabels:
      app: legacy-laravel-redis
  template:
    metadata:
      labels:
        app: legacy-laravel-redis
    spec:
      containers:
      - name: redis
        image: redis:6.2
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - name: redis-data
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: redis-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi # Adjust storage size as needed

---
# redis-headless-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: legacy-laravel-redis-service
spec:
  selector:
    app: legacy-laravel-redis
  ports:
    - protocol: TCP
      port: 6379
      targetPort: 6379
  clusterIP: None # Headless service

Kubernetes Secrets

Sensitive information like database passwords should be stored in Kubernetes Secrets. Create a secret manifest:

# legacy-laravel-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: legacy-laravel-secrets
type: Opaque
data:
  db-root-password: YOUR_ROOT_PASSWORD_BASE64 # Base64 encoded
  db-password: YOUR_DB_PASSWORD_BASE64     # Base64 encoded

To encode your passwords, use the base64 command: echo -n 'your_password' | base64. Apply these manifests using kubectl apply -f <filename.yaml>.

Finalizing and Monitoring

After deploying all components to LKE, thoroughly test the application. Monitor logs for both the application and Nginx pods using kubectl logs <pod-name>. For production environments, consider setting up Prometheus and Grafana for more robust monitoring of resource utilization and application performance.

Containerizing and orchestrating legacy Laravel applications on modern infrastructure like Linode provides significant benefits in terms of scalability, resilience, and manageability. The key is a meticulous approach to dependency mapping and a clear understanding of both Docker and Kubernetes concepts.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Go Goroutines vs. Node.js Event Loop: Scaling I/O-Bound Microservices Under High Load
  • Elixir Phoenix vs. Go Gin: Concurrency Models and Fault Tolerance Under Peak Request Volume
  • Python Celery vs. Go Channels: Distributed Task Queue Overhead and Memory Reliability
  • Scala Pekko vs. Go Goroutines: Actor Model vs. CSP for Event-Driven Reactive Systems
  • Java Loom Virtual Threads vs. Go Goroutines: Under-the-Hood Scheduler and Thread Overhead Comparison

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (584)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (806)
  • PHP (5)
  • PHP Development (21)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (19)
  • Ruby on Rails (1)
  • Security & Compliance (543)
  • SEO & Growth (491)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (7)
  • WordPress Theme Development (357)

Recent Posts

  • Go Goroutines vs. Node.js Event Loop: Scaling I/O-Bound Microservices Under High Load
  • Elixir Phoenix vs. Go Gin: Concurrency Models and Fault Tolerance Under Peak Request Volume
  • Python Celery vs. Go Channels: Distributed Task Queue Overhead and Memory Reliability

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (806)
  • Debugging & Troubleshooting (584)
  • Security & Compliance (543)
  • SEO & Growth (491)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala