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

Vengala Vinay

Having 9+ Years of Experience in Software Development

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

Dockerizing and Orchestrating Legacy Laravel Systems on Modern DigitalOcean Infrastructure

Assessing Legacy Laravel Applications for Containerization

Before diving into Dockerfiles and orchestration, a thorough assessment of the legacy Laravel application is paramount. This isn’t just about identifying dependencies; it’s about understanding the application’s architecture, its state management, and its interaction with external services. Many legacy systems were built with assumptions about the underlying operating system and filesystem that don’t translate directly to a containerized, ephemeral environment.

Key areas to scrutinize include:

  • File System Operations: Applications that heavily rely on direct file system manipulation (e.g., writing logs to specific paths, creating temporary files in predictable locations, or expecting persistent storage at the root level) will require careful handling. Docker containers are typically stateless, and persistent data should be managed via volumes or bind mounts.
  • Database Connections: How are database credentials managed? Are they hardcoded, read from environment variables, or fetched from a configuration file? Containerized applications strongly favor environment variables for configuration.
  • External Service Integrations: Identify all external APIs, message queues, caching layers, and other services the application communicates with. These will need to be accessible from within the Docker network or exposed externally.
  • Background Jobs/Cron Jobs: Laravel’s queue system and scheduled tasks (cron jobs) need special consideration. These often run as separate processes and will require their own container definitions or a dedicated orchestration strategy.
  • Session Management: If the application uses file-based sessions, this will not work reliably in a stateless container. Migration to a shared session driver like Redis or Memcached is often necessary.
  • Asset Compilation: Laravel Mix or other asset compilation processes might be part of the build pipeline, not necessarily a runtime requirement within the container itself, but their output needs to be correctly handled.

Crafting the Dockerfile for a Legacy Laravel App

The Dockerfile is the blueprint for your container image. For a legacy Laravel application, we’ll aim for a multi-stage build to keep the final image lean and secure. This involves using a builder stage with development dependencies and then copying only the necessary artifacts to a production-ready runtime stage.

Let’s assume a typical Laravel structure with Composer for dependency management and potentially Node.js for asset compilation.

Base Image and Builder Stage

We’ll start with a PHP-FPM image, as it’s designed for serving web applications. The builder stage will install Composer and Node.js.

# Stage 1: Builder
FROM php:8.1-fpm as builder

# Install system dependencies
RUN apt-get update && apt-get install -y \
    git \
    unzip \
    libzip-dev \
    libpng-dev \
    libjpeg-dev \
    libfreetype6-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd \
    && docker-php-ext-install pdo pdo_mysql zip exif pcntl opcache

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Install Node.js and npm (for asset compilation if needed)
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
    && apt-get install -y nodejs \
    && npm install -g npm@latest

# Set working directory
WORKDIR /var/www/html

# Copy application files
COPY . .

# Install Composer dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction

# Compile assets (if applicable)
# RUN npm install && npm run build

Runtime Stage

The runtime stage will be based on a lean PHP-FPM image, copying only the compiled application code and necessary configurations.

# Stage 2: Runtime
FROM php:8.1-fpm

# Install system dependencies for runtime
RUN apt-get update && apt-get install -y \
    libzip4 \
    libpng-dev \
    libjpeg-dev \
    libfreetype6 \
    libonig5 \
    libxml2 \
    zip \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd \
    && docker-php-ext-install pdo pdo_mysql zip exif pcntl opcache

# Copy application files from builder stage
COPY --from=builder /var/www/html /var/www/html

# Copy compiled assets from builder stage (if applicable)
# COPY --from=builder /var/www/html/public/build /var/www/html/public/build

# Ensure correct permissions for storage and bootstrap/cache
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

# Expose port
EXPOSE 9000

# Set entrypoint (optional, can be handled by orchestration)
# CMD ["php-fpm"]

Configuring Nginx as a Reverse Proxy

Nginx will serve as the reverse proxy, handling incoming HTTP requests and forwarding them to the PHP-FPM container. We’ll use a separate Nginx container for better separation of concerns and easier configuration management.

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$ {
        # Ensure the fastcgi_pass directive points to the correct PHP-FPM service name and port
        # This will be defined in your Docker Compose file.
        fastcgi_pass php-fpm:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

    # Deny access to hidden files
    location ~ /\.ht {
        deny all;
    }

    # Cache static assets for a year
    location ~* \.(css|js|jpg|jpeg|gif|png|ico|svg|webp|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public";
    }
}

This Nginx configuration assumes a service named php-fpm is available on port 9000, which will be defined in our Docker Compose file.

Orchestrating with Docker Compose

