Files
hermes-skills/skills/devops/docker-service-deployment/references/docker-migration-pitfalls.md
T

4.5 KiB

Docker Volume Migration Pitfalls

Common failures when moving Docker services between hosts.

Permission denied on data volume (n8n, Jul 10 2026)

Symptom: Container starts but immediately crashes with EACCES: permission denied, open '/home/node/.n8n/crash.journal'

Root cause: Docker named volumes copied from host A (scp the _data directory) carry the UID/GID from host A's filesystem. The container on host B runs as a different UID (n8n uses UID 1000, node user). The files are owned by root or ippadmin from the copy operation, not UID 1000.

Fix:

VOL=$(docker volume inspect <volume_name> --format "{{.Mountpoint}}")
chown -R 1000:1000 "$VOL"
docker compose restart <service>

For n8n specifically: UID 1000 is the node user inside the container. Both n8n_data and postgres_data volumes may need this fix.

Prevention: After copying volume data to a new host, always check ownership before starting containers:

ls -la /var/lib/docker/volumes/<volume>/_data/ | head -5

Volume data owned by wrong user (general)

Any Docker container that writes to a bind-mounted or named volume will have files owned by the container's internal UID. When copying between hosts:

  1. Identify the container's UID: docker run --rm <image> id
  2. Apply: chown -R <UID>:<UID> <volume_path>
  3. Then start the container

SSL redirect loop after migration (n8n, Jul 10 2026)

Symptom: n8n returns 308 redirect loop when accessed through Caddy with HTTPS

Root cause: n8n's N8N_PROTOCOL is set to https but Caddy terminates SSL. n8n sees the request came via HTTP (from Caddy's proxy) and redirects to HTTPS, creating an infinite loop.

Fix: Set N8N_PROTOCOL=http in .env — Caddy handles SSL termination, n8n should trust the proxy.

Caddy handle fallback overrides path routes (Jul 10, 2026)

Symptom: Browser shows "app1 — /n8n/ /webui/ /litellm/" instead of the app.

Root cause: A handle { respond "..." 200 } at the bottom of the Caddyfile catches ALL requests — including /n8n/signin — because Caddy evaluates handle blocks before handle_path directives.

Fix: Replace the handle { respond ... } with a bare reverse_proxy as the catch-all. Path-specific handle_path routes take priority over the catch-all reverse_proxy, but NOT over a catch-all handle block.

Lesson: Only use handle blocks for specific path prefixes. For a true catch-all, use reverse_proxy directly without wrapping in handle. Never put handle { respond } as a fallback when you also have handle_path routes — it overrides them.

Symptom: app1.itpropartner.com/n8n/ shows the app but all assets (JS, CSS, images) 404

Root cause: handle_path /n8n/* correctly strips the prefix before proxying, so n8n sees /signin instead of /n8n/signin. But n8n's HTML references assets at /assets/..., /static/... — these paths DON'T match /n8n/* and fall through to the default handler.

Fix: Give the service its own subdomain instead of a sub-path. Path-based routing only works for apps that support a baseUrl config option. n8n does not.

Lesson: Use sub-domain routing (n8n.itpropartner.com) for apps without baseUrl support, sub-path routing (domain.com/app/) only for apps that support it.

Let's Encrypt validation blocked by UFW / DNS (Jul 10, 2026)

Symptom: Caddy log shows authorization failed: HTTP 400 ... Timeout during connect (likely firewall problem). Cert never issues.

Root cause: UFW default deny blocks HTTP-01 challenge from Let's Encrypt's servers, OR DNS still points to wrong IP (stale SiteGround records during Cloudflare migration).

Fix: (1) Ensure ufw allow 80/tcp and ufw allow 443/tcp on the target server. (2) Verify DNS resolves to the correct IP from outside the server: ping app1.itpropartner.com. Core's DNS may be stale while your laptop's is fresh — test from an external source.

n8n Caddy deployment checklist (Jul 10, 2026)

When deploying n8n behind Caddy on a new host:

  1. N8N_PROTOCOL=http — Caddy terminates SSL
  2. N8N_HOST=<domain> — match the public domain
  3. WEBHOOK_URL=https://<domain>/ — for webhook callbacks
  4. Caddy: reverse_proxy 127.0.0.1:5678 { flush_interval -1 } — WebSocket support
  5. Caddy: header { X-Forwarded-Proto https; X-Forwarded-Host {host} } — proxy headers
  6. UFW: allow 80/tcp + 443/tcp for Let's Encrypt validation
  7. DNS must resolve to this server's IP BEFORE starting Caddy (otherwise cert issuance blocks on first request)