# Nextcloud AIO over Tailscale (VM `192.168.0.178`) Nextcloud AIO running behind Caddy reverse proxy, accessed exclusively over Tailscale at `pop-os.tail3fb075.ts.net`. ## Architecture ``` Browser (any network) → Tailscale tunnel → pop-os.tail3fb075.ts.net:443 → Caddy (TLS with Tailscale HTTPS cert, port 443) → reverse_proxy to localhost:11000 → AIO Apache container (plain HTTP, port 11000) → Nextcloud ``` - **Only devices on your tailnet** (logged into your Tailscale account) can reach the server. This works from any wifi, mobile data, or network -- Tailscale encrypts traffic end-to-end via WireGuard. - AIO's Apache container listens on port 11000 (no TLS). Caddy handles TLS on port 443 with a publicly-trusted certificate provisioned via `tailscale cert`. - The AIO admin interface is on port 8080 (self-signed cert). Access it via `https://100.78.69.20:8080` (use the Tailscale IP, not the FQDN, to avoid HSTS issues). - `SKIP_DOMAIN_VALIDATION=true` is required because Tailscale domains are private and cannot be verified via public DNS. ## Prerequisites - Docker running on the VM - Tailscale installed and connected - SSH access to the VM: `ssh sze@192.168.0.178` - Caddy installed locally on the VM - Tailscale HTTPS enabled in the [admin console](https://login.tailscale.com/admin/dns) (under HTTPS Certificates) ## Current Authorized Devices | Device | Tailscale IP | OS | |---|---|---| | pop-os (server) | 100.78.69.20 | Linux | | szes-macbook-pro | 100.78.115.109 | macOS | | iphone182 | 100.101.165.57 | iOS | To add a new device: install Tailscale on it and sign in with the same account. To remove access: delete the device from the [Tailscale admin console](https://login.tailscale.com/admin/machines). --- ## Fresh Deploy (From Scratch) ### 0. Verify Tailscale and MagicDNS On the VM: ```bash tailscale status ``` From another tailnet device: ```bash ping pop-os.tail3fb075.ts.net ``` If the hostname does not resolve, enable MagicDNS in the [Tailscale admin console](https://login.tailscale.com/admin/dns). ### 1. Remove all existing AIO containers and volumes ```bash docker ps -a --filter name=nextcloud-aio --format "{{.Names}}" | xargs -r docker rm -f docker volume ls --format "{{.Name}}" | grep nextcloud_aio | xargs -r docker volume rm ``` Both the master volume AND the data volumes (database, nextcloud_data, etc.) must be removed together. If only the master volume is reset, the new instance generates new database credentials that don't match the old database volume, causing `password authentication failed for user "oc_nextcloud"`. ### 2. Launch the master container ```bash docker run \ --sig-proxy=false \ --name nextcloud-aio-mastercontainer \ --restart always \ --publish 8080:8080 \ -e APACHE_PORT=11000 \ -e APACHE_IP_BINDING=0.0.0.0 \ -e SKIP_DOMAIN_VALIDATION=true \ -v nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ -v /var/run/docker.sock:/var/run/docker.sock:ro \ nextcloud/all-in-one:latest ``` Key env vars: | Variable | Value | Why | |---|---|---| | `APACHE_PORT` | `11000` | AIO Apache serves plain HTTP here; Caddy handles TLS on 443 | | `APACHE_IP_BINDING` | `0.0.0.0` | Allow connections from Caddy on localhost | | `SKIP_DOMAIN_VALIDATION` | `true` | Tailscale domains are private, can't be verified via public DNS | ### 3. Get the passphrase ```bash docker logs nextcloud-aio-mastercontainer 2>&1 ``` If it doesn't appear in logs, fetch it from the setup page: ```bash curl -kLs https://localhost:8080 | grep -oP 'initial-password.*?<\/span>' | sed 's/.*">//' | sed 's/<.*//' ``` ### 4. Configure Caddy reverse proxy Caddy must be installed and configured to proxy HTTPS on port 443 to AIO Apache on port 11000. Install Caddy (if not already installed): ```bash curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' -o /tmp/caddy-gpg.key sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg /tmp/caddy-gpg.key curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' -o /tmp/caddy-repo.list sudo cp /tmp/caddy-repo.list /etc/apt/sources.list.d/caddy-stable.list sudo apt update && sudo apt install -y caddy ``` Provision a Tailscale HTTPS certificate (publicly trusted, from Let's Encrypt): ```bash sudo tailscale cert \ --cert-file /etc/caddy/certs/tailscale-cert.pem \ --key-file /etc/caddy/certs/tailscale-key.pem \ pop-os.tail3fb075.ts.net sudo chown caddy:caddy /etc/caddy/certs/* sudo chmod 600 /etc/caddy/certs/tailscale-key.pem ``` Write the Caddyfile (covers Nextcloud, Nezha, and New-API on separate HTTPS ports): ```bash cat > /tmp/Caddyfile << 'EOF' https://pop-os.tail3fb075.ts.net:443 { reverse_proxy localhost:11000 tls /etc/caddy/certs/tailscale-cert.pem /etc/caddy/certs/tailscale-key.pem } https://pop-os.tail3fb075.ts.net:8443 { reverse_proxy localhost:8008 tls /etc/caddy/certs/tailscale-cert.pem /etc/caddy/certs/tailscale-key.pem } https://pop-os.tail3fb075.ts.net:3443 { reverse_proxy localhost:3000 tls /etc/caddy/certs/tailscale-cert.pem /etc/caddy/certs/tailscale-key.pem } EOF sudo cp /tmp/Caddyfile /etc/caddy/Caddyfile sudo systemctl reload caddy ``` | Service | HTTPS URL | Backend | Why this port | |---|---|---|---| | Nextcloud | `https://pop-os.tail3fb075.ts.net` (443) | localhost:11000 | Standard HTTPS port | | Nezha Dashboard | `https://pop-os.tail3fb075.ts.net:8443` | localhost:8008 | 8443 = HTTPS equivalent of 8008 | | New-API | `https://pop-os.tail3fb075.ts.net:3443` | localhost:3000 | 3443 = HTTPS equivalent of 3000 | **Why separate ports instead of path-based routing (`/nezha/`, `/api/`)?** Both Nezha and New-API are full web apps with their own routing and assets served at absolute paths (e.g. `/logo.png`, `/static/js/...`). Path-prefix proxying with `uri strip_prefix` breaks these apps because asset requests go to the wrong path. Separate ports avoid this entirely -- each app thinks it's at the root. The Tailscale cert is publicly trusted -- no browser warnings, no need to install a local CA on client devices. ### Custom Domain (future) When you have your own domain, you can map it to these services in two ways: 1. **Cloudflare Tunnel (public access):** `cloudflared` routes `app.yourdomain.com` → `localhost:PORT`. Anyone on the internet can access it (add Cloudflare Access for auth). See the [Cloudflare Tunnel section](#cloudflare-tunnel-for-future-public-services) below. 2. **DNS CNAME (private, Tailscale-only):** Add a CNAME record `nextcloud.yourdomain.com` → `pop-os.tail3fb075.ts.net`. Still requires Tailscale to reach, but gives a nicer URL. Would need a separate TLS cert for the custom domain (e.g. via Cloudflare Origin cert or Let's Encrypt with DNS challenge). **Cert renewal:** Tailscale certs are valid for 90 days. Re-run the `tailscale cert` command and reload Caddy to renew. Consider adding a cron job: ```bash # Add to root's crontab: sudo crontab -e 0 3 1 * * tailscale cert --cert-file /etc/caddy/certs/tailscale-cert.pem --key-file /etc/caddy/certs/tailscale-key.pem pop-os.tail3fb075.ts.net && systemctl reload caddy ``` ### 5. Complete the AIO setup wizard 1. Open `https://100.78.69.20:8080` (accept the self-signed cert warning) - Chrome: type `thisisunsafe` on the warning page - Safari: Show Details → visit this website - Firefox: Advanced → Accept the Risk and Continue 2. Enter the passphrase 3. When asked for domain, enter: `pop-os.tail3fb075.ts.net` 4. Select optional containers, start the stack 5. Wait for all containers to come up (several minutes on first run) ### 6. Access Nextcloud - **Nextcloud Web:** `https://pop-os.tail3fb075.ts.net` (trusted cert, green lock) - **Nezha Dashboard:** `https://pop-os.tail3fb075.ts.net:8443` - **New-API:** `https://pop-os.tail3fb075.ts.net:3443` - **Desktop client:** server address `pop-os.tail3fb075.ts.net` - **Mobile app (iPhone auto-upload):** server address `pop-os.tail3fb075.ts.net` -- works on any wifi or mobile data as long as Tailscale is connected - **Adding new devices:** install Tailscale on the device and sign in with the same account; remove access from the [Tailscale admin console](https://login.tailscale.com/admin/machines) --- ## Reset Only (Keep Data Volumes) Use this when you forget the passphrase but want to keep your Nextcloud data. Only the master config is reset: ```bash docker stop nextcloud-aio-mastercontainer docker rm nextcloud-aio-mastercontainer docker volume rm nextcloud_aio_mastercontainer ``` Then relaunch with the same docker run command from step 2 above. The child containers and their data volumes are preserved. **Warning:** If you also need to reset the database, you must remove ALL volumes (follow the full "Fresh Deploy" procedure) or the credentials will mismatch. --- ## Verification ```bash # Master container healthy docker ps --filter name=nextcloud-aio-mastercontainer --format "{{.Names}}\t{{.Status}}" # All child containers running docker ps --filter name=nextcloud-aio --format "table {{.Names}}\t{{.Status}}" # Caddy serving on 443 sudo ss -tlnp | grep ':443' # HTTPS works via FQDN (no -k needed -- cert is trusted) curl -I https://pop-os.tail3fb075.ts.net # AIO admin interface curl -kI https://100.78.69.20:8080 # Env vars correct docker inspect nextcloud-aio-mastercontainer --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -E 'APACHE|SKIP' ``` ## Cloudflare Tunnel (for future public services) `cloudflared` (v2026.3.0) is installed on the VM for when you want to expose services publicly through a custom domain. Cloudflare Tunnel is free, requires no port forwarding, and hides the server's real IP. **Prerequisites:** A domain with DNS managed by Cloudflare. ### Setup (once you have a domain) ```bash # Authenticate (opens a browser link to authorize) cloudflared tunnel login # Create a tunnel cloudflared tunnel create homelab # Create config mkdir -p ~/.cloudflared cat > ~/.cloudflared/config.yml << 'EOF' tunnel: homelab credentials-file: /home/sze/.cloudflared/.json ingress: - hostname: app.yourdomain.com service: http://localhost:3000 - service: http_status:404 EOF # Add DNS record automatically cloudflared tunnel route dns homelab app.yourdomain.com # Run the tunnel cloudflared tunnel run homelab ``` To run as a persistent service: ```bash sudo cloudflared service install sudo systemctl enable --now cloudflared ``` Multiple services can share one tunnel -- add more `hostname` entries to the ingress rules in `config.yml`. --- ## Troubleshooting | Issue | Fix | |---|---| | `password authentication failed for user "oc_nextcloud"` | Credential mismatch -- remove ALL volumes and redeploy fresh | | Caddy returns 502 | AIO Apache child not running yet -- check `docker ps` and wait for containers to start | | Let's Encrypt errors in Apache logs | Expected and harmless -- Caddy handles TLS, not AIO's Apache | | Can't reach from another device | Ensure Tailscale is connected on both devices: `tailscale status` | | Browser rejects cert on port 8080 | Use Tailscale IP (`100.78.69.20`), not the FQDN; bypass cert warning (8080 still uses self-signed) | | Browser warns on main Nextcloud URL | Re-run `tailscale cert` -- cert may have expired (90-day validity) | | Works at home but not on other wifi | Tailscale must be connected on the client device -- check the Tailscale app | | Forgot passphrase | Follow "Reset Only" procedure above | | Need full clean start | Follow "Fresh Deploy" procedure -- removes all data |