Business & Tech

Migrating a PHP/MySQL Website into Docker Containers: A Step-by-Step Guide

Sonam Lama

Administrator

If you’re still running that PHP website on a traditional LAMP stack setup, you’re not alone. Thousands of websites across Nepal and beyond are built this way, from small business sites in Biratnagar to complex web applications serving clients in Kathmandu. But as your business grows and deployment becomes more complex, you’ve probably started hearing about Docker and wondering if it’s time to make the leap.

docker-container

Here’s the thing: migrating to Docker isn’t just about following trends. It’s about solving real problems that every developer faces like inconsistent environments, deployment headaches, scaling issues, and that dreaded “it works on my machine” syndrome. Today, we’re going to walk through exactly how to containerize your PHP/MySQL application, step by step, with all the real-world gotchas I’ve learned from migrating dozens of sites.

Why Migrate to Docker Container? The Real Benefits

Before we dive into the technical stuff, let’s talk about why this migration makes sense in 2025:

Development Consistency: No more “install Apache, PHP 8.1, MySQL 5.7, configure virtual hosts…” on every new developer’s machine. One docker-compose up and everyone’s running identical environments.

Easy Deployment: Deploy the same containers to staging and production. No more environment-specific bugs that only show up when you go live.

Version Management: Need to test PHP 8.2? Just change one line in your Dockerfile. Want to rollback? Easy.

Resource Efficiency: Containers share the host OS kernel, using fewer resources than full virtual machines.

Scalability: When your business grows (and it will!), scaling individual services becomes much simpler.

Understanding Your Current Setup

Most traditional PHP/MySQL sites follow this pattern:

ComponentTraditional SetupDocker Equivalent
Web ServerApache/Nginx on hostNginx container
PHPmod_php or PHP-FPMPHP-FPM container
DatabaseMySQL/MariaDB on hostMySQL container
File StorageDirect filesystemDocker volumes
ConfigurationMultiple config filesEnvironment variables + configs

The key insight is that we’re not changing your application’s logic, we’re just packaging it differently.

Pre-Migration Assessment

Before touching any code, let’s audit what you’re working with:

1. Document Your Current Environment

# Check your current versions
php -v
mysql --version
apache2 -v  # or nginx -v

# List installed PHP extensions
php -m

# Check database size and structure
mysql -u root -p -e "SELECT table_schema AS 'Database', 
    ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)' 
    FROM information_schema.tables 
    GROUP BY table_schema;"

2. Identify Dependencies

  • What PHP extensions does your app use?
  • Are there any custom Apache/Nginx configurations?
  • Do you have any cron jobs running?
  • Are there file upload directories that need persistence?
  • Any external service connections that need special network setup?

3. Code Audit Checklist

  • Hardcoded localhost database connections
  • File paths that assume specific directory structures
  • Sessions stored in files vs database
  • Any absolute paths in your code
  • Mail server configurations

Step-by-Step Migration Process

Step 1: Project Structure Setup

Let’s create a clean Docker structure for your existing project:

your-website/
├── docker-compose.yml
├── docker/
│   ├── nginx/
│   │   ├── Dockerfile
│   │   └── default.conf
│   ├── php/
│   │   ├── Dockerfile
│   │   └── php.ini
│   └── mysql/
│       └── init/
│           └── 01-create-database.sql
├── src/  (your existing PHP files go here)
├── data/  (for persistent MySQL data)
└── logs/  (for application logs)

Step 2: Creating the Docker Compose File

This is the heart of your containerized setup:

version: '3.8'

