Skip to content

Commit 866e532

Browse files
Merge pull request #5 from VocaHQ/fix/compose-env-passthrough-and-config-drift
fix: pass gateway settings through Compose and reconcile env docs
2 parents 5807da5 + 50c0e40 commit 866e532

9 files changed

Lines changed: 153 additions & 46 deletions

File tree

.dockerignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,19 @@ data
99
*.sqlite*
1010
*.wav
1111
*.log
12+
13+
# A populated .env holds the bearer token. No Dockerfile copies it, but keeping
14+
# it out of the build context means it is never sent to the daemon or captured
15+
# in a build cache layer.
16+
.env
17+
.envrc
18+
19+
# Never needed by a build stage; .git in particular is large and changes on
20+
# every commit, which would invalidate the context for no reason.
21+
.git
22+
.gitignore
23+
.github
24+
docs
25+
scripts
26+
LICENSE
27+
justfile

.env.example

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
# Bearer token the phone apps present on every request. At least 32 characters.
1414
# Compose passes it to the container as a secret, not as an environment
1515
# variable. Generate one with: openssl rand -hex 32
16+
#
17+
# Fill this in, or append a second VOCAGATEWAY_TOKEN= line as shown above —
18+
# Compose uses the last assignment when a key repeats, so the appended value
19+
# wins over this empty placeholder.
1620
VOCAGATEWAY_TOKEN=
1721

1822
# Host interface and port Docker publishes the gateway on.
@@ -29,10 +33,36 @@ VOCAGATEWAY_PUBLISH_PORT=8765
2933
# it.
3034
#VOCAGATEWAY_NETWORK_MODE=host
3135

36+
# Address the pairing QR should encode. On the default bridge network the
37+
# container only sees its own private bridge IP (172.x), never the host's LAN
38+
# NIC, so auto-discovery cannot produce a phone-reachable address — set this to
39+
# the URL the phone should actually use. This is the alternative to
40+
# VOCAGATEWAY_NETWORK_MODE=host, and the only option on Docker Desktop.
41+
# VOCAGATEWAY_PAIRING_URL is an accepted alias, checked second.
42+
#VOCAGATEWAY_PUBLIC_URL=http://192.168.1.20:8765
43+
3244
# Use a prebuilt multi-architecture image instead of building the local tag.
3345
#VOCAGATEWAY_IMAGE=ghcr.io/your-user/vocaphone-gateway:latest
3446

3547
# Serve the Swagger UI at /docs and the schema at /openapi.json. Off by
3648
# default: the schema and UI are extra surface area, so enable this only for
3749
# local development.
3850
#VOCAGATEWAY_DEBUG=true
51+
52+
# Listener inside the container. Only meaningful under
53+
# VOCAGATEWAY_NETWORK_MODE=host, where the container binds the host directly and
54+
# VOCAGATEWAY_PUBLISH_HOST/_PORT are discarded; on the default bridge network
55+
# the gateway must keep listening on all container interfaces for the published
56+
# port to reach it.
57+
#VOCAGATEWAY_BIND_HOST=0.0.0.0
58+
#VOCAGATEWAY_PORT=8765
59+
60+
# Pin an engine instead of letting `auto` pick the first runnable one. The
61+
# container can run sherpa-onnx, faster-whisper, moonshine, and whisper.cpp;
62+
# vocamac, handy, whisperkit, and mlx-audio are macOS-only and rejected here.
63+
#VOCAGATEWAY_ENGINE=auto
64+
65+
# How long a failed session's audio is kept so the phone can retry, and whether
66+
# audio is deleted immediately after a successful transcription.
67+
#VOCAGATEWAY_RETENTION_HOURS=24
68+
#VOCAGATEWAY_DELETE_SUCCESSFUL_AUDIO=true

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,5 @@ USER vocaphone
6161
EXPOSE 8765
6262
VOLUME ["/data"]
6363
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
64-
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8765/health/live', timeout=3)"]
64+
CMD ["python", "-c", "import os, urllib.request; port = os.environ.get('VOCAGATEWAY_PORT', '8765'); urllib.request.urlopen(f'http://127.0.0.1:{port}/health/live', timeout=3)"]
6565
ENTRYPOINT ["vocaphone-server"]

Dockerfile.cuda

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,5 @@ USER vocaphone
5656
EXPOSE 8765
5757
VOLUME ["/data"]
5858
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
59-
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8765/health/live', timeout=3)"]
59+
CMD ["python", "-c", "import os, urllib.request; port = os.environ.get('VOCAGATEWAY_PORT', '8765'); urllib.request.urlopen(f'http://127.0.0.1:{port}/health/live', timeout=3)"]
6060
ENTRYPOINT ["vocaphone-server"]

