Skip to content

Latest commit

 

History

History
582 lines (417 loc) · 15.1 KB

File metadata and controls

582 lines (417 loc) · 15.1 KB

Cascade — Deploy from Scratch

Full server setup: Ubuntu 22.04 / 24.04 + AmneziaWG kernel module + Docker + Caddy reverse proxy.


Requirements

OS Ubuntu 22.04 LTS or Ubuntu 24.04 LTS
Kernel 6.1+ (see Step 1)
RAM 512 MB minimum
Access Root
Network Public IP, ports 443/TCP and WireGuard UDP ports open

Step 1 — Upgrade kernel to 6.x (Ubuntu 22.04 only)

Ubuntu 24.04: skip this step — ships with kernel 6.8 by default.

Ubuntu 22.04 ships with kernel 5.15. The AmneziaWG DKMS module requires ≥ 6.1 (timer_delete symbol). Install the HWE kernel:

apt update && apt install -y linux-generic-hwe-22.04
reboot

After reboot verify:

uname -r
# expected: 6.8.x-xx-generic

Step 2 — Install AmneziaWG kernel module

add-apt-repository ppa:amnezia/ppa
apt install -y amneziawg

Load the module immediately and register it for autoload on boot:

modprobe amneziawg
echo "amneziawg" | tee /etc/modules-load.d/amneziawg.conf

Verify:

lsmod | grep amneziawg
# expected: amneziawg   131072  0

Step 3 — Install Docker

curl -fsSL https://get.docker.com | sh

Step 4 — Configure kernel parameters

Enable IP forwarding and tune network buffers (required for WireGuard routing and HTTP/3):

cat > /etc/sysctl.d/99-cascade.conf << 'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
net.core.rmem_max = 7340032
net.core.wmem_max = 7340032
EOF

sysctl --system

Step 5 — Clone repository

Minimal/stripped-down VPS images often don't ship git — install it first if the clone below fails with "command not found":

apt-get update && apt-get install -y git
git clone https://github.com/JohnnyVBut/cascade.git
cd cascade

Step 6 — Pull Cascade image

The Docker image is built by GitHub Actions on every merge to master and published to GitHub Container Registry. No local build required:

docker compose -f docker-compose.yml pull

Local development only: to use a locally-built image, run ./build.sh and add -f docker-compose.override.yml to your compose commands.


Step 7 — Configure Cascade

Edit docker-compose.yml. The key variables:

environment:
  - PASSWORD_HASH=          # leave empty for open mode — first user created via UI
  - WG_HOST=1.2.3.4         # public IP or domain (can also be set in Settings UI)
  - PORT=8888               # Web UI port (listens on localhost only)
  - BIND_ADDR=127.0.0.1     # bind to localhost — Caddy proxies from outside

Open mode: if PASSWORD_HASH is empty, the web UI shows a "Create First User" form on first visit. Create your admin account there. After creation, the form disappears and login is required.

Optional — pre-set password hash (non-interactive / CI):

docker run --rm -it ghcr.io/johnnybut/cascade:latest /app/cascade hash
# Enter password when prompted — copy the $2a$... hash into PASSWORD_HASH=

Step 8 — Start Cascade

docker compose -f docker-compose.yml up -d

Verify it is healthy and listening on localhost:

docker ps
curl http://127.0.0.1:8888/api/health
# expected: {"host":"...","status":"ok","version":"3.0.0-alpha"}

Step 9 — Obtain TLS certificate (acme.sh)

Cascade must be running (Step 8) before this step. acme.sh uses standalone mode — it temporarily binds port 80 to complete the ACME HTTP-01 challenge. Port 80 must be free (Caddy is not started yet at this point).

Install acme.sh:

curl https://get.acme.sh | sh -s email=YOUR@EMAIL.COM
source ~/.bashrc

Option A — bare IP address (most common for VPS)

Let's Encrypt supports TLS certificates for bare IP addresses, but only via the shortlived profile (6-day validity). Standard 90-day certificates for IPs are not available from Let's Encrypt. Without --certificate-profile shortlived you will get:

Error creating new order :: Default profile does not permit IP address identifiers.
~/.acme.sh/acme.sh --issue \
  --server letsencrypt \
  -d YOUR.SERVER.IP \
  --standalone \
  --certificate-profile shortlived

acme.sh installs a cron job that renews automatically every 3 days — no manual action needed.

Testing with Let's Encrypt Staging CA

Before going live, use the LE staging environment to avoid hitting rate limits. Staging issues untrusted certificates — browsers will show a warning, but the full ACME flow is validated.

~/.acme.sh/acme.sh --issue \
  --server letsencrypt_test \
  -d YOUR.SERVER.IP \
  --standalone \
  --certificate-profile shortlived

--certificate-profile shortlived is required for bare IPs on both staging and production. Without it the order is rejected regardless of which CA is used.

Via setup.sh:

bash deploy/setup.sh --staging          # interactive
bash deploy/setup.sh --yes --staging    # non-interactive