services:
  nginx:
    build:
      context: ./docker/nginx
      dockerfile: Dockerfile
    container_name: ${PROJECT_NAME}-nginx
    ports:
      - "${NGINX_PORT:-80}:80"
      - "${NGINX_SSL_PORT:-443}:443"
    volumes:
      - ./src:/var/www/html
      - ./logs/nginx:/var/log/nginx
    depends_on:
      - php
    networks:
      - app-network
    restart: unless-stopped

  php:
    build:
      context: ./docker/php
      dockerfile: Dockerfile
    container_name: ${PROJECT_NAME}-php
    volumes:
      - ./src:/var/www/html
      - ./docker/php/php.ini:/usr/local/etc/php/php.ini
      - ./logs/php:/var/log/php
    environment:
      - DB_HOST=mysql
      - DB_PORT=3306
      - DB_NAME=${DB_NAME:-your_database}
      - DB_USER=${DB_USER:-your_user}
      - DB_PASS=${DB_PASS:-your_password}
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - app-network
    restart: unless-stopped

  mysql:
    image: mysql:8.0
    container_name: ${PROJECT_NAME}-mysql
    ports:
      - "${MYSQL_PORT:-3306}:3306"
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS:-rootpassword}
      MYSQL_DATABASE: ${DB_NAME:-your_database}
      MYSQL_USER: ${DB_USER:-your_user}
      MYSQL_PASSWORD: ${DB_PASS:-your_password}
    volumes:
      - ./data/mysql:/var/lib/mysql
      - ./docker/mysql/init:/docker-entrypoint-initdb.d
      - ./logs/mysql:/var/log/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      timeout: 20s
      retries: 10
    networks:
      - app-network
    restart: unless-stopped

  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    container_name: ${PROJECT_NAME}-phpmyadmin
    ports:
      - "${PMA_PORT:-8080}:80"
    environment:
      PMA_HOST: mysql
      PMA_PORT: 3306
      PMA_USER: ${DB_USER:-your_user}
      PMA_PASSWORD: ${DB_PASS:-your_password}
    depends_on:
      - mysql
    networks:
      - app-network
    restart: unless-stopped

volumes:
  mysql_data:

networks:
  app-network:
    driver: bridge

Step 3: PHP Container Configuration

Create docker/php/Dockerfile:

FROM php:8.1-fpm

# Install system dependencies
RUN apt-get update && apt-get install -y \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    libzip-dev \
    zip \
    unzip \
    && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip

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

# Set working directory
WORKDIR /var/www/html

# Copy custom PHP configuration
COPY php.ini /usr/local/etc/php/

# Create user for running the application
RUN groupadd -g 1000 www && \
    useradd -u 1000 -ms /bin/bash -g www www

# Change ownership of working directory
RUN chown -R www:www /var/www/html

# Switch to user www
USER www

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]

Step 4: Nginx Configuration

Create docker/nginx/Dockerfile:

FROM nginx:alpine

# Copy custom nginx config
COPY default.conf /etc/nginx/conf.d/

# Expose port 80
EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

And docker/nginx/default.conf:

server {
    listen 80;
    server_name localhost;
    root /var/www/html;
    index index.php index.html index.htm;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;

    # Handle static files
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

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

    # PHP files processing
    location ~ \.php$ {
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        
        # Security: Don't execute PHP files in uploads directory
        location ~* /uploads/.*\.php$ {
            return 403;
        }
    }

    # Security: Deny access to sensitive files
    location ~ /\. {
        deny all;
        return 404;
    }

    location ~ /(README|CHANGELOG|composer\.(json|lock))$ {
        deny all;
        return 404;
    }
}

Step 5: Environment Configuration

Create a .env file in your project root:

PROJECT_NAME=your-website
NGINX_PORT=80
NGINX_SSL_PORT=443
MYSQL_PORT=3306
PMA_PORT=8080

DB_HOST=mysql
DB_NAME=your_database_name
DB_USER=your_db_user
DB_PASS=your_secure_password
DB_ROOT_PASS=your_root_password

Updating Your PHP Application

Now comes the important part, updating your existing PHP code to work with containers:

Database Connection Updates

Before (traditional):

<?php
$host = 'localhost';
$dbname = 'your_database';
$username = 'your_user';
$password = 'your_password';

