Loading...
Home
Explore
Contact
Sign in
Cloud Migrations & Setup

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.

Fixwebnode Support
Fixwebnode Support
12 min read 1 views
Linode to DigitalOcean Docker Migration: Zero Data Loss

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.

SymptomQuick fixWhen to call Fixwebnode
App starts on DO but data is stale or emptyRe-run dump/restore; mount the correct volume pathLive traffic already partial-cutover; risk of split-brain writes
Container healthy, HTTPS fails after DNS flipIssue cert on new IP; lower TTL before cutoverCustom cert chains, load balancers, or multi-domain setups
Image builds on Linode but will not pull/run on DOPush to a registry; align architecture and env filesPrivate 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 $USER

Log out and back in so group membership applies. Verify:

docker version
docker compose version

Step 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/null

Note 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"]
EOF

Build 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-export

Step 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:
EOF

Put 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.service

Step 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.sha256

Step 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.sha256

Copy 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 web

Step 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.sha256

Point 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 nginx

Step 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.service

Leave 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/uploads

Step 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 /data

When 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.log

Step 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 --redirect

Step 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 || true

Step 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 web

Step 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.

Share this article
Fixwebnode Support
Fixwebnode Support

Hey there!
I am your assistant for Fixwebnode. Ask about our services, quotes, packages, orders, or how to get support.
While you wait
What’s your name and best email? We’ll reply even if you leave.