# 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:**
```bash
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:
```bash
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)