Dockerfile.vulkan

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,5 @@ USER vocaphone
5656
EXPOSE 8765
5757
VOLUME ["/data"]
5858
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
59-
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8765/health/live', timeout=3)"]
59+
CMD ["python", "-c", "import os, urllib.request; port = os.environ.get('VOCAGATEWAY_PORT', '8765'); urllib.request.urlopen(f'http://127.0.0.1:{port}/health/live', timeout=3)"]
6060
ENTRYPOINT ["vocaphone-server"]

README.md

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -162,18 +162,27 @@ CLI. The same Dockerfile builds on Linux `amd64` and `arm64`.
162162

163163
```sh
164164
umask 077
165-
printf 'VOCAGATEWAY_TOKEN=%s\n' "$(openssl rand -hex 32)" > .env
166-
printf 'VOCAGATEWAY_PUBLISH_HOST=127.0.0.1\n' >> .env
167-
printf 'VOCAGATEWAY_PUBLISH_PORT=8765\n' >> .env
165+
cp .env.example .env
166+
printf 'VOCAGATEWAY_TOKEN=%s\n' "$(openssl rand -hex 32)" >> .env
168167
docker compose up --detach --build
169168
docker compose ps
170169
curl --fail http://127.0.0.1:8765/health/live
171170
```
172171

172+
[`.env.example`](.env.example) is the annotated template: it already sets the
173+
loopback publication defaults and comments out every other supported setting,
174+
so starting from it is how you find out what is tunable. Appending the token
175+
overrides the empty `VOCAGATEWAY_TOKEN=` placeholder it ships with — Compose
176+
takes the last assignment of a repeated key.
177+
173178
The token is provided as a Compose secret rather than a container environment
174179
variable. Models, configuration, and the SQLite database persist in the
175180
`vocagateway_vocagateway-data` named volume mounted at `/data`.
176181

182+
Before pairing a phone, read the bridge-network note below: on the default
183+
network the QR cannot auto-discover a reachable address, and
184+
`VOCAGATEWAY_PUBLIC_URL` in `.env` is what fixes it.
185+
177186
The container is live before a model is installed, so `/health/ready` initially
178187
returns `503`. Open the WebUI, enter the token from `.env`, download/select a
179188
recommended sherpa-onnx, Moonshine, or faster-whisper model, and check again:
@@ -190,12 +199,16 @@ firewall. Never expose port 8765 to the public internet.
190199
The default bridge network also hides the host's real LAN address from the
191200
gateway's own address auto-discovery (used for the pairing QR): the container
192201
only ever sees its private bridge IP, not the host's Wi-Fi/Ethernet interface.
193-
On Linux Docker Engine (not Docker Desktop on macOS/Windows), set
194-
`VOCAGATEWAY_NETWORK_MODE=host` in `.env` instead so the container shares the
195-
host's network namespace and discovery finds the real `192.168.x.x` address.
196-
This ignores `VOCAGATEWAY_PUBLISH_HOST`/`PORT` — the container binds directly on
197-
the host per `VOCAGATEWAY_BIND_HOST`/`VOCAGATEWAY_PORT`, so lock down port 8765
198-
with the host firewall first.
202+
Two ways out, both set in `.env`:
203+
204+
- `VOCAGATEWAY_PUBLIC_URL=http://192.168.1.20:8765` names the address the phone
205+
should use and skips discovery entirely. This works everywhere, including
206+
Docker Desktop on macOS and Windows.
207+
- `VOCAGATEWAY_NETWORK_MODE=host`, on Linux Docker Engine only, shares the
208+
host's network namespace so discovery finds the real `192.168.x.x` address by
209+
itself. This ignores `VOCAGATEWAY_PUBLISH_HOST`/`PORT` — the container binds
210+
directly on the host per `VOCAGATEWAY_BIND_HOST`/`VOCAGATEWAY_PORT`, so lock
211+
down port 8765 with the host firewall first.
199212

200213
## WebUI
201214