When you are ready for production, set ACME_STAGING=0 in deploy/.env and re-run setup.sh — it will detect the staging cert, delete it, and issue a trusted production certificate.

Option B — domain name

If you have a domain pointing to the server, use a standard 90-day certificate instead:

~/.acme.sh/acme.sh --issue \
  --server letsencrypt \
  -d yourdomain.example.com \
  --standalone

No --certificate-profile flag needed. Auto-renewal every 60 days.

Install the certificate to a persistent location:

mkdir -p /etc/ssl/cascade

~/.acme.sh/acme.sh --install-cert -d YOUR.SERVER.IP \
  --key-file       /etc/ssl/cascade/server.key \
  --fullchain-file /etc/ssl/cascade/server.crt \
  --reloadcmd      "docker exec cascade-caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null || true"

Step 10 — Deploy Caddy reverse proxy

Caddy sits in front of Cascade: serves the decoy site on HTTPS, and only routes requests under a secret path to the admin UI.

cd ~/cascade/deploy/caddy
cp .env.example .env

Edit .env:

# Secret path prefix for the admin UI — choose something random, no slashes
ADMIN_PATH=your_random_secret_here

# Cascade port (must match PORT in docker-compose.yml)
CASCADE_PORT=8888

Start Caddy:

docker compose up -d --build
docker compose logs -f

Expected output — no errors, then:

{"level":"info","msg":"serving initial configuration"}

Step 11 — Verify full stack

# Decoy site (no admin path)
curl -k https://YOUR.SERVER.IP
# → StreamVault HTML

# Admin UI (with secret path — note the trailing slash)
curl -k https://YOUR.SERVER.IP/YOUR_ADMIN_PATH/api/health
# → {"status":"ok",...}

Open in browser: https://YOUR.SERVER.IP/YOUR_ADMIN_PATH/


Ports reference

Port Protocol Purpose
443 TCP + UDP (HTTP/3) HTTPS — Caddy (public)
80 TCP ACME renewal only (not permanently open)
8888 TCP Cascade UI — bound to 127.0.0.1, not public
51830 UDP WireGuard interface wg10 (first tunnel)
51831 UDP WireGuard interface wg11, etc.

WireGuard UDP ports must be open in the host firewall:

ufw allow 51830:51840/udp

Data directory

All Cascade state is stored in ~/cascade/data/:

data/
  wireguard.db          ← SQLite: interfaces, peers, routes, NAT, firewall rules, etc.
  *.save                ← ipset snapshots (auto-restored on startup)
  /etc/amnezia/amneziawg/wg10.conf   ← generated WireGuard configs (inside container)

The data directory is mounted into the container via docker-compose.yml.


Updating

cd ~/cascade
git pull origin master
docker compose -f docker-compose.yml pull
docker compose -f docker-compose.yml down
docker compose -f docker-compose.yml up -d

The image is pre-built by CI — docker compose pull fetches the latest version from GHCR without compiling anything on the server.

Caddy does not need to be restarted for Cascade updates.

⚠️ Kernel module mode: the Docker image tracks AmneziaWG's :latest protocol line, but the host's kernel module does not update itself. This mainly affects installs from before the AmneziaWG 3.0 protocol jump (2026-07-30) — the module stayed on the pre-3.0 line while the image moved to 3.0.x. If the two drift apart, interfaces fail to start with Unable to modify interface: Invalid argument. Re-sync the kernel module whenever you update the container in this mode:

sudo bash deploy/switch-mode.sh --kernel

Userspace mode is unaffected — it has no host kernel-module dependency.


Clean Reinstall

Use this procedure when you need to wipe all Cascade state and start from scratch (lost admin password, corrupted database, major configuration change, etc.).

⚠️ This deletes all interfaces, peers, routes, NAT rules, and firewall rules. Export any configs you need before proceeding.

Step 1 — Stop and remove the container

cd ~/cascade
docker compose down

Step 2 — Bring down WireGuard interfaces

Cascade runs with --network host, so WireGuard interfaces survive container removal. Bring them down manually:

# List active WireGuard interfaces:
ip -d link show | grep -E "wg[0-9]+"

# Bring each one down (repeat for wg10, wg11, etc.):
awg-quick down wg10
# or for standard WireGuard:
wg-quick down wg10

If awg-quick / wg-quick are not available on the host, use ip link:

ip link delete wg10

Step 3 — Remove all Cascade data

# SQLite database, ipset snapshots — all configuration:
rm -rf ~/cascade/data/

# WireGuard config files generated by Cascade:
rm -f /etc/amnezia/amneziawg/wg*.conf
rm -f /etc/wireguard/wg*.conf

Step 4 — Flush leftover iptables rules (optional but recommended)