Docker Compose is ideal for defining and running multi-container Docker applications. It allows us to define our services (Laravel app, Nginx, database, Redis, etc.), networks, and volumes in a single YAML file.

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: legacy_laravel_app
    restart: unless-stopped
    env_file:
      - .env # Load environment variables from .env file
    volumes:
      - ./storage:/var/www/html/storage # Persist storage data
      - ./bootstrap/cache:/var/www/html/bootstrap/cache # Persist cache data
    networks:
      - app-network

  nginx:
    image: nginx:latest
    container_name: legacy_laravel_nginx
    restart: unless-stopped
    ports:
      - "80:80" # Map host port 80 to container port 80
      - "443:443" # Map host port 443 to container port 443 (for SSL)
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf # Mount custom Nginx config
      - ./public:/var/www/html/public # Mount public directory for Nginx to serve static files
      # Add SSL certificates if using HTTPS
      # - ./certs/your_domain.crt:/etc/nginx/ssl/your_domain.crt
      # - ./certs/your_domain.key:/etc/nginx/ssl/your_domain.key
    depends_on:
      - app
    networks:
      - app-network

  # Example for a database service (e.g., MySQL)
  # db:
  #   image: mysql:8.0
  #   container_name: legacy_laravel_db
  #   restart: unless-stopped
  #   environment:
  #     MYSQL_ROOT_PASSWORD: your_root_password
  #     MYSQL_DATABASE: your_database_name
  #     MYSQL_USER: your_database_user
  #     MYSQL_PASSWORD: your_database_password
  #   volumes:
  #     - db_data:/var/lib/mysql
  #   networks:
  #     - app-network

  # Example for a Redis service
  # redis:
  #   image: redis:alpine
  #   container_name: legacy_laravel_redis
  #   restart: unless-stopped
  #   volumes:
  #     - redis_data:/data
  #   networks:
  #     - app-network

networks:
  app-network:
    driver: bridge

# Define named volumes for data persistence
# volumes:
#   db_data:
#   redis_data:

Key considerations for the docker-compose.yml:

  • app service: This service builds the Laravel application image using the Dockerfile defined in the current directory. It loads environment variables from a .env file and mounts the storage and bootstrap/cache directories to ensure persistence.
  • nginx service: This service uses the official Nginx image. It maps host ports 80 and 443 to the container. Crucially, it mounts our custom nginx.conf and the application’s public directory. The depends_on ensures the Nginx container starts after the app container, though it doesn’t guarantee the app is ready to serve.
  • Database and Redis: Uncomment and configure the db and redis services if your application relies on them. Ensure the .env file contains the correct credentials for these services, using the service names (e.g., DB_HOST=db, REDIS_HOST=redis).
  • Networks: A custom bridge network (app-network) is created to allow containers to communicate with each other using their service names.
  • Volumes: Named volumes (commented out in the example) are recommended for databases and Redis to manage data persistence. For the Laravel app, we’re using bind mounts for storage and bootstrap/cache, which is common for development and simpler deployments. For production, named volumes might offer better management.

Deploying to DigitalOcean Droplets

With the Docker and Docker Compose setup ready, deploying to DigitalOcean involves provisioning a Droplet, installing Docker and Docker Compose, and then running your application.

Provisioning the Droplet

Choose a Droplet size that suits your application’s needs. A general-purpose Droplet with at least 2GB of RAM is a good starting point for a moderately trafficked legacy application. Ensure you select a recent Ubuntu LTS version (e.g., 22.04 LTS).

Installing Docker and Docker Compose

Connect to your Droplet via SSH and run the following commands to install Docker and Docker Compose:

# Update package list
sudo apt-get update

# Install Docker CE
sudo apt-get install -y \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io

# Add your user to the docker group to run docker commands without sudo
sudo usermod -aG docker $USER
newgrp docker # Apply group changes to the current session

# Install Docker Compose (v2 is recommended and integrated with Docker CLI)
# If you need standalone v1, use: sudo apt-get install docker-compose
sudo apt-get install docker-compose-plugin -y

After running these commands, you might need to log out and log back in for the group changes to take effect.

Deploying the Application

Transfer your application code, Dockerfile, docker-compose.yml, and nginx.conf to your Droplet. A common approach is to use git clone if your code is in a repository, or scp for manual transfers.

# Navigate to your application directory
cd /path/to/your/laravel/app

# Ensure your .env file is correctly configured for the production environment
# This includes database credentials, app key, etc.
# Example:
# APP_ENV=production
# APP_KEY=base64:...
# DB_HOST=db # If using a db service in docker-compose
# DB_PORT=3306
# DB_DATABASE=your_db_name
# DB_USERNAME=your_db_user
# DB_PASSWORD=your_db_password

# Build and start the containers
docker compose up --build -d

# Check the status of your containers
docker compose ps

# View logs for debugging
docker compose logs -f app
docker compose logs -f nginx

The --build flag ensures that your Docker image is built from the Dockerfile. The -d flag runs the containers in detached mode (in the background).

Handling Legacy Challenges and Best Practices

Legacy applications often present unique challenges when moving to containers. Here are some common issues and how to address them:

Environment Variable Management

