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.

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:
| Component | Traditional Setup | Docker Equivalent |
| Web Server | Apache/Nginx on host | Nginx container |
| PHP | mod_php or PHP-FPM | PHP-FPM container |
| Database | MySQL/MariaDB on host | MySQL container |
| File Storage | Direct filesystem | Docker volumes |
| Configuration | Multiple config files | Environment 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:
- Set up replication from your current MySQL to the containerized one
- Test thoroughly with the replica
- Switch DNS/traffic when ready
- 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 Case | Command/Action | Expected Result |
| Web Access | Visit http://localhost | Site loads correctly |
| Database Connection | Check app functionality | No DB errors |
| PHP Info | Create phpinfo.php page | PHP version and extensions correct |
| File Uploads | Test upload functionality | Files saved correctly |
| Session Handling | Login/logout functionality | Sessions work properly |
| Error Handling | Check logs directory | Errors 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
- Use specific image tags instead of latest
- Run containers as non-root users (already configured)
- Limit container resources:
deploy: resources: limits: memory: 512M cpus: 0.5 - Use secrets for sensitive data instead of environment variables
- 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.

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.