Dockerizing and Orchestrating Legacy Shopify Systems on Modern Linode Infrastructure
Deconstructing the Legacy Shopify Monolith for Containerization
Many established e-commerce businesses find themselves with a “legacy” Shopify system. This often means a monolithic application, tightly coupled with custom themes, numerous third-party apps (some of which might be poorly architected or have limited API access), and potentially a complex, un-containerized backend infrastructure. The goal is to migrate this to a modern, scalable, and manageable environment on Linode using Docker and orchestration. This isn’t a simple lift-and-shift; it requires a strategic decomposition.
The first step is to identify the core components that can be containerized. For a typical Shopify setup, this includes:
- The Shopify application itself (often a PHP-based monolith, though Shopify’s core is SaaS, we’re talking about the custom code, themes, and potentially backend services that interact with it).
- Database (e.g., MySQL, PostgreSQL).
- Caching layers (e.g., Redis, Memcached).
- Background job processors (e.g., Sidekiq for Ruby, or custom PHP queues).
- Static asset serving (often handled by Shopify CDN, but custom assets might need a dedicated solution).
- Any custom microservices or APIs that the Shopify store relies on.
For this exercise, we’ll assume a common scenario: a PHP-based custom backend service that interacts with Shopify’s API, a MySQL database, and a Redis cache. We’ll aim to containerize these and orchestrate them using Docker Compose for initial deployment on Linode.
Crafting Dockerfiles for Core Components
We’ll start by creating Dockerfiles for each distinct service. This ensures isolation and reproducibility.
Dockerfile for the Custom PHP Backend Service
This Dockerfile will build an image for our custom PHP application. We’ll use an official PHP-FPM image as a base, install necessary extensions, copy our application code, and configure Nginx to serve it.
# Dockerfile for PHP Backend Service
FROM php:8.2-fpm
# Install system dependencies and PHP extensions
RUN apt-get update && apt-get install -y \
git \
unzip \
libzip-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libonig-dev \
libxml2-dev \
libssl-dev \
libcurl4-openssl-dev \
libicu-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 intl opcache \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy application code
COPY . /var/www/html
# Install Composer dependencies
RUN composer install --no-dev --optimize-autoloader
# Expose port
EXPOSE 9000
# Default command to run PHP-FPM
CMD ["php-fpm"]
This Dockerfile assumes your PHP application is structured to run with PHP-FPM. You’ll need to adjust the `COPY . /var/www/html` line if your application code resides in a subdirectory.
Dockerfile for Nginx (to serve PHP and static assets)
A separate Nginx container is crucial for efficient static asset serving and proxying PHP requests to the PHP-FPM container.
# Dockerfile for Nginx FROM nginx:stable-alpine # Remove default Nginx configuration RUN rm /etc/nginx/conf.d/default.conf # Copy custom Nginx configuration COPY nginx.conf /etc/nginx/conf.d/default.conf # Copy static assets (if any are managed outside Shopify CDN) # COPY html /usr/share/nginx/html # Expose port EXPOSE 80
The `nginx.conf` file will be critical. Here’s a sample:
# nginx.conf
server {
listen 80;
server_name your-domain.com; # Replace with your actual domain
root /var/www/html/public; # Adjust if your public directory is different
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php-fpm-service:9000; # Service name from docker-compose
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to hidden files
location ~ /\.ht {
deny all;
}
# Cache static assets for a year
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public";
}
}
Note the `fastcgi_pass php-fpm-service:9000;` line. `php-fpm-service` is the name we’ll assign to our PHP-FPM container in `docker-compose.yml`. This is how containers on the same Docker network communicate.
Docker Compose for Orchestration
Docker Compose is ideal for defining and running multi-container Docker applications. It allows us to configure our services, networks, and volumes in a single YAML file.
# docker-compose.yml
version: '3.8'
services:
php-fpm-service:
build:
context: ./php-app # Directory containing your PHP Dockerfile
dockerfile: Dockerfile
container_name: php_fpm_app
volumes:
- ./php-app:/var/www/html # Mount your PHP app code for development/debugging
# For production, you might want to copy code during build and not mount
networks:
- app-network
depends_on:
- mysql
- redis
nginx-service:
image: nginx:stable-alpine
container_name: nginx_webserver
ports:
- "80:80" # Map host port 80 to container port 80
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf # Mount custom Nginx config
- ./php-app/public:/var/www/html/public # Mount public directory for Nginx
# If you have static assets outside public, mount them here
networks:
- app-network
depends_on:
- php-fpm-service
mysql-db:
image: mysql:8.0
container_name: mysql_database
environment:
MYSQL_ROOT_PASSWORD: your_strong_root_password # CHANGE THIS
MYSQL_DATABASE: shopify_db
MYSQL_USER: shopify_user
MYSQL_PASSWORD: your_strong_db_password # CHANGE THIS
volumes:
- mysql_data:/var/lib/mysql # Persist database data
networks:
- app-network
redis-cache:
image: redis:latest
container_name: redis_cache
ports:
- "6379:6379" # Optional: expose Redis to host for debugging
volumes:
- redis_data:/data # Persist Redis data
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
mysql_data:
redis_data:
Key considerations in this `docker-compose.yml`:
- `build` context for `php-fpm-service`: Points to the directory containing the PHP Dockerfile.
- Volumes for PHP app: The `./php-app:/var/www/html` mount is useful for development. In a production CI/CD pipeline, you’d typically build the code into the image during the `docker build` step and omit this host mount for better performance and security.
- `ports` for `nginx-service`: Exposes port 80 on the Linode host to the Nginx container.
- `depends_on`: Ensures services start in a logical order (e.g., database before application). Note that `depends_on` only guarantees start order, not readiness. For robust applications, you’d implement health checks or retry mechanisms.
- `networks`: All containers are on a custom bridge network (`app-network`), allowing them to communicate using their service names.
- `volumes` for persistence: `mysql_data` and `redis_data` ensure that your database and cache data survive container restarts or removals.
Deployment on Linode
Once you have your Dockerfiles and `docker-compose.yml` ready, deploying to Linode is straightforward.
1. Provision a Linode Instance
Choose a Linode instance size appropriate for your expected load. A general-purpose instance with sufficient RAM and CPU should suffice for a moderately trafficked legacy Shopify store. Ensure you have SSH access configured.
2. Install Docker and Docker Compose
Connect to your Linode instance via SSH and install Docker and Docker Compose. The exact commands can vary slightly based on your chosen Linux distribution (e.g., Ubuntu, Debian).
# Install Docker (example for Ubuntu)
sudo apt-get update
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 immediately
# Install Docker Compose (check for the latest version on GitHub releases)
LATEST_COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d\" -f4)
sudo curl -L "https://github.com/docker/compose/releases/download/${LATEST_COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
3. Deploy Your Application
Transfer your application code, Dockerfiles, and `docker-compose.yml` to your Linode instance. A common method is using `git clone` if your code is in a repository, or `scp`/`rsync` for direct file transfer.
# Navigate to your project directory on Linode cd /path/to/your/project # Build and start the containers docker-compose up -d
The `-d` flag runs the containers in detached mode (in the background). You can check the status of your containers with `docker ps` and view logs with `docker logs [container_name]`.
Scaling and Advanced Considerations
While Docker Compose is excellent for development and small-scale deployments, for true production scalability and high availability, you’ll want to move to a more robust orchestration platform.
1. Linode Kubernetes Engine (LKE)
For larger deployments, Linode Kubernetes Engine (LKE) is the natural next step. You would convert your `docker-compose.yml` into Kubernetes manifests (Deployments, Services, StatefulSets, PersistentVolumeClaims).
# Example Kubernetes Deployment for PHP-FPM (simplified)
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-fpm-app
spec:
replicas: 3 # Scale this number
selector:
matchLabels:
app: php-fpm
template:
metadata:
labels:
app: php-fpm
spec:
containers:
- name: php-fpm
image: your-dockerhub-repo/php-app:latest # Your built image
ports:
- containerPort: 9000
volumeMounts:
- name: app-code
mountPath: /var/www/html
volumes:
- name: app-code
persistentVolumeClaim:
claimName: php-app-pvc # Assuming a PVC for code if not baked into image
---
# Example Kubernetes Service for PHP-FPM
apiVersion: v1
kind: Service
metadata:
name: php-fpm-service
spec:
selector:
app: php-fpm
ports:
- protocol: TCP
port: 9000
targetPort: 9000
type: ClusterIP # Internal service
---
# Example Kubernetes Deployment for Nginx
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-webserver
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:stable-alpine
ports:
- containerPort: 80
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf # Mount specific file
- name: app-public-assets
mountPath: /var/www/html/public
volumes:
- name: nginx-config
configMap:
name: nginx-configmap # A ConfigMap holding nginx.conf
- name: app-public-assets
persistentVolumeClaim:
claimName: php-app-pvc # Assuming same PVC for public assets
---
# Example Kubernetes Service for Nginx (Load Balancer)
apiVersion: v1
kind: Service
metadata:
name: nginx-ingress
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancer # Linode will provision a Load Balancer
You would also need Kubernetes `StatefulSet` for MySQL and `Deployment` for Redis, along with appropriate `PersistentVolumeClaims` for data persistence. A ConfigMap would hold your `nginx.conf`.
2. Database and Cache Management
For production, consider using managed database services (like Linode’s managed MySQL) or more robust self-hosted solutions. For caching, Redis is a good choice, but ensure you have a strategy for persistence and high availability if needed.
3. CI/CD Pipeline
Automate your build, test, and deployment process. Tools like GitLab CI, GitHub Actions, or Jenkins can be integrated with Linode to automatically build Docker images, push them to a registry (like Docker Hub or Linode Container Registry), and deploy them to your LKE cluster or even update your Docker Compose setup.
4. Monitoring and Logging
Implement comprehensive monitoring (e.g., Prometheus, Grafana) and centralized logging (e.g., ELK stack, Loki) to gain visibility into your application’s performance and health. Linode’s monitoring tools can provide infrastructure-level insights.
By containerizing your legacy Shopify system and orchestrating it on Linode, you gain significant advantages in terms of scalability, reliability, and manageability, paving the way for future modernization efforts.