Homelab Hand Docs
Administration

Operations & troubleshooting

Deploying with deploy.sh, Cloudflare Tunnel and Access, upgrades, logs and fixes for common problems.

Deploying and updating with deploy.sh

deploy.sh installs or updates the hub on a server over SSH from your workstation. Every run:

  1. Copies the codeto DEPLOY_DIR (default /opt/htech-hub) with tar over SSH. No rsync is needed, and build output, the iOS project and git history are left out, so the transfer stays small. The new code is unpacked next to the old and swapped in, so an interrupted transfer never leaves a half-copied folder.
  2. Creates .envwith a random HUB_SECRET_KEY, only on the first deploy. It's never overwritten.
  3. Writes docker-compose.server.ymlwhich publishes HUB_PORT (default 8756) and the apps gateway ports, mounts the Docker socket, sets HUB_AUTO_LOCAL=1 and the origin, and opts out of Watchtower (the image is built locally).
  4. Builds and startsdocker compose up -d --build, prunes old images and waits for /healthz.

Accounts, environments, settings and stacks live in the htech-hub_hub-data volume and survive every deploy.

One-time setup

ssh-keygen -t ed25519 -f ~/.ssh/htech_deploy -N ""
ssh-copy-id -i ~/.ssh/htech_deploy.pub root@SERVER     # you type the password

Deploy or update

DEPLOY_HOST=root@SERVER ./deploy.sh
VariableDefault
DEPLOY_DIR/opt/htech-hub
HUB_PORT8756
DEPLOY_KEY~/.ssh/htech_deploy
HUB_PUBLIC_URL– (see below)

Non-root users need passwordless sudo for Docker.

UniFi IPS and big transfers. If your UniFi gateway runs intrusion prevention, a large or unusual SSH transfer can be flagged and your workstation blocked. If SSH suddenly times out after a deploy, check UniFi → Insights / Security for a block on your IP and allow it. Keeping deploys small (the default) avoids this.

Public https address

By default deploy.sh sets HUB_ORIGIN=http://<ip>:<port>. Once the hub is behind HTTPS, deploy with:

HUB_PUBLIC_URL=https://hub.example.com DEPLOY_HOST=root@SERVER ./deploy.sh

This sets HUB_ORIGIN, HUB_RP_ID and secure cookies, which passkeys need. From then on, sign in through the public URL.

Cloudflare Tunnel

  1. Add a public hostnameCloudflare Zero Trust → Networks → Tunnels → your tunnel → Public Hostname → Add: hostname hub.example.com, service HTTP → htech-hub:8080.
  2. Run cloudflaredon the hub's Docker network:
    docker run -d --name cloudflared --restart unless-stopped --network htech-hub_default \
      cloudflare/cloudflared:latest tunnel --no-autoupdate run --token <TUNNEL_TOKEN>
    If cloudflared already runs elsewhere on your LAN, skip this and set the service to http://<server-ip>:8756.
  3. Apps gateway (optional)Add a second hostname, e.g. apps.example.com → http://<server-ip>:8757, and enter it under Settings → General → Apps gateway address.
  4. Redeploywith HUB_PUBLIC_URL=https://hub.example.com.

WebSockets (terminals, agents, Hawser Edge, live map and camera video) work through tunnels out of the box.

Cloudflare Access

Protecting the hostname with Cloudflare Access is recommended: nobody reaches the hub's sign-in page without passing your identity provider first. Machines still need a way through:

ClientHow
Edge agents, Hawser EdgeA Bypass policy for /api/agent/ws and /api/hawser/connect. These are authenticated by the hub's own tokens.
iPhone / iPad / Mac appA Service Auth policy with a service token, entered in the app (see the apps).
n8n, MCP clients, scriptsA service token sent as CF-Access-Client-Id / CF-Access-Client-Secret headers, rather than bypassing /api or /mcp.
Google / Apple sign-inNothing extra. The callback happens in your browser, after Access.
Apps gatewayPut its hostname in the same Access application, so one sign-in covers both.