# Remove stale FORWARD / NAT / mangle rules from previous run:
iptables-nft -F FIREWALL_FORWARD 2>/dev/null || true
iptables-nft -X FIREWALL_FORWARD 2>/dev/null || true
iptables-nft -t mangle -F FIREWALL_MANGLE 2>/dev/null || true
iptables-nft -t mangle -X FIREWALL_MANGLE 2>/dev/null || true
iptables-nft -t nat -F POSTROUTING

Tip: A server reboot achieves the same result without risk of affecting other services.

Step 5 — Pull latest image and start

docker compose -f docker-compose.yml pull
docker compose -f docker-compose.yml down
docker compose -f docker-compose.yml up -d

On first start with an empty data/ directory, Cascade creates a fresh database. Open the UI — you will be prompted to create the first admin account.


Troubleshooting

AmneziaWG module not loaded after reboot

modprobe amneziawg
# If that fails — check DKMS build status:
dkms status
uname -r        # must be 6.x

Cascade container exits immediately

docker logs cascade

Common causes:

  • PASSWORD_HASH is empty or malformed

Interfaces not appearing in UI

# Confirm API is reachable through Caddy:
curl -k https://YOUR.SERVER.IP/YOUR_ADMIN_PATH/api/health

# Check Cascade logs:
docker logs cascade | tail -30

If the UI loads but the Interfaces page is empty — make sure you are accessing the UI via https://YOUR.SERVER.IP/YOUR_ADMIN_PATH/ (with trailing slash). Without the trailing slash, relative API paths resolve incorrectly.

Caddy certificate errors

# Re-issue the certificate:
~/.acme.sh/acme.sh --issue --server letsencrypt -d YOUR.SERVER.IP \
  --standalone --certificate-profile shortlived --force

# Reinstall:
~/.acme.sh/acme.sh --install-cert -d YOUR.SERVER.IP \
  --key-file /etc/ssl/cascade/server.key \
  --fullchain-file /etc/ssl/cascade/server.crt \
  --reloadcmd "docker exec cascade-caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null || true"

# Restart Caddy:
cd ~/cascade/deploy/caddy && docker compose restart

WireGuard tunnel not passing traffic

# Check interface is up:
ip -d link show wg10

# Check NAT rule exists:
iptables-nft -t nat -L POSTROUTING -n -v | grep MASQUERADE

# Check IP forwarding:
sysctl net.ipv4.ip_forward    # must be 1

# If PostUp rules are missing — Stop and Start the interface in the UI

QUIC UDP buffer warning in Caddy logs

failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 7168 kiB, got: 416 kiB)

This is a warning, not an error. HTTP/3 works but with a smaller buffer. To silence it, apply the sysctl settings from Step 4 and restart Caddy.

Disk filling up over time

On small VPS disks (e.g. 10 GB), repeated manual ./build.sh runs and normal container operation can accumulate disk usage outside of anything Cascade itself stores. Check where the space actually went before deleting anything:

df -h /
du -xh --max-depth=1 / 2>/dev/null | sort -rh | head -20

Then drill into whichever top-level directory is largest (du -xh --max-depth=1 /usr, /var, etc.) until you find the actual offender. Common culprits, safest-first:

1. Docker build cache — grows with every manual docker build/./build.sh run; a --filter "until=..." prune often leaves cache entries marked shared: true behind. Confirm with docker system df -v (look at "Build cache usage"), then clear it fully — this never touches running containers or volumes, only cached intermediate build layers, so the next build is just slower (no cache), not lossy:

docker system df -v
docker builder prune -a -f

2. Dangling images — left behind when a rebuild reassigns the cascade:latest tag to a new image, orphaning the old one:

docker image prune -f

3. Docker container logs — the cascade service has no log size limit by default (json-file driver), so long-running installs can accumulate a large log file:

docker inspect --format='{{.LogPath}}' cascade | xargs du -h
# safe to truncate live, without touching the running container:
truncate -s 0 $(docker inspect --format='{{.LogPath}}' cascade)

4. systemd journal / apt cache — not Cascade-related, but common on small VPS images:

journalctl --vacuum-size=200M
apt-get clean

5. Old kernel packagessetup.sh upgrades Ubuntu 22.04 to the HWE 6.x kernel; leftover previous kernel versions in /usr/lib/modules and /usr/src add up. Confirm the currently running kernel first (uname -r), then remove only the others via apt-get autoremove — never remove the kernel that's currently booted.

6. Old pre-restore-*.tar.gz backups — created automatically before every restore-from-backup operation, in /etc/wireguard/data/; there's currently no automatic pruning, so old ones accumulate if you restore from backup repeatedly:

ls -lh /etc/wireguard/data/pre-restore-*.tar.gz
rm /etc/wireguard/data/pre-restore-<old-timestamp>.tar.gz

acme.sh auto-renewal

acme.sh installs a cron job automatically during installation. Verify:

crontab -l | grep acme
# expected: something like: 0 0 * * * /root/.acme.sh/acme.sh --cron --home /root/.acme.sh ...

On renewal, acme.sh runs the --reloadcmd configured in Step 9, which reloads Caddy config without downtime.