@@ -387,14 +400,28 @@ uv run vocaphone-server
387400
| `VOCAGATEWAY_CONFIG_FILE` | `~/.config/vocagateway/config.json` | `/data/config/config.json` | WebUI engine/model choice |
388401
| `VOCAGATEWAY_ENGINE` | `auto` | `auto` | `auto`, `vocamac`, `handy`, `mlx-audio`, `whisperkit`, `sherpa-onnx`, `faster-whisper`, `moonshine`, or `whisper.cpp` |
389402
| `VOCAGATEWAY_WHISPER_BINARY` | `/opt/homebrew/bin/whisper-cli` | `/usr/local/bin/whisper-cli` | `whisper.cpp` executable |
390-
| `VOCAGATEWAY_WHISPER_MODEL` | base model path | base model path | Fallback `whisper.cpp` model |
403+
| `VOCAGATEWAY_WHISPER_MODEL` | `~/.local/share/whisper.cpp/models/ggml-base.en.bin` | same, and normally absent | Fallback `whisper.cpp` model used only when no model is selected in the WebUI |
391404
| `VOCAGATEWAY_WHISPERKIT_BINARY` | `whisperkit-cli` | unavailable | WhisperKit executable |
392405
| `VOCAGATEWAY_VOCAMAC_APP` | `/Applications/VocaMac.app` | unavailable | Optional VocaMac app bundle |
393406
| `VOCAGATEWAY_VOCAMAC_MODEL` | unset | unset | Pin a VocaMac model instead of following the app's choice |
407+
| `VOCAGATEWAY_HANDY_BINARY` | `/Applications/Handy.app/Contents/MacOS/handy` | unavailable | Optional Handy application binary |
408+
| `VOCAGATEWAY_HANDY_MODEL` | unset | unset | Pin a Handy model (`owner/repository/model.gguf`) |
409+
| `VOCAGATEWAY_HANDY_FALLBACK_MODEL` | `handy-computer/whisper-base-gguf/whisper-base-Q8_0.gguf` | unavailable | Model used when the pinned Handy model is missing |
394410
| `VOCAGATEWAY_RETENTION_HOURS` | `24` | `24` | Failed-session retry retention |
395411
| `VOCAGATEWAY_DELETE_SUCCESSFUL_AUDIO` | `true` | `true` | Delete source/normalized audio after success |
396-
397-
Compose-specific variables live in `.env`:
412+
| `VOCAGATEWAY_PUBLIC_URL` | unset | unset | Address the pairing QR encodes, overriding auto-discovery |
413+
| `VOCAGATEWAY_PAIRING_URL` | unset | unset | Alias for `VOCAGATEWAY_PUBLIC_URL`, checked second |
414+
| `VOCAGATEWAY_DEBUG` | `false` | `false` | Serve the Swagger UI at `/docs` and the schema at `/openapi.json` |
415+
416+
Under Compose, `VOCAGATEWAY_BIND_HOST`, `PORT`, `ENGINE`, `RETENTION_HOURS`,
417+
`DELETE_SUCCESSFUL_AUDIO`, `PUBLIC_URL`, `PAIRING_URL`, and `DEBUG` are read
418+
from `.env` and passed into the container. `VOCAGATEWAY_TOKEN` becomes a
419+
Compose secret at `/run/secrets/vocagateway_token` rather than an environment
420+
variable. The remaining paths and binaries are fixed by the image to their
421+
container locations, and the macOS-only engine variables have no effect there.
422+
423+
Compose-only variables, which the gateway process itself never reads, also live
424+
in `.env`:
398425

399426
| Variable | Default | Purpose |
400427
| --- | --- | --- |

compose.yaml

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
name: vocagateway
22

