Skip to content

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

  1. Pre-Migration Checklist
  2. Phase 1: Docker Setup (No Downtime)
  3. Phase 2: Data Migration (No Downtime)
  4. Phase 3: Final Cutover (Downtime)
  5. Verification
  6. Rollback Procedure
  7. 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


Congratulations! Production is now running on Docker with uv! 🎉🐳