Hardcoded configurations are a major anti-pattern. Ensure all environment-specific settings (database credentials, API keys, mail configurations) are managed via the .env file and loaded into the container. For sensitive information, consider using Docker secrets or a dedicated secrets management tool in more complex orchestrations.

Persistent Storage

As mentioned, storage and bootstrap/cache directories must be persistent. Using bind mounts (as shown in docker-compose.yml) is straightforward. For production, consider using named volumes managed by Docker, which can offer better performance and easier backup strategies.

Background Jobs and Cron Jobs

Legacy applications might rely on system cron jobs. In a Dockerized environment, these should be managed within the application container or, preferably, as separate worker containers. You can define a separate service in docker-compose.yml for a queue worker:

# ... inside docker-compose.yml
  queue_worker:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: legacy_laravel_queue_worker
    restart: unless-stopped
    env_file:
      - .env
    volumes:
      - ./storage:/var/www/html/storage
      - ./bootstrap/cache:/var/www/html/bootstrap/cache
    command: php artisan queue:work --tries=3 --timeout=300 # Adjust command as needed
    depends_on:
      - app # Or directly on db/redis if needed
      # - db
      # - redis
    networks:
      - app-network

For system cron jobs, you can either install cron within your PHP container and configure it, or use a dedicated cron container that triggers commands in your application container via docker exec. However, the Laravel scheduler with a single cron entry pointing to schedule:run is the idiomatic Laravel approach.

Database Migrations

Database migrations need to be run after the database is available and the application container is ready. You can automate this by adding a command to your application service’s entrypoint script or by running it manually after deployment:

# Run migrations manually after starting services
docker compose exec app php artisan migrate --force

For automated migration runs, consider a dedicated migration job in your orchestration or a script that waits for the database to be ready before executing the migration command.

Monitoring and Logging

With containers, logs are typically sent to standard output (stdout) and standard error (stderr). Docker Compose collects these logs. For production, integrate with a centralized logging solution like ELK stack, Graylog, or cloud-native logging services on DigitalOcean. Monitoring container health, resource usage, and application performance is crucial. Tools like Prometheus and Grafana, or DigitalOcean’s monitoring features, can be employed.

Scaling and Advanced Considerations

Once the legacy application is containerized and running on a single Droplet, scaling becomes the next logical step. DigitalOcean offers several ways to achieve this:

Horizontal Scaling with Docker Swarm or Kubernetes

For true horizontal scaling, a single Droplet with Docker Compose is insufficient. You’ll need an orchestration platform:

  • Docker Swarm: Simpler to set up than Kubernetes. You can create a Swarm cluster across multiple Droplets. Your docker-compose.yml can often be deployed directly to Swarm with minor modifications.
  • Kubernetes: The industry standard for container orchestration. DigitalOcean offers managed Kubernetes (DOKS), which significantly simplifies cluster management. You would translate your docker-compose.yml into Kubernetes manifests (Deployments, Services, Ingress, etc.).

In a scaled environment, you’ll need to consider:

  • Load Balancing: DigitalOcean Load Balancers can distribute traffic across multiple instances of your Nginx service.
  • Shared Storage: For stateful applications or shared file access, consider distributed file systems like Ceph or managed object storage.
  • Database Scaling: A single database instance will become a bottleneck. Explore read replicas, sharding, or managed database services (like DigitalOcean Managed Databases).
  • Caching: A distributed cache like Redis or Memcached is essential for performance at scale.

Serverless and Managed Services

For certain parts of a legacy application, or if refactoring is an option, consider migrating them to serverless functions (e.g., DigitalOcean Functions) or managed services. This can reduce operational overhead and improve scalability for specific tasks.

CI/CD Pipeline Integration

Automate your build, test, and deployment process. Integrate Docker image building and deployment into your CI/CD pipeline using tools like GitLab CI, GitHub Actions, Jenkins, or DigitalOcean’s own CI/CD features. This ensures consistent and reliable deployments.

Conclusion

Containerizing and orchestrating legacy Laravel applications on modern infrastructure like DigitalOcean is a powerful strategy for improving reliability, scalability, and manageability. By carefully assessing the application, crafting robust Dockerfiles and Compose configurations, and leveraging DigitalOcean’s services, you can successfully modernize even older systems, paving the way for future enhancements and a more resilient architecture.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Step-by-Step: Diagnosing thread pools deadlock during concurrent ActiveRecord transaction processing on Linode Servers
  • Securing Your E-commerce APIs: Preventing SQL Injection (SQLi) in customized checkout queries in WooCommerce Implementations
  • Disaster Recovery 101: Architecting Auto-Failovers for MySQL and Ruby Deployments on Linode
  • High-Throughput Caching Strategies: Scaling MySQL for Perl Application APIs
  • Disaster Recovery 101: Architecting Auto-Failovers for DynamoDB and Laravel Deployments on DigitalOcean

Copyright © 2026 · Vinay Vengala