Access has its own session length (e.g. 24 hours). The hub's own session is 12 hours by default (HUB_SESSION_HOURS). Whichever ends first sends you to sign in again. That's expected, not a sign the hub is unprotected.

Backing up the hub

  • Easiest: a configuration backup, daily to Google Drive or as a downloaded .hhbak, restorable from the setup screen.
  • By hand: back up both the data volume and .env:
    docker run --rm -v htech-hub_hub-data:/d -v $PWD:/b alpine tar czf /b/hub-data.tgz -C /d .
    cp /opt/htech-hub/.env ~/hub.env.backup
    To restore, put .env back, untar the volume, then deploy.

Upgrades

The database is SQLite (/data/hub.db). New tables and columns are added automatically at start-up, so upgrades need no manual steps. Downgrades aren't supported. If the page looks odd after an update, hard-refresh. Static files are cache-busted, but some proxies cache aggressively.

Logs and health

TaskCommand
Follow logsdocker logs -f htech-hub
Healthcurl http://SERVER:8756/healthz
Shell inside the hubdocker exec -it htech-hub sh
Resource usedocker stats htech-hub

Troubleshooting

SymptomLikely cause and fix
Environment offline: Agent not connectedThe edge agent can't reach the hub. Check HUB_URL, the firewall, WebSocket support on your proxy, and the Access bypass. Use AGENT_INSECURE=1 / TLS_SKIP_VERIFY=true for self-signed certificates.
SSH host key changedThe host was rebuilt. Verify it, then ⋯ → Reset pinned SSH key.
Synology: certificate changedDSM's certificate was replaced or renewed. With SSH set up, renewals are accepted automatically; otherwise tick Forget the saved certificate.
Synology: 2-step verification neededThe trusted-device token was removed or expired. Enter a code on the NAS card.
Stacks / updates fail on socket or API environmentsThe compose helper image (docker:cli) couldn't be pulled. Check the host's internet/registry access, or set HUB_COMPOSE_IMAGE.
Docker API via curl failedInstall curl on the host (SSH/agent image updates use it).
Image check shows ? check failedRegistry rate limits or no internet from the host. Hover the badge for details.
Passkey button does nothingThe page isn't HTTPS on HUB_RP_ID (or localhost). Passkeys can't work on a bare IP.
Device stuck in Not linked on the mapThe hub can't match it by MAC/IP/hostname. Add UniFi, or make the environment's hostname match.
Terminal opens then closesThe container has no sh, or the connection doesn't support terminals (use the command console).
An app opens in a refresh loop through the hubThe app wants to be served at /. Use the LAN route, or ✎ the tile to open it directly. Admin apps like Nginx Proxy Manager open on their admin port automatically.
App unreachable remotelyExternal app access is off, or no apps gateway address is set.
No alert when something went downCheck the webhook's event list and minimum severity, and look for notify.failed in the event log. Use the webhook's Test button.
Cameras: 429 Too Many RequestsProtect rate-limited the hub. It backs off by itself. If it persists, check for other tools polling Protect at the same time.
App: HTTP error 403 through CloudflareCloudflare's bot protection or an Access policy blocked the request. Add the service token policy, and check Security → Events in Cloudflare.
Signed out after some hoursThe hub or Access session ended (12 h by default). Sign in again, or raise HUB_SESSION_HOURS.

FAQ

Does it phone home?

No. There's no telemetry and no cloud account. The only outbound connections are the ones you configure (registries for image checks, your notification services, cloud backup destinations, Google/Apple sign-in), plus dashboard-icons for app icons, which are cached.

Can I run it without Docker socket access?

Yes. Don't mount the socket and manage the hub's own host over SSH like any other environment.

How many environments can it handle?

Dozens. Polling is concurrent and lightweight. A lab with Proxmox, two NAS units, UniFi, CrowdSec, cameras and a dozen app integrations uses about 140 MB of RAM.

Does it replace Portainer / Uptime Kuma / Dockhand?

It covers the day-to-day parts of all three in one place, and can work alongside them: it reads Portainer- and Dockhand-deployed stacks and talks to Hawser agents.