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
- 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.
- 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.
- 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:
| Approach | Notes |
|---|---|
| VPN back home (WireGuard on the UDM, or Tailscale) | Recommended. Nothing is exposed; the phone simply joins your network. |
| Cloudflare Tunnel | Real hostname, real certificate, no inbound port. Set TRUST_PROXY=true. |
| Your own reverse proxy | Caddy, nginx or Traefik terminating TLS. Set TRUST_PROXY=true, and turn off response buffering on /v1/events or the door state will lag. |
| Port forwarding | No. 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: