Garage Opener

Self-hosting the bridge

The bridge is one small service in a container. It talks to your Protect console, keeps the door state machine, serves the app and runs the alert rules. It runs on a NAS, a Raspberry Pi or any box with Docker, on amd64 and arm64.

You don't have to run it. The app connects straight to your Protect console and works on your home network with nothing else installed. Run the bridge if you want always-on alert rules, an exportable activity log, family invites, or background-reliable Shortcuts.

Before you start

  1. Set one relay output to Pulse. In Protect, open the relay and configure the output that is wired to your opener's push-button terminals as Pulse, type garage door. Note which output it is: the numbering in the app starts at 1, but the API's own ids start at 0.
  2. Mount the sensor and set its mount type to Garage. This is what the bridge reads to decide whether the door is open. If the mount type is anything else, auto-discovery will not pair it.
  3. Create a Protect API key. Console → Settings → Control Plane → Integrations → Create API Key. Copy it once; it is not shown again.

    This key can read every camera on your console. Treat it like a password to your house, because functionally that is what it is.

Run it

Create a directory, drop in a docker-compose.yml and a .env, and start it:

# .env: the five things that matter
GARAGE_BRIDGE_TAG=latest                # see "keeping it up to date" below
PROTECT_URL=https://192.168.1.1         # your console, with https://
PROTECT_API_KEY=…                       # the key you just created
BRIDGE_TOKENS=$(openssl rand -hex 24)   # admin token: generate it, don't invent it
WEBHOOK_SECRET=$(openssl rand -hex 24)  # path secret for the Alarm Manager webhook
TZ=Europe/Zurich
docker compose up -d
curl -s localhost:8787/healthz

You should get {"ok":true,"ready":true,…}, with the version that is actually running. If ready is false, the bridge is up but hasn't reached the console yet, and the message says why.

A note on tokens

BRIDGE_TOKENS is the credential that opens your garage door. The bridge refuses to start on anything shorter than 24 characters, or on anything guessable. Generate it. Your family's phones do not need an entry here. They join by invite, below.

Host networking, or not

The app finds the bridge over Bonjour, and multicast does not cross Docker's bridge network, so the reference compose uses network_mode: host. If that isn't available to you, for instance on Docker Desktop for macOS or Windows, publish 8787:8787 instead, set BONJOUR=false, and type the address into the app. Everything except discovery is identical.

Pair your phone

Don't copy your admin token into the app. While no phone has joined, the bridge prints a single-use invite at startup:

docker logs garage-bridge | tail -20

Paste the garageopener://join… link, or just the code, into the app's Family → Join sheet. It's good for fifteen minutes; restart the container for a fresh one. Once a phone has joined, the banner stops and any further phones are invited from the app itself.

Let the console tell the bridge about the door

The bridge polls, but webhooks make it react immediately. In Protect, create two Alarm Manager rules, sensor opened and sensor closed, both posting to:

http://<bridge-host>:8787/v1/webhooks/alarm-manager/<WEBHOOK_SECRET>

The console has to be able to reach that address; it is on the same network as the bridge, so this needs nothing from the internet. A wrong secret returns a bare 404, so if the rules appear to do nothing, check the secret first.

Certificates

Your console presents a self-signed certificate, so the default is PROTECT_TLS=insecure, which is fine on a LAN. Pin it properly once you're running:

openssl s_client -connect 192.168.1.1:443 -servername 192.168.1.1 </dev/null 2>/dev/null \
  | openssl x509 -noout -fingerprint -sha256

Put the result in PROTECT_TLS=fingerprint:<sha256>. If your console has a real certificate, use system. The bridge refuses to start with insecure if PROTECT_URL is not a LAN address, because that would expose your API key to anyone on the path.

Reaching it from outside the house

Set PUBLIC_URL to whatever address the phone will use, since that is what invite links carry. Then pick one:

ApproachNotes
VPN back home (WireGuard on the UDM, or Tailscale)Recommended. Nothing is exposed; the phone simply joins your network.
Cloudflare TunnelReal hostname, real certificate, no inbound port. Set TRUST_PROXY=true.
Your own reverse proxyCaddy, nginx or Traefik terminating TLS. Set TRUST_PROXY=true, and turn off response buffering on /v1/events or the door state will lag.
Port forwardingNo. This puts a device that opens your garage on the public internet behind one token, usually without TLS.

TRUST_PROXY matters more than it looks. Without it, every request appears to come from the proxy, so the per-address limit on invite claims becomes five a minute for your entire household. With it set when nothing in front strips X-Forwarded-For, a caller can forge its own address, so set it only when a proxy you control is genuinely there.

Keeping it up to date

latest is the default and is the right choice for most people. Docker never updates a container on its own, so latest simply means you get the current release the next time you choose to pull. That keeps you on security fixes without having to follow releases.

docker compose pull && docker compose up -d
curl -s localhost:8787/healthz   # check the version that came down

Pin an exact version instead, GARAGE_BRIDGE_TAG=0.4.1, if you want upgrades to be a decision rather than a side effect of pulling, or if you run something that pulls automatically such as Watchtower. With latest and an automatic updater together, a release that needs a configuration change would restart your bridge into a refusal to start while you are not looking.

Either way, read the changelog before a pull. Anything that requires you to touch your configuration is listed under Action required, and the bridge fails loudly at startup rather than running in a degraded state.

Source and licence

The bridge and its OpenAPI contract are open source under Apache-2.0. You are running it on your own hardware with a credential to your console, so you should be able to read it rather than take it on trust, including the security review carried out before it was published.

The iOS app is not open source, and neither is the cloud-alerts service. That service is something we operate rather than something you run, it holds no credential to your console, and it cannot open or close anything. What it stores is set out in full on the privacy page.

Out of scope

One door per bridge. UniFi Protect only, with no other relays and no other hubs. No Home Assistant integration. No Android. A non-Docker install (Node 22 directly) will run but is not supported: Docker is the tested path and the only one the instructions cover.

Stuck? The FAQ covers the common ones, and support@garageopener.app reaches a person.

Last updated: