---
title: "NPM to Caddy Migration Runbook"
description: "Step-by-step operational runbook for migrating from Nginx Proxy Manager to Caddy on 10.10.0.16. Four phases: prepare (test on alternate ports), cutover (<2 min downtime), validate (24-48h monitoring), and cleanup. Includes rollback plan."
type: runbook
domain: server-configs
tags: [caddy, nginx-proxy-manager, migration, reverse-proxy, dns, pihole, cloudflare]
---

# NPM to Caddy Migration Plan

Step-by-step guide to migrate from Nginx Proxy Manager to Caddy on `10.10.0.16`.

## Prerequisites

- [ ] Cloudflare API token with `Zone:DNS:Edit` for manticorum.com
- [ ] SSH access to `pihole` (10.10.0.16) and `ubuntu-manticore` (10.10.0.226)
- [ ] Docker and docker compose installed on 10.10.0.16
- [ ] Familiarity with current NPM proxy hosts (see README.md)

## Phase 1: Prepare (no downtime, no changes to production)

### 1.1 Create Cloudflare API Token

1. Go to https://dash.cloudflare.com/profile/api-tokens
2. Create token with permissions:
   - **Zone - DNS - Edit** (scoped to manticorum.com)
3. Save the token securely

### 1.2 Deploy Caddy Config to Host

```bash
# From workstation
scp -r server-configs/caddy-migration/ pihole:/home/cal/caddy/
ssh pihole "cp /home/cal/caddy/.env.example /home/cal/caddy/.env"
```

Edit `.env` on the host:
```bash
ssh pihole "nano /home/cal/caddy/.env"
# Set CF_API_TOKEN=<your token>
```

### 1.3 Build and Test Caddy (on alternate ports)

Temporarily modify `docker-compose.yml` to use non-conflicting ports:
```yaml
ports:
  - "8080:80"
  - "8443:443"
  - "8443:443/udp"
```

```bash
ssh pihole "cd /home/cal/caddy && docker compose up -d --build"
```

Verify the container starts and the Cloudflare module is loaded:
```bash
ssh pihole "docker logs caddy 2>&1 | head -30"
ssh pihole "docker exec caddy caddy list-modules | grep cloudflare"
```

Verify config is valid:
```bash
ssh pihole "docker exec caddy caddy validate --config /etc/caddy/Caddyfile"
```

Test a proxy host directly (bypass DNS):
```bash
curl -k --resolve sbadev.manticorum.com:8443:10.10.0.16 https://sbadev.manticorum.com:8443/
```

### 1.4 Verify Cert Issuance

Check that Caddy successfully obtains a wildcard cert:
```bash
ssh pihole "docker logs caddy 2>&1 | grep -i 'certificate\|tls\|acme'"
```

You should see successful ACME DNS-01 challenge completion.

### 1.5 Stop Test Caddy

```bash
ssh pihole "cd /home/cal/caddy && docker compose down"
```

Revert `docker-compose.yml` ports back to 80/443.

## Phase 2: Cutover (brief downtime)

Expected downtime: **< 2 minutes** (stop NPM, start Caddy, sync DNS).

### 2.1 Backup NPM

```bash
ssh pihole "cd ~/nginx-proxy-manager && tar czf ~/npm-backup-$(date +%Y%m%d).tar.gz data/ letsencrypt/"
```

### 2.2 Take a Snapshot

If 10.10.0.16 is a VM/LXC on Proxmox, take a snapshot first:
```bash
# From proxmox host (adjust VMID)
pct snapshot <VMID> pre-caddy-migration
```

### 2.3 Stop NPM

```bash
ssh pihole "cd ~/nginx-proxy-manager && docker compose down"
```

Ports 80, 443, and 81 are now free.

### 2.4 Start Caddy

```bash
ssh pihole "cd /home/cal/caddy && docker compose up -d"
```

### 2.5 Verify Services

Quick smoke test of key services:
```bash
# Test from workstation (DNS should already point to 10.10.0.16 via Pi-hole)
curl -sI https://git.manticorum.com | head -5
curl -sI https://n8n.manticorum.com | head -5
curl -sI https://jellyfin.manticorum.com | head -5
curl -sI https://foundry.manticorum.com | head -5
curl -sI https://status.manticorum.com | head -5

# Test internal-only access
curl -sI https://radarr.manticorum.com | head -5   # should work from local
curl -sI https://sonarr.manticorum.com | head -5
```

### 2.6 Update Pi-hole Sync

Deploy the new sync script:
```bash
ssh pihole "cp /home/cal/caddy/scripts/caddy-pihole-sync.sh /home/cal/scripts/"
ssh pihole "chmod +x /home/cal/scripts/caddy-pihole-sync.sh"
```

Test dry run:
```bash
ssh pihole "/home/cal/scripts/caddy-pihole-sync.sh --dry-run"
```

Run sync:
```bash
ssh pihole "/home/cal/scripts/caddy-pihole-sync.sh"
```

Update cron to use the new script:
```bash
ssh pihole "crontab -l | sed 's|npm-pihole-sync.sh|caddy-pihole-sync.sh|g' | crontab -"
```

Also update the `CADDYFILE` path variable in the script if the deployment path differs from `/home/cal/caddy/Caddyfile`.

## Phase 3: Validate (next 24-48 hours)

### 3.1 Monitor Caddy Logs

```bash
ssh pihole "docker logs caddy -f"
```

Look for:
- Successful TLS handshakes
- No upstream connection errors
- Cert renewal events (if timing aligns)

### 3.2 Check Uptime Kuma

Verify all monitored services at https://status.manticorum.com show UP.

### 3.3 Test WebSocket Services

These services use WebSockets and should be tested interactively:
- **Foundry VTT** (foundry.manticorum.com) - open a game session
- **n8n** (n8n.manticorum.com) - open workflow editor
- **Memos** (memos.manticorum.com) - create/edit a memo
- **Termix** (termix.manticorum.com) - open a terminal session

### 3.4 Test External Access

If any services are accessed via Cloudflare from outside:
1. From a phone on cellular (not on home WiFi)
2. Access public services and verify they load
3. Access internal-only services and verify 403 response

### 3.5 Verify Access Restrictions

```bash
# From a machine NOT on 10.0.0.0/23 or 10.10.0.0/24:
curl -sI https://radarr.manticorum.com  # Should return 403
curl -sI https://sonarr.manticorum.com  # Should return 403
```

## Phase 4: Cleanup

### 4.1 Remove NPM (after validation period)

```bash
# Keep backup, remove containers and images
ssh pihole "cd ~/nginx-proxy-manager && docker compose rm -f"
ssh pihole "docker image rm jc21/nginx-proxy-manager:latest"
```

### 4.2 Update Documentation

- Update `server-configs/networking/nginx-proxy-manager-pihole.md` to reference Caddy
- Update any Uptime Kuma monitors that check port 81 (NPM admin)
- Update `CONTEXT.md` networking section

### 4.3 Free Port 81

Port 81 (NPM admin UI) is no longer needed. Caddy's admin API runs on localhost:2019 inside the container by default (not exposed).

## Rollback Plan

If something goes wrong, rollback takes < 1 minute:

```bash
# Stop Caddy
ssh pihole "cd /home/cal/caddy && docker compose down"

# Restart NPM
ssh pihole "cd ~/nginx-proxy-manager && docker compose up -d"

# Revert cron to old sync script
ssh pihole "crontab -l | sed 's|caddy-pihole-sync.sh|npm-pihole-sync.sh|g' | crontab -"
```

Or restore from Proxmox snapshot:
```bash
pct rollback <VMID> pre-caddy-migration
```

## Adding New Services After Migration

Edit the Caddyfile and reload - no web UI needed:

```bash
ssh pihole "nano /home/cal/caddy/Caddyfile"

# Add a new block:
# newservice.manticorum.com {
#     reverse_proxy 10.10.0.xxx:port {
#         import proxy_headers
#     }
# }

# Validate
ssh pihole "docker exec caddy caddy validate --config /etc/caddy/Caddyfile"

# Apply (zero downtime)
ssh pihole "docker exec caddy caddy reload --config /etc/caddy/Caddyfile"

# Sync DNS to Pi-holes
ssh pihole "/home/cal/scripts/caddy-pihole-sync.sh"
```
