Linode to DigitalOcean Docker Migration: Zero Data Loss
Move a live app from Linode to DigitalOcean without downtime drama. Containerise cleanly, back up volumes and databases, cut over DNS safely—plus the three failure modes Australian operators hit most, with DIY fixes.
If your production app is still running bare on a Linode instance and you need it on DigitalOcean without losing data, containerising first is the cleanest path. This guide walks Australian sole traders and small businesses through a practical Docker migration: inventory, image build, volume and database transfer, cutover, and rollback. Work is fully remote. When you want a specialist to own the cutover window, start a conversation with Fixwebnode.
We cover real CLI steps you can run on Ubuntu-based Linode and DigitalOcean droplets, common failure modes (incomplete dumps, volume path mismatches, SSL/DNS lag), and when DIY stops being safe. Fixwebnode supports operators across Australia remotely—see all service areas for coverage context.
Is it hard to move a live Linode app to DigitalOcean with Docker?
No—not if you containerise first, freeze writes briefly for a consistent database dump and volume snapshot, then bring the same image and data up on DigitalOcean before you flip DNS. The hard parts are data consistency and secrets, not the cloud brand change itself. Plan a short maintenance window, verify restores on the new host, then cut over.
| Symptom | Quick fix | When to call Fixwebnode |
|---|---|---|
| App starts on DO but data is stale or empty | Re-run dump/restore; mount the correct volume path | Live traffic already partial-cutover; risk of split-brain writes |
| Container healthy, HTTPS fails after DNS flip | Issue cert on new IP; lower TTL before cutover | Custom cert chains, load balancers, or multi-domain setups |
| Image builds on Linode but will not pull/run on DO | Push to a registry; align architecture and env files | Private registries, multi-service Compose, or CI secrets |
Why this migration matters
Moving provider without containers often means reinstalling packages, hunting config drift, and hoping the database dump matches what production actually wrote. Docker freezes the runtime: same base image, same process user, same entrypoint. Your job becomes moving state—databases, uploads, queues—and proving the new droplet serves the same responses before customers notice.
For Australian businesses, latency to Sydney/Melbourne regions, AEST cutover windows, and backup retention matter more than marketing feature lists. Treat the migration as a controlled failover, not a rebuild from memory.
Common issues when moving Linode workloads to DigitalOcean
1. Incomplete database dump during live traffic
Symptom: App boots on DigitalOcean, login works, but recent orders or posts are missing. Logs show no crash—only older rows.
2. Persistent data path mismatch after containerise
Symptom: Container status is healthy, but uploads 404 or the app recreates an empty uploads directory inside the container layer instead of the host volume.
3. TLS and DNS lag after the IP change
Symptom: HTTP works on the droplet IP; browsers still hit the old Linode IP or show certificate name/IP mismatch for hours after you “went live.”
4. Secrets and connection strings still point at Linode
Symptom: App container restarts in a loop; logs show connection refused to an old private IP, managed DB hostname, or Redis that only existed on Linode’s network.
Prerequisites and inventory (do this on Linode first)
SSH to the Linode host. Confirm Docker is available or install it, then inventory what actually runs in production.
Step 1 — Install Docker Engine and Compose plugin (Ubuntu)
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) 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 docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USERLog out and back in so group membership applies. Verify:
docker version
docker compose versionStep 2 — Capture the live stack footprint
sudo ss -tulpn
ps aux --sort=-%mem | head -n 30
sudo lsof -i -P -n | head -n 50
df -h
sudo du -sh /var/www /home /opt /srv 2>/dev/nullNote the app root, PHP/Node/Python runtime, database engine, and any directories that hold uploads or media. Lower DNS TTL on your domain now (300 seconds is a practical target) so cutover later is not stuck behind a long cache.
Step-by-step: clean Docker migration with zero data loss
Build a reproducible image on Linode
Create a project directory and a Dockerfile that matches production (example: Node API; adapt base image for PHP-FPM, Python, etc.).
mkdir -p ~/app-migrate && cd ~/app-migrate
# Copy application source into ./app
# Example Dockerfile for a Node service
cat > Dockerfile <<'EOF'
FROM node:20-bookworm-slim
WORKDIR /usr/src/app
COPY app/package*.json ./
RUN npm ci --omit=dev
COPY app/ .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]
EOFBuild and tag locally, then push to a registry both clouds can reach (Docker Hub, GitHub Container Registry, or a private registry):
docker build -t youruser/yourapp:linode-export .
docker login
docker push youruser/yourapp:linode-exportStep 3 — Compose file with named volumes (do not bake data into the image)
cat > docker-compose.yml <<'EOF'
services:
web:
image: youruser/yourapp:linode-export
restart: unless-stopped
env_file:
- .env.production
ports:
- "3000:3000"
volumes:
- app_uploads:/usr/src/app/uploads
depends_on:
- db
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: CHANGE_ME
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432"
volumes:
app_uploads:
pgdata:
EOFPut secrets only in .env.production (never commit it). For MySQL/MariaDB, swap the db service image and data directory accordingly.
Consistent data export (zero data loss path)
Zero data loss means a brief write freeze or read-only window so dump + volume copy represent one moment in time—not a moving target.
Step 4 — Put the app in maintenance / stop writers
# If already containerised on Linode:
docker compose stop web
# If bare metal app: stop the process manager briefly
# sudo systemctl stop your-app.serviceStep 5 — Database dump with verification
# PostgreSQL example (adjust user/db)
docker compose exec -T db pg_dump -U app -d appdb -Fc -f /tmp/appdb.dump
docker compose cp db:/tmp/appdb.dump ./appdb.dump
ls -lh appdb.dump
sha256sum appdb.dump | tee appdb.dump.sha256
# MySQL/MariaDB alternative:
# docker compose exec -T db mysqldump -uapp -p"$MYSQL_PASSWORD" --single-transaction --routines --triggers appdb > appdb.sql
# sha256sum appdb.sql | tee appdb.sql.sha256Step 6 — Export named volumes (uploads and DB files if needed)
# Identify volume names
docker volume ls
# Stream volume contents to a tarball via a helper container
docker run --rm -v app-migrate_app_uploads:/data -v "$PWD":/backup alpine \
tar czf /backup/app_uploads.tar.gz -C /data .
sha256sum app_uploads.tar.gz | tee app_uploads.tar.gz.sha256Copy artefacts off-box immediately (object storage or scp to your workstation), then you may restart writers on Linode if the cutover is not immediate—but schedule final re-dump right before DNS flip if traffic continued.
scp appdb.dump appdb.dump.sha256 app_uploads.tar.gz app_uploads.tar.gz.sha256 .env.production user@your-workstation:~/migrate-artefacts/Provision DigitalOcean and restore
Step 7 — Create the droplet and install Docker (same Docker install commands as above on a fresh Ubuntu LTS droplet in your preferred Australian-friendly region). Open only required ports in the cloud firewall (22 from your IP, 80/443 public).
Step 8 — Pull image, place Compose and env, restore volumes
mkdir -p ~/app-migrate && cd ~/app-migrate
# scp compose, env, dumps, and tarballs onto the droplet first
docker pull youruser/yourapp:linode-export
docker compose up -d db
# Wait for Postgres ready
until docker compose exec -T db pg_isready -U app; do sleep 2; done
# Restore dump
docker compose cp ./appdb.dump db:/tmp/appdb.dump
docker compose exec -T db pg_restore -U app -d appdb --clean --if-exists /tmp/appdb.dump
# Recreate uploads volume contents
docker compose up -d web
docker run --rm -v app-migrate_app_uploads:/data -v "$PWD":/backup alpine \
sh -c 'rm -rf /data/* /data/.[!.]* 2>/dev/null; tar xzf /backup/app_uploads.tar.gz -C /data'
docker compose restart webStep 9 — Verify before DNS
docker compose ps
docker compose logs --tail=100 web db
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3000/health || true
# Compare row counts / checksums against Linode dump expectations
docker compose exec -T db psql -U app -d appdb -c 'SELECT COUNT(*) FROM important_table;'
sha256sum -c appdb.dump.sha256
sha256sum -c app_uploads.tar.gz.sha256Point a hosts-file override on your laptop to the new droplet IP and click through critical flows (login, checkout, file upload, admin). Only then touch public DNS.
Edge proxy, TLS, and cutover
Step 10 — Reverse proxy and certificates on DigitalOcean
sudo apt-get install -y nginx certbot python3-certbot-nginx
# Proxy pass to 127.0.0.1:3000 — then:
sudo certbot --nginx -d your.domain.example
sudo nginx -t && sudo systemctl reload nginxStep 11 — DNS flip and watch both sides
# From your laptop after TTL has expired window
dig +short your.domain.example A
curl -I https://your.domain.example
# Keep Linode read-only or stopped writers to prevent split-brain
# On Linode, after success:
# docker compose stop
# or sudo systemctl stop your-app.serviceLeave Linode powered on (billing aside) until you have 24–48 hours of clean metrics on DigitalOcean. Do not destroy volumes until checksums and business sign-off are done.
How to fix each common issue
Fix: incomplete database dump
Step 1 — Stop app writers; confirm no open write transactions.
docker compose stop web
# Postgres: check activity
docker compose exec -T db psql -U app -d appdb -c 'SELECT pid, state, query FROM pg_stat_activity;'Step 2 — Re-dump with a custom format and test restore into a throwaway database.
docker compose exec -T db pg_dump -U app -d appdb -Fc -f /tmp/appdb2.dump
docker compose exec -T db createdb -U app appdb_verify || true
docker compose exec -T db pg_restore -U app -d appdb_verify --clean --if-exists /tmp/appdb2.dump
docker compose exec -T db psql -U app -d appdb_verify -c 'SELECT COUNT(*) FROM important_table;'Step 3 — Only promote the verified dump. If traffic must stay up, use primary/replica or a short maintenance page—do not “dump while hot” without transaction-safe flags and acceptance of a small lag window.
When to call Fixwebnode: multi-database apps, large dumps that cannot fit a simple maintenance window, or any sign of split-brain after a partial DNS flip.
Fix: volume / upload path mismatch
Step 1 — Inspect what the running container actually mounts.
docker compose ps
docker inspect $(docker compose ps -q web) --format '{{json .Mounts}}' | jq .Step 2 — Align Compose mount target with the path the app writes (check app config). Recreate—not only restart—after changing mounts:
docker compose up -d --force-recreate web
docker compose exec web ls -la /usr/src/app/uploadsStep 3 — Re-extract the tarball into the named volume if the directory is empty, then fix ownership to the container user.
docker compose exec web id
# example fix if app runs as node uid 1000
docker run --rm -v app-migrate_app_uploads:/data alpine chown -R 1000:1000 /dataWhen to call Fixwebnode: bind mounts mixed with named volumes across several services, or media on object storage still dual-writing to disk.
Fix: TLS and DNS after IP change
Step 1 — Confirm the droplet answers on 80/443 before the flip.
curl -I http://NEW_DROPLET_IP
sudo tail -n 50 /var/log/nginx/error.logStep 2 — Issue or renew certificates only after DNS A/AAAA records point at DigitalOcean (or use DNS-01 if you must pre-issue).
sudo certbot renew --dry-run
sudo certbot --nginx -d your.domain.example --redirectStep 3 — Watch resolver cache: compare dig from multiple resolvers; keep Linode firewall closed to public HTTP once you intend DO to own traffic, so stale DNS fails closed instead of serving old code.
When to call Fixwebnode: CDN in front, multiple hostnames, or email/SPF records entangled with the same cutover.
Fix: secrets still targeting Linode
Step 1 — Diff env files; search for old private IPs and hostnames.
grep -E 'linode|192\.|10\.|redis|postgres|mysql' .env.production || trueStep 2 — Update connection strings to DigitalOcean-local service names (db, redis) or new managed endpoints; recreate containers so env is re-read.
docker compose up -d --force-recreate web
docker compose logs --tail=200 webStep 3 — Rotate passwords that were copied in plain text during migration; revoke old Linode firewall rules that exposed database ports publicly (they should bind to 127.0.0.1 only).
When to call Fixwebnode: unknown sprawl of API keys, webhook endpoints still hitting the old IP, or production secrets only living in shell history.
When DIY is enough vs when to book Fixwebnode
DIY is reasonable when you have one or two containers, a single database under a few tens of gigabytes, SSH access on both sides, and a willing maintenance window. Follow the dump → checksum → restore → hosts-file test → DNS path above and keep Linode as cold standby.
Book Fixwebnode when any of these apply: zero-tolerance data loss (payments, bookings, patient/customer records), multi-service Compose with workers and queues, live traffic that cannot pause, unclear legacy installs on Linode (no Dockerfile yet), or a failed cutover already serving mixed old/new state. Fixwebnode is a direct remote specialist for Australian operators—not a freelance marketplace. Share SSH method, domain DNS access, and current stack notes via the website repair Australia landing page.
Closing: plan the cutover, then prove it
Moving Linode to DigitalOcean is not hard when the unit of migration is a container image plus verified state. Inventory the live host, build and push an image, freeze writers for a consistent dump and volume archive, restore on DigitalOcean, prove health on the new IP, then flip DNS and decommission writers on the old side.
If you want a specialist to run the window with you—or to recover a migration that already drifted—start a conversation with Fixwebnode through https://fixwebnode.com.au/website-repair-australia. Remote help is available for teams across Australia; geography overview lives on service areas.