3+
# Every gateway service takes the same settings; only the build and the device
4+
# wiring differ per profile. Keeping them in one anchor means a variable added
5+
# here reaches all four services instead of three of them.
6+
x-gateway-environment: &gateway-environment
7+
VOCAGATEWAY_TOKEN_FILE: /run/secrets/vocagateway_token
8+
VOCAGATEWAY_DEBUG: ${VOCAGATEWAY_DEBUG:-false}
9+
# Blank means "not set": the gateway ignores an empty override and falls back
10+
# to auto-discovery. On the default bridge network discovery only ever sees
11+
# the container's private bridge IP, so this is how a phone-reachable address
12+
# gets into the pairing QR without switching to host networking.
13+
VOCAGATEWAY_PUBLIC_URL: ${VOCAGATEWAY_PUBLIC_URL:-}
14+
VOCAGATEWAY_PAIRING_URL: ${VOCAGATEWAY_PAIRING_URL:-}
15+
# Listener inside the container. Only worth changing under
16+
# VOCAGATEWAY_NETWORK_MODE=host, where the container binds the host directly
17+
# and the ports mapping below is discarded.
18+
VOCAGATEWAY_BIND_HOST: ${VOCAGATEWAY_BIND_HOST:-0.0.0.0}
19+
VOCAGATEWAY_PORT: ${VOCAGATEWAY_PORT:-8765}
20+
VOCAGATEWAY_ENGINE: ${VOCAGATEWAY_ENGINE:-auto}
21+
VOCAGATEWAY_RETENTION_HOURS: ${VOCAGATEWAY_RETENTION_HOURS:-24}
22+
VOCAGATEWAY_DELETE_SUCCESSFUL_AUDIO: ${VOCAGATEWAY_DELETE_SUCCESSFUL_AUDIO:-true}
23+
324
services:
425
gateway:
526
image: ${VOCAGATEWAY_IMAGE:-vocaphone-gateway:local}
@@ -10,10 +31,8 @@ services:
1031
init: true
1132
network_mode: ${VOCAGATEWAY_NETWORK_MODE:-bridge}
1233
ports:
13-
- "${VOCAGATEWAY_PUBLISH_HOST:-127.0.0.1}:${VOCAGATEWAY_PUBLISH_PORT:-8765}:8765"
14-
environment:
15-
VOCAGATEWAY_TOKEN_FILE: /run/secrets/vocagateway_token
16-
VOCAGATEWAY_DEBUG: ${VOCAGATEWAY_DEBUG:-false}
34+
- "${VOCAGATEWAY_PUBLISH_HOST:-127.0.0.1}:${VOCAGATEWAY_PUBLISH_PORT:-8765}:${VOCAGATEWAY_PORT:-8765}"
35+
environment: *gateway-environment
1736
secrets:
1837
- vocagateway_token
1938
volumes:
@@ -31,10 +50,8 @@ services:
3150
init: true
3251
network_mode: ${VOCAGATEWAY_NETWORK_MODE:-bridge}
3352
ports:
34-
- "${VOCAGATEWAY_PUBLISH_HOST:-127.0.0.1}:${VOCAGATEWAY_PUBLISH_PORT:-8765}:8765"
35-
environment:
36-
VOCAGATEWAY_TOKEN_FILE: /run/secrets/vocagateway_token
37-
VOCAGATEWAY_DEBUG: ${VOCAGATEWAY_DEBUG:-false}
53+
- "${VOCAGATEWAY_PUBLISH_HOST:-127.0.0.1}:${VOCAGATEWAY_PUBLISH_PORT:-8765}:${VOCAGATEWAY_PORT:-8765}"
54+
environment: *gateway-environment
3855
secrets: [vocagateway_token]
3956
volumes: [vocagateway-data:/data]
4057

@@ -48,10 +65,9 @@ services:
4865
init: true
4966
network_mode: ${VOCAGATEWAY_NETWORK_MODE:-bridge}
5067
ports:
51-
- "${VOCAGATEWAY_PUBLISH_HOST:-127.0.0.1}:${VOCAGATEWAY_PUBLISH_PORT:-8765}:8765"
68+
- "${VOCAGATEWAY_PUBLISH_HOST:-127.0.0.1}:${VOCAGATEWAY_PUBLISH_PORT:-8765}:${VOCAGATEWAY_PORT:-8765}"
5269
environment:
53-
VOCAGATEWAY_TOKEN_FILE: /run/secrets/vocagateway_token
54-
VOCAGATEWAY_DEBUG: ${VOCAGATEWAY_DEBUG:-false}
70+
<<: *gateway-environment
5571
NVIDIA_VISIBLE_DEVICES: all
5672
gpus: all
5773
secrets: [vocagateway_token]
@@ -67,10 +83,8 @@ services:
6783
init: true
6884
network_mode: ${VOCAGATEWAY_NETWORK_MODE:-bridge}
6985
ports:
70-
- "${VOCAGATEWAY_PUBLISH_HOST:-127.0.0.1}:${VOCAGATEWAY_PUBLISH_PORT:-8765}:8765"
71-
environment:
72-
VOCAGATEWAY_TOKEN_FILE: /run/secrets/vocagateway_token
73-
VOCAGATEWAY_DEBUG: ${VOCAGATEWAY_DEBUG:-false}
86+
- "${VOCAGATEWAY_PUBLISH_HOST:-127.0.0.1}:${VOCAGATEWAY_PUBLISH_PORT:-8765}:${VOCAGATEWAY_PORT:-8765}"
87+
environment: *gateway-environment
7488
devices:
7589
- /dev/dri:/dev/dri
7690
secrets: [vocagateway_token]

docs/deployment.md

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,20 @@ The Compose project lives in this repository root:
131131

132132
```sh
133133
umask 077
134-
printf 'VOCAGATEWAY_TOKEN=%s\n' "$(openssl rand -hex 32)" > .env
135-
printf 'VOCAGATEWAY_PUBLISH_HOST=127.0.0.1\n' >> .env
136-
printf 'VOCAGATEWAY_PUBLISH_PORT=8765\n' >> .env
134+
cp .env.example .env
135+
printf 'VOCAGATEWAY_TOKEN=%s\n' "$(openssl rand -hex 32)" >> .env
137136
docker compose up --detach --build
138137
```
139138