try {
    $pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
} catch(PDOException $e) {
    die('Connection failed: ' . $e->getMessage());
}
?>

After (Docker):

<?php
$host = $_ENV['DB_HOST'] ?? 'mysql';
$dbname = $_ENV['DB_NAME'] ?? 'your_database';
$username = $_ENV['DB_USER'] ?? 'your_user';
$password = $_ENV['DB_PASS'] ?? 'your_password';

try {
    $pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
    error_log('Database connection failed: ' . $e->getMessage());
    die('Database connection failed');
}
?>

File Upload Handling

Make sure your upload directories are properly mapped:

<?php
// Use relative paths or environment variables
$upload_dir = $_ENV['UPLOAD_DIR'] ?? './uploads/';

// Ensure directory exists
if (!is_dir($upload_dir)) {
    mkdir($upload_dir, 0755, true);
}

// Your upload logic here
?>

Data Migration Strategy

Method 1: Direct Database Import

For smaller databases (under 1GB):

# Export from your current server
mysqldump -u root -p your_database > backup.sql

# Start your Docker services
docker-compose up -d

# Import into containerized MySQL
docker-compose exec mysql mysql -u root -p your_database < backup.sql

Method 2: Live Migration for Large Databases

For bigger databases or minimal downtime:

  1. Set up replication from your current MySQL to the containerized one
  2. Test thoroughly with the replica
  3. Switch DNS/traffic when ready
  4. Stop replication and update configurations

File Migration

