Production Docker Migration Guide¶
This guide provides step-by-step instructions for migrating the production Uplink server from pipenv to Docker with uv, solving environment versioning issues and providing a consistent, reproducible production environment.
Current State:
- Production uses pipenv for dependency management
- Commands run via pipenv run ./manage.py
- Direct systemd services (uplink, uplink-daphne, uplink-huey)
- Local MySQL and Redis installations
- Python 3.9.10 managed by pyenv
- Cron jobs use pipenv
Target State:
- Production uses Docker Compose with uv inside containers
- All services containerized (web, daphne, huey, mysql, redis)
- Commands run via docker compose exec web python manage.py
- Environment consistency between dev and production
- Cron jobs run inside containers
- Easy rollback and version control
Estimated Migration Time: 2-3 hours
Estimated Downtime: 10-15 minutes (for final cutover)
Table of Contents¶
- Pre-Migration Checklist
- Phase 1: Docker Setup (No Downtime)
- Phase 2: Data Migration (No Downtime)
- Phase 3: Final Cutover (Downtime)
- Verification
- Rollback Procedure
- Post-Migration Notes
Pre-Migration Checklist¶
1. Backup Current State¶
# Create backups directory if it doesn't exist
mkdir -p /srv/uplink/backups
# Backup database locally
cd /srv/uplink/app
mysqldump -u uplink_user -p uplink > /srv/uplink/backups/uplink_db_backup_$(date +%Y%m%d_%H%M%S).sql
# Backup media files locally
tar -czf /srv/uplink/backups/media_backup_$(date +%Y%m%d_%H%M%S).tar.gz /srv/uplink/app/media/
# Backup current .env file
cp /srv/uplink/app/.env /srv/uplink/backups/.env.backup.$(date +%Y%m%d)
# Save current crontab
crontab -l > /srv/uplink/backups/crontab.backup.$(date +%Y%m%d)
# Save systemd service files
sudo cp /etc/systemd/system/uplink*.service /srv/uplink/backups/
# Verify backups completed successfully
ls -lh /srv/uplink/backups/
2. Test Docker Setup Locally¶
Before migrating production:
- ✅ Docker setup works on your development machine
- ✅ All services run correctly in Docker
- ✅ Database migrations work in Docker
- ✅ PrestaShop integration works from Docker
- ✅ WebSocket connections (Daphne) functional
- ✅ Background tasks (Huey) processing correctly
- ✅ Review docker-compose.yml and docker-compose.prod.yml
3. Schedule Maintenance Window¶
- Choose low-traffic period (e.g., weekend evening or early morning)
- Plan for 2-3 hours total (most work done before downtime)
- Final cutover: 10-15 minutes downtime
- Notify team and stakeholders
- Have rollback plan ready
4. Verify Current System State¶
# Check current Python version
python --version
# Check current services status
sudo systemctl status uplink
sudo systemctl status uplink-daphne
sudo systemctl status uplink-huey
sudo systemctl status mysql
sudo systemctl status redis
# Check disk space (Docker needs space for images/volumes)
df -h
# Ensure at least 10GB free space
# Check current MySQL data location
sudo mysql -e "SHOW VARIABLES LIKE 'datadir';"
# Note current MySQL port
sudo netstat -tlnp | grep mysql
5. Server Requirements Check¶
# Verify Docker can be installed (Ubuntu 20.04+, Debian 10+)
lsb_release -a
# Check RAM (recommend 4GB+ for all containers)
free -h
# Check CPU
lscpu | grep "CPU(s)"
Phase 1: Docker Setup (No Downtime)¶
This phase installs Docker and prepares the environment while current services keep running.
Step 1.1: Install Docker and Docker Compose¶
# Update package index
sudo apt-get update
# Install prerequisites
sudo apt-get install -y ca-certificates curl gnupg lsb-release
# Add Docker's official GPG key
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Set up Docker repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Verify Docker installation
sudo docker --version
sudo docker compose version
# Add current user to docker group (to run without sudo)
sudo usermod -aG docker $USER
# Apply group changes (or logout/login)
newgrp docker
# Test Docker
docker run hello-world
Step 1.2: Pull Latest Code with Docker Configuration¶
cd /srv/uplink/app
# Ensure you're on the correct branch
git fetch origin
git checkout 1130-dockerise-uplink-and-switch-to-using-uv
git pull origin 1130-dockerise-uplink-and-switch-to-using-uv
# Verify Docker files exist
ls -l Dockerfile docker-compose.yml docker-compose.prod.yml .dockerignore
Step 1.3: Configure Production Environment Variables¶
cd /srv/uplink/app
# Create production .env file (based on .env.production.example)
# Copy your current .env and update for Docker
# Key differences for Docker:
# - DATABASE_HOST=db (not 127.0.0.1)
# - REDIS_HOST=redis (not 127.0.0.1)
# - DATABASE_PORT=3306 (internal port, not external)
# - HUEY_IMMEDIATE=False
# - SECRET_KEY with escaped $ signs ($$)
# Example production .env updates:
cat >> .env << 'EOF'
# Docker-specific overrides
DATABASE_HOST=db
DATABASE_PORT=3306
REDIS_HOST=redis
REDIS_PORT=6379
HUEY_IMMEDIATE=False
EOF
# Verify .env has correct values
grep -E "DATABASE_HOST|REDIS_HOST" .env
Step 1.4: Build Docker Images¶
cd /srv/uplink/app
# Build production images (uses Dockerfile + docker-compose.prod.yml)
docker compose -f docker-compose.yml -f docker-compose.prod.yml build
# This will take 5-10 minutes first time
# Verify images built
docker images | grep uplink
Phase 2: Data Migration (No Downtime)¶
This phase prepares Docker containers and migrates data while current services continue running.
Step 2.1: Start Docker Services (Without Web/Daphne/Huey)¶
We'll start MySQL and Redis first to test connectivity.
cd /srv/uplink/app
# Start only db and redis (not web services yet)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d db redis
# Check containers running
docker compose ps
# Check logs
docker compose logs db
docker compose logs redis
# Verify MySQL container is healthy
docker compose exec db mysql -u root -p$MYSQL_ROOT_PASSWORD -e "SELECT 1;"
Step 2.2: Migrate Database to Docker MySQL¶
Option A: Fresh Database (Recommended for First Test)
cd /srv/uplink/app
# Create new database in Docker MySQL
docker compose exec db mysql -u root -p$MYSQL_ROOT_PASSWORD -e "CREATE DATABASE IF NOT EXISTS uplink CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
docker compose exec db mysql -u root -p$MYSQL_ROOT_PASSWORD -e "CREATE USER IF NOT EXISTS 'uplink_user'@'%' IDENTIFIED BY 'your_password';"
docker compose exec db mysql -u root -p$MYSQL_ROOT_PASSWORD -e "GRANT ALL PRIVILEGES ON uplink.* TO 'uplink_user'@'%';"
docker compose exec db mysql -u root -p$MYSQL_ROOT_PASSWORD -e "FLUSH PRIVILEGES;"
# Import from backup
docker compose exec -T db mysql -u uplink_user -p uplink < /srv/uplink/backups/uplink_db_backup_YYYYMMDD_HHMMSS.sql
# Verify data imported
docker compose exec db mysql -u uplink_user -p uplink -e "SHOW TABLES;"
docker compose exec db mysql -u uplink_user -p uplink -e "SELECT COUNT(*) FROM catalogue_product;"
Option B: Live Database Replication (Advanced)
For zero-downtime migration, set up MySQL replication from current MySQL to Docker MySQL.
# This is more complex - consider Option A for first migration
# Replication setup is beyond scope of this guide
# See: https://dev.mysql.com/doc/refman/8.0/en/replication.html
Step 2.3: Test Database Connection from Container¶
cd /srv/uplink/app
# Start a temporary web container to test
docker compose -f docker-compose.yml -f docker-compose.prod.yml run --rm web python manage.py check
# Test database connection
docker compose -f docker-compose.yml -f docker-compose.prod.yml run --rm web python manage.py dbshell
# Run migrations (should show all applied already if data imported)
docker compose -f docker-compose.yml -f docker-compose.prod.yml run --rm web python manage.py migrate
# Test a query
docker compose -f docker-compose.yml -f docker-compose.prod.yml run --rm web python manage.py shell -c "from catalogue.models import Product; print(Product.objects.count())"
Step 2.4: Migrate Media Files to Docker Volume¶
cd /srv/uplink/app
# Media files will be mounted from host to container
# Ensure media directory has correct permissions
sudo chown -R www-data:www-data media/
# The docker-compose.yml should already have:
# volumes:
# - ./media:/app/media
# Verify media volume mount
docker compose -f docker-compose.yml -f docker-compose.prod.yml run --rm web ls -la /app/media/
Phase 3: Final Cutover (Downtime)¶
⏱️ Downtime begins here (10-15 minutes)
Step 3.1: Stop Current Services¶
# Stop all current services
sudo systemctl stop uplink
sudo systemctl stop uplink-daphne
sudo systemctl stop uplink-huey
# Verify services stopped
sudo systemctl status uplink
sudo systemctl status uplink-daphne
sudo systemctl status uplink-huey
Step 3.2: Final Database Sync (If Using Live Data)¶
If you're migrating live data that changed since Phase 2:
# Quick dump of current production database
mysqldump -u uplink_user -p uplink > /tmp/final_sync.sql
# Import into Docker MySQL
docker compose exec -T db mysql -u uplink_user -p uplink < /tmp/final_sync.sql
# Clean up
rm /tmp/final_sync.sql
Step 3.3: Start All Docker Services¶
cd /srv/uplink/app
# Start all services with production config
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Check all containers running
docker compose ps
# All services should show "healthy" or "running"
# Expected: db, redis, web, daphne, huey
Step 3.4: Run Final Migrations and Checks¶
cd /srv/uplink/app
# Run any pending migrations
docker compose exec web python manage.py migrate
# Collect static files
docker compose exec web python manage.py collectstatic --noinput
# Create superuser if needed (for fresh database)
# docker compose exec web python manage.py createsuperuser
# Run system check
docker compose exec web python manage.py check
Step 3.5: Configure Nginx/Reverse Proxy (If Needed)¶
If you use Nginx as reverse proxy, update config to point to Docker ports:
# Edit Nginx config
sudo nano /etc/nginx/sites-available/uplink
# Update upstream to point to Docker ports:
# upstream uplink_web {
# server 127.0.0.1:8000; # Docker web container
# }
#
# upstream uplink_daphne {
# server 127.0.0.1:9000; # Docker daphne container
# }
# Test Nginx config
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx
Step 3.6: Update Cron Jobs for Docker¶
# Edit crontab
crontab -e
# Update all cron jobs to use Docker exec pattern:
# OLD: cd /srv/uplink/app && pipenv run ./manage.py COMMAND
# NEW: cd /srv/uplink/app && docker compose exec -T web python manage.py COMMAND
# Example updated crontab:
# */2 * * * * cd /srv/uplink/app && docker compose exec -T web python manage.py prestashop_import_products
# */2 * * * * cd /srv/uplink/app && docker compose exec -T web python manage.py prestashop_import_orders
# 15 */12 * * * cd /srv/uplink/app && docker compose exec -T web python manage.py prestashop_import_carriers
# */10 * * * * cd /srv/uplink/app && docker compose exec -T web python manage.py prestashop_send_products
# 30 4 * * * cd /srv/uplink/app && docker compose exec -T web python manage.py prestashop_send_products --full
# */15 * * * * cd /srv/uplink/app && docker compose exec -T web python manage.py prestashop_sync_existing_order_statuses
# 0 8 * * * cd /srv/uplink/app && docker compose exec -T web python manage.py create_stock_levels_for_all_products
# Note: -T flag disables TTY allocation (required for cron)
⏱️ Downtime ends here
Verification¶
1. Check Service Health¶
cd /srv/uplink/app
# All containers should be running
docker compose ps
# Check container logs for errors
docker compose logs web | tail -50
docker compose logs daphne | tail -50
docker compose logs huey | tail -50
docker compose logs db | tail -50
docker compose logs redis | tail -20
# Check container resource usage
docker stats --no-stream
2. Test Application Access¶
# Test web application directly
curl -I http://localhost:8000/admin/
# Should return 200 or 302 (redirect to login)
# If using Nginx, test through Nginx
curl -I https://yourdomain.com/admin/
3. Test Critical Functionality¶
- [ ] Access admin panel in browser
- [ ] Login works
- [ ] Create/view products
- [ ] Create/view orders
- [ ] Test PrestaShop integration (check extra_hosts working)
- [ ] Test printer commands (WebSockets via Daphne)
- [ ] Verify background tasks processing (Huey)
- [ ] Check that media files load correctly
4. Monitor Cron Jobs¶
# Wait for next cron execution (2 minute interval)
# Check logs for cron job execution
docker compose logs web | grep -i prestashop
# Manually test a cron job command
cd /srv/uplink/app
docker compose exec web python manage.py prestashop_import_products --dry-run
5. Database Verification¶
cd /srv/uplink/app
# Check database connectivity
docker compose exec db mysql -u uplink_user -p uplink -e "SELECT COUNT(*) as total_products FROM catalogue_product;"
# Verify data integrity
docker compose exec web python manage.py check
6. Performance Check¶
# Check container resource usage
docker stats
# Check application response time
time curl -I http://localhost:8000/admin/
# Compare with pre-migration benchmarks
Rollback Procedure¶
If critical issues arise, rollback to the old setup:
Quick Rollback (10 minutes)¶
# 1. Stop Docker containers
cd /srv/uplink/app
docker compose down
# 2. Restore old crontab
crontab /srv/uplink/backups/crontab.backup.YYYYMMDD
# 3. Restore .env file
cp /srv/uplink/backups/.env.backup.YYYYMMDD /srv/uplink/app/.env
# 4. Start old services
sudo systemctl start uplink
sudo systemctl start uplink-daphne
sudo systemctl start uplink-huey
# 5. Verify old services running
sudo systemctl status uplink
sudo systemctl status uplink-daphne
sudo systemctl status uplink-huey
# 6. If Nginx was updated, restore old config
sudo cp /srv/uplink/backups/uplink.nginx.conf /etc/nginx/sites-available/uplink
sudo nginx -t
sudo systemctl reload nginx
# 7. Test application
curl -I http://localhost:8000/admin/
Database Rollback (If Data Lost)¶
# Restore database from backup
mysql -u uplink_user -p uplink < /srv/uplink/backups/uplink_db_backup_YYYYMMDD_HHMMSS.sql
# Run migrations
cd /srv/uplink/app
pipenv run ./manage.py migrate
# Restart services
sudo systemctl restart uplink uplink-daphne uplink-huey
Post-Migration Notes¶
New Deployment Commands¶
Old (pipenv/systemd):
cd /srv/uplink/app
git pull origin main
pipenv install
pipenv run ./manage.py migrate
pipenv run ./manage.py collectstatic --noinput
sudo systemctl restart uplink uplink-daphne uplink-huey
New (Docker):
cd /srv/uplink/app
git pull origin main
docker compose -f docker-compose.yml -f docker-compose.prod.yml build
docker compose exec web python manage.py migrate
docker compose exec web python manage.py collectstatic --noinput
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --force-recreate
Or use the deploy script:
cd /srv/uplink/app
./deploy.sh docker-build
./deploy.sh migrate
./deploy.sh static
./deploy.sh docker-restart
Managing Docker Services¶
# Start all services
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Stop all services
docker compose down
# Restart specific service
docker compose restart web
# View logs
docker compose logs -f web
docker compose logs -f --tail=100 huey
# Execute commands in containers
docker compose exec web python manage.py shell
docker compose exec db mysql -u uplink_user -p
# Access container shell
docker compose exec web bash
Automatic Startup on Boot¶
# Enable Docker to start on boot
sudo systemctl enable docker
# Create systemd service for docker-compose
sudo nano /etc/systemd/system/uplink-docker.service
# Add this content:
[Unit]
Description=Uplink Docker Compose Service
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/uplink/app
ExecStart=/usr/bin/docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
ExecStop=/usr/bin/docker compose down
User=ubuntu
Group=docker
[Install]
WantedBy=multi-user.target
# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable uplink-docker.service
sudo systemctl start uplink-docker.service
# Check status
sudo systemctl status uplink-docker.service
Docker Container Logs¶
# View all logs
docker compose logs
# Follow logs in real-time
docker compose logs -f
# View specific service logs
docker compose logs web
docker compose logs daphne
docker compose logs huey
# View last N lines
docker compose logs --tail=100 web
# Save logs to file
docker compose logs > /srv/uplink/backups/docker-logs-$(date +%Y%m%d).log
Database Backups with Docker¶
# Backup database from Docker MySQL
docker compose exec db mysqldump -u uplink_user -p uplink > /srv/uplink/backups/uplink_db_backup_$(date +%Y%m%d_%H%M%S).sql
# Restore database to Docker MySQL
docker compose exec -T db mysql -u uplink_user -p uplink < /srv/uplink/backups/uplink_db_backup_YYYYMMDD_HHMMSS.sql
# Automate backups with cron
# Add to crontab:
# 0 2 * * * cd /srv/uplink/app && docker compose exec -T db mysqldump -u uplink_user -pYOUR_PASSWORD uplink | gzip > /srv/uplink/backups/uplink_db_backup_$(date +\%Y\%m\%d_\%H\%M\%S).sql.gz
Benefits Achieved¶
- ✅ Environment consistency - Dev and production are identical
- ✅ No version conflicts - All dependencies containerized
- ✅ Easy rollback - Just revert to previous image
- ✅ Isolated services - Each service in its own container
- ✅ Reproducible deployments - Build once, deploy anywhere
- ✅ Better resource management - Container limits and monitoring
- ✅ Modern infrastructure - Industry standard approach
- ✅ 10-100x faster dependency installation - uv inside containers
Monitoring¶
For the first week after migration:
# Monitor container health
docker compose ps
watch -n 5 'docker compose ps'
# Monitor resource usage
docker stats
# Check application logs
docker compose logs -f web | grep ERROR
docker compose logs -f daphne | grep ERROR
docker compose logs -f huey | grep ERROR
# Monitor cron job execution
docker compose logs web | grep manage.py
# Check disk usage (Docker can consume space)
docker system df
df -h
Cleanup (After Successful Migration)¶
Once confident (1-2 weeks after migration):
# Disable old systemd services
sudo systemctl disable uplink.service
sudo systemctl disable uplink-daphne.service
sudo systemctl disable uplink-huey.service
# Remove old service files
sudo rm /etc/systemd/system/uplink*.service
sudo systemctl daemon-reload
# Clean up old Docker images/containers
docker system prune -a
# Archive old backups
tar -czf /srv/uplink/backups/pre-docker-backups.tar.gz /srv/uplink/backups/*.backup.*
Troubleshooting¶
Issue: Container won't start¶
Solution:
# Check logs
docker compose logs <service-name>
# Check container status
docker compose ps
# Try rebuilding
docker compose build --no-cache <service-name>
docker compose up -d <service-name>
# Check environment variables
docker compose exec <service-name> env
Issue: Database connection errors¶
Solution:
# Verify db container running
docker compose ps db
# Check database logs
docker compose logs db
# Verify .env has correct DATABASE_HOST=db
grep DATABASE_HOST .env
# Test connection manually
docker compose exec web python manage.py dbshell
# Check network
docker network ls
docker network inspect uplink_uplink_network
Issue: Cron jobs not executing¶
Solution:
# Test cron command manually
cd /srv/uplink/app
docker compose exec -T web python manage.py prestashop_import_products
# Check cron is running
sudo systemctl status cron
# Check cron logs
grep CRON /var/log/syslog | tail -20
# Verify crontab installed
crontab -l | grep manage.py
# Note: Must use -T flag with docker compose exec in cron
Issue: Permission errors with media files¶
Solution:
# Fix media directory permissions
sudo chown -R www-data:www-data /srv/uplink/app/media/
# Or match container user
# Check user in container
docker compose exec web id
# Adjust permissions to match
sudo chown -R 1000:1000 /srv/uplink/app/media/
Issue: Out of disk space¶
Solution:
# Check Docker disk usage
docker system df
# Clean up unused images/containers
docker system prune -a
# Remove old volumes (careful!)
docker volume prune
# Check host disk space
df -h
Issue: PrestaShop connection refused¶
Solution:
# Verify extra_hosts in docker-compose.yml
grep -A 5 extra_hosts docker-compose.yml
# Should show:
# extra_hosts:
# - "localhost:host-gateway"
# - "eu.localhost:host-gateway"
# Restart containers after changes
docker compose down
docker compose up -d
# Test from inside container
docker compose exec web ping -c 3 localhost
docker compose exec web curl -I http://localhost:8080
Success Criteria¶
Migration is successful when:
- ✅ All Docker containers running:
docker compose ps - ✅ Web application accessible via browser
- ✅ Admin panel working
- ✅ Database queries functioning
- ✅ Media files loading correctly
- ✅ Cron jobs executing every 2-15 minutes
- ✅ PrestaShop integration functioning
- ✅ WebSocket connections working (device commands via Daphne)
- ✅ Background tasks processing (Huey)
- ✅ No errors in logs for 24+ hours
- ✅ Deployment script works for updates
- ✅ Automatic startup on server reboot configured
Additional Resources¶
- Docker Documentation
- Docker Compose Documentation
- Django Docker Best Practices
- Local Docker setup: See DOCKER_SETUP_GUIDE.md
- Deployment automation: See DEPLOYMENT.md
Congratulations! Production is now running on Docker with uv! 🎉🐳