140-
[`.env.example`](../.env.example) is the annotated template for
141-
the same file, covering the optional image tag, network mode and Swagger UI
142-
settings. Copy it and append a generated token rather than committing either
143-
file.
139+
[`.env.example`](../.env.example) is the annotated template for the same file.
140+
It ships the loopback publication defaults uncommented and everything else
141+
commented out with an explanation: the pairing-QR address, the container
142+
listener, the engine choice, session retention, the optional image tag, network
143+
mode, and the Swagger UI. Start from it rather than writing `.env` by hand, so
144+
the options are in front of you. Never commit either file.
145+
146+
The appended token overrides the empty `VOCAGATEWAY_TOKEN=` placeholder in the
147+
template; Compose uses the last assignment when a key repeats in `.env`.
144148

145149
`VOCAGATEWAY_PUBLISH_HOST=127.0.0.1` is the safe default for Tailscale Serve. Set
146150
it to `0.0.0.0` only when direct LAN access is intentional and protected by the
@@ -228,7 +232,7 @@ Build one tag for both supported Linux architectures from the repository root:
228232
docker buildx build \
229233
--platform linux/amd64,linux/arm64 \
230234
--tag ghcr.io/your-user/vocaphone-gateway:latest \
231-
--push server
235+
--push .
232236
```
233237

234238
Set `VOCAGATEWAY_IMAGE` in `.env` to use that tag. Compose still includes a
@@ -260,9 +264,22 @@ VPN and never forward it from a router.
260264
With the default bridge network, the container only ever sees its own private
261265
bridge address (for example `172.19.0.2`), never the host's real Wi-Fi/Ethernet
262266
interface — so the pairing card's auto-discovered candidate list won't include
263-
a `192.168.x.x` address even after the change above. On Linux Docker Engine
264-
(not Docker Desktop on macOS/Windows), share the host's network namespace
265-
instead so discovery sees the real LAN IP directly:
267+
a `192.168.x.x` address even after the change above.
268+
269+
The portable fix is to stop relying on discovery and name the address the phone
270+
should use:
271+
272+
```dotenv
273+
VOCAGATEWAY_PUBLIC_URL=http://192.168.1.20:8765
274+
```
275+
276+
The gateway puts that URL first in the pairing card and encodes it in the QR.
277+
It works on every Docker flavour, including Docker Desktop on macOS and
278+
Windows, and is the only option there. `VOCAGATEWAY_PAIRING_URL` is an accepted
279+
alias, checked second.
280+
281+
On Linux Docker Engine (not Docker Desktop) you can instead share the host's
282+
network namespace so discovery sees the real LAN IP by itself:
266283

267284
```dotenv
268285
VOCAGATEWAY_NETWORK_MODE=host

docs/troubleshooting.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ deployment, also confirm Tailscale is connected and Serve is active. The
152152
recording should remain on the iPhone for Retry.
153153

154154
For a container deployment, also check `docker compose ps` from `server/` and
155-
confirm the `vocaphone_vocaphone-data` volume is still mounted.
155+
confirm the `vocagateway_vocagateway-data` volume is still mounted.
156156

157157
## A LAN hostname such as homelabone does not connect
158158

@@ -174,10 +174,13 @@ Keep the host firewall enabled. Do not use this LAN configuration to expose port
174174
If the pairing QR itself shows no LAN address to pick from (or only shows a
175175
`172.x`/bridge address), that's the same root cause: the container's default
176176
bridge network only exposes its own private interface to address
177-
auto-discovery, never the host's real LAN NIC. On Linux Docker Engine (not
178-
Docker Desktop), set `VOCAGATEWAY_NETWORK_MODE=host` in `.env` instead so
179-
the container shares the host's network namespace and discovery finds the
180-
`192.168.x.x` address directly. See [deployment.md](deployment.md#trusted-local-network).
177+
auto-discovery, never the host's real LAN NIC. Set
178+
`VOCAGATEWAY_PUBLIC_URL=http://192.168.1.20:8765` in `.env` to name the address
179+
the phone should use — this works on every Docker flavour, including Docker
180+
Desktop. On Linux Docker Engine only, `VOCAGATEWAY_NETWORK_MODE=host` is the
181+
alternative: the container shares the host's network namespace and discovery
182+
finds the `192.168.x.x` address directly. Recreate the service after either
183+
change. See [deployment.md](deployment.md#trusted-local-network).
181184

182185
## 401 unauthorized
183186

0 commit comments

Comments
 (0)