# Copy your existing website files to the src directory
cp -r /var/www/html/* ./src/

# Fix permissions
sudo chown -R $USER:$USER ./src
chmod -R 755 ./src

Testing Your Containerized Application

Start Your Services

# Build and start all services
docker-compose up -d --build

# Check service status
docker-compose ps

# View logs
docker-compose logs -f php
docker-compose logs -f nginx
docker-compose logs -f mysql

Validation Checklist

Test CaseCommand/ActionExpected Result
Web AccessVisit http://localhostSite loads correctly
Database ConnectionCheck app functionalityNo DB errors
PHP InfoCreate phpinfo.php pagePHP version and extensions correct
File UploadsTest upload functionalityFiles saved correctly
Session HandlingLogin/logout functionalitySessions work properly
Error HandlingCheck logs directoryErrors logged appropriately

Common Migration Issues and Solutions

Issue 1: Permission Problems

Symptom: “Permission denied” errors for file operations

Solution:

# Fix container user permissions
docker-compose exec php chown -R www:www /var/www/html
docker-compose exec nginx chown -R nginx:nginx /var/log/nginx

Issue 2: Database Connection Timeouts

Symptom: “Connection timeout” or “Can’t connect to MySQL”

Solution: Add health checks and proper service dependencies (already included in our docker-compose.yml)

Issue 3: PHP Extensions Missing

Symptom: “Call to undefined function” errors

Solution: Add missing extensions to your PHP Dockerfile:

RUN docker-php-ext-install mysqli pdo_mysql gd zip curl xml

Issue 4: Session Storage Issues

Symptom: Users getting logged out randomly 

Solution: Use database sessions or shared session storage:

ini_set('session.save_handler', 'files');
ini_set('session.save_path', '/tmp/sessions');

Performance Optimization

PHP-FPM Tuning

Update your docker/php/php.ini:
; Performance settings
memory_limit = 256M
max_execution_time = 300
max_input_time = 300
post_max_size = 64M
upload_max_filesize = 32M

; OPcache settings
opcache.enable = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 4000
opcache.validate_timestamps = 0
opcache.revalidate_freq = 0

MySQL Configuration

Add to your docker-compose.yml MySQL service:

mysql:
  # ... other configuration
  command: >
    --innodb-buffer-pool-size=256M
    --innodb-log-file-size=64M
    --max-connections=100
    --query-cache-type=1
    --query-cache-size=32M

Nginx Optimization

Update your nginx configuration:

# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;

# Enable HTTP/2
listen 443 ssl http2;

# Connection keep-alive
keepalive_timeout 30;
keepalive_requests 100;

Monitoring and Maintenance

Health Monitoring Script

Create scripts/health-check.sh:
#!/bin/bash

# Check if all services are running
services=("nginx" "php" "mysql")

for service in "${services[@]}"; do
    if docker-compose ps $service | grep -q "Up"; then
        echo "✓ $service is running"
    else
        echo "✗ $service is down"
        docker-compose logs $service
    fi
done

# Check disk usage
echo "Docker disk usage:"
docker system df

# Check database connections
echo "MySQL connections:"
docker-compose exec mysql mysql -u root -p${DB_ROOT_PASS} -e "SHOW PROCESSLIST;"

Backup Strategy

#!/bin/bash
# backup.sh

DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="./backups/$DATE"

mkdir -p $BACKUP_DIR

# Backup database
docker-compose exec mysql mysqldump -u root -p${DB_ROOT_PASS} ${DB_NAME} > $BACKUP_DIR/database.sql

# Backup application files
tar -czf $BACKUP_DIR/files.tar.gz ./src

# Keep only last 7 days of backups
find ./backups -name "20*" -type d -mtime +7 -exec rm -rf {} \;

echo "Backup completed: $BACKUP_DIR"

Deployment to Production

Using Docker Compose in Production

Update your production docker-compose.yml:

# Add to your services
services:
  nginx:
    # ... existing config
    restart: always
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
  
  php:
    # ... existing config  
    restart: always
    environment:
      - APP_ENV=production
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Security Hardening

  1. Use specific image tags instead of latest
  2. Run containers as non-root users (already configured)
  3. Limit container resources:
    deploy:  resources:    limits:      memory: 512M      cpus: 0.5
  4. Use secrets for sensitive data instead of environment variables
  5. Regular security updates for base images

Scaling Considerations

When your containerized application needs to handle more traffic:

Horizontal Scaling

# Scale PHP workers
docker-compose up -d --scale php=3

# Use a load balancer (nginx upstream)
upstream php_backend {
    server php_1:9000;
    server php_2:9000;
    server php_3:9000;
}

Database Scaling

Consider read replicas or database clustering:

mysql-master:
  image: mysql:8.0
  environment:
    - MYSQL_REPLICATION_MODE=master
    
mysql-slave:
  image: mysql:8.0
  environment:
    - MYSQL_REPLICATION_MODE=slave
    - MYSQL_MASTER_HOST=mysql-master

Conclusion

Migrating your PHP/MySQL website to Docker containers might seem daunting at first, but the benefits are worth it. You get consistent environments, easier deployments, better resource utilization, and a foundation that scales with your business.

container

The key is to take it step by step. Don’t try to migrate everything at once, start with a development environment, test thoroughly, then gradually move to staging and production. Document everything you do, because you’ll likely be helping other developers in Nepal make the same transition.

Remember that containerization is not just a one-time migration, it’s a new way of thinking about application deployment and management. The initial investment in learning and setup pays dividends in reduced deployment stress, faster development cycles, and more reliable applications.

Whether you’re running a small business website in Pokhara or managing enterprise applications in Kathmandu, Docker gives you the tools to deploy confidently and scale effectively. The future of web development is containerized, and now you’re ready to be part of it.

At Nest Nepal, we’ve helped dozens of businesses make this transition successfully. The combination of proper planning, thorough testing, and gradual migration ensures that your website keeps running smoothly while you gain all the benefits of modern containerized deployment.

Ready to make the leap? Start with a development environment, follow this guide step by step, and don’t hesitate to reach out when you need help. Your future self will thank you for making the move to Docker! 🐳

Need assistance migrating your PHP/MySQL application to Docker? Our experienced team at Nest Nepal can help you plan, execute, and optimise your containerization strategy. Contact us to learn more about our migration services.

Leave a Reply

Your email address will not be published. Required fields are marked *