Native Windows deployment: config file, service, signed auto-update
- internal/config: .env-style config file (gpu-turnstile.env next to the exe, -config flag or GPU_TURNSTILE_CONFIG); process env overrides file. - internal/service: Windows service via golang.org/x/sys/windows/svc — graceful SCM stop, 'service install/remove' commands, restart-on-failure recovery (also applies staged updates). First external dependency, Windows-only; Linux/Docker build unaffected (go.mod stays at 1.23). - internal/update: polls the Gitea releases API, verifies the Ed25519 signature of the downloaded binary against an embedded public key (openssl-signed by CI), swaps it in next to the running exe, and once the GPU lock is idle exits with code 3 so service recovery restarts onto the new version. Dev builds and empty pubkey never update. - CI: tag builds additionally produce gpu-turnstile.exe + .sig + .sha256 attached to a Gitea release. - LOG_FILE env var so the service has somewhere to log.
This commit is contained in:
@@ -102,6 +102,12 @@ load time. Off by default.
|
||||
|
||||
## Configuration (env)
|
||||
|
||||
Configuration comes from environment variables and/or an `.env`-style
|
||||
config file (`KEY=VALUE` lines, `#` comments). File lookup order:
|
||||
`-config <path>` flag, then `GPU_TURNSTILE_CONFIG`, then
|
||||
`gpu-turnstile.env` next to the executable. Process environment variables
|
||||
override file values. A missing file is fine; a malformed one is fatal.
|
||||
|
||||
| Var | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `LISTEN_OLLAMA` | `:11434` | Ollama-facing listener |
|
||||
@@ -113,6 +119,8 @@ load time. Off by default.
|
||||
| `LLM_WAIT_TIMEOUT` | `10m` | max time an LLM request waits for the lock before 503 |
|
||||
| `WARM_MODEL` | `` | optional model to reload after an image job |
|
||||
| `LOGLEVEL` | `warn` | `info` logs every request (colored arrows in text mode), `debug` adds lock transitions. `LOG_LEVEL` is accepted as an alias |
|
||||
| `LOG_FORMAT` | `text` | `json` for structured JSON logs |
|
||||
| `LOG_FILE` | `` | append logs to this file instead of stderr (useful as a service) |
|
||||
| `UNLOAD_POLL_INTERVAL` | `500ms` | `/api/ps` poll interval while unloading |
|
||||
| `HISTORY_POLL_INTERVAL` | `1s` | `/history/<id>` poll interval while a job runs |
|
||||
| `PROBE_TIMEOUT` | `5s` | startup probe of both upstreams |
|
||||
@@ -122,10 +130,39 @@ load time. Off by default.
|
||||
| `BACKOFF_INITIAL` | `1s` | first retry wait when an upstream refuses a connection |
|
||||
| `BACKOFF_MAX` | `60s` | cap for the exponential retry backoff |
|
||||
| `PROMPT_CAPTURE_LIMIT` | `65536` | bytes of the `/prompt` response buffered to find `prompt_id` (pass-through is unaffected) |
|
||||
| `AUTO_UPDATE` | `true` | poll the Gitea releases API for signed updates |
|
||||
| `UPDATE_INTERVAL` | `6h` | auto-update check interval |
|
||||
| `UPDATE_REPO` | `https://git.rambossek.at/PUBLIC/gpu-turnstile` | repository to check for releases |
|
||||
| `UPDATE_ASSET` | `gpu-turnstile.exe` | release asset to download |
|
||||
|
||||
Startup fails fast on unparsable values. Both upstreams are probed once at
|
||||
start (`/api/version`, `/system_stats`); failure is logged, not fatal.
|
||||
|
||||
## Native Windows deployment
|
||||
|
||||
The binary runs natively on Windows (the primary deployment) as well as in
|
||||
Docker.
|
||||
|
||||
- `gpu-turnstile.exe service install [-config path]` registers an
|
||||
auto-start Windows service (needs an elevated shell). Recovery actions
|
||||
restart it 5 s after any failure. `service remove` uninstalls.
|
||||
- Use a config file (above) for the service — Windows services have no
|
||||
convenient environment. Logs go to `LOG_FILE` since there is no console.
|
||||
- **Auto-update**: on startup and every `UPDATE_INTERVAL`, the binary
|
||||
checks `UPDATE_REPO`'s latest release; if its tag is a newer `vX.Y.Z`,
|
||||
it downloads `UPDATE_ASSET` plus its `.sig` (and `.sha256` when present)
|
||||
and verifies an Ed25519 signature against the public key embedded in
|
||||
`internal/update/pubkey.go`. A verified binary is swapped in next to the
|
||||
running exe (rename-aside, allowed on Windows), and once the GPU lock is
|
||||
idle the process exits with code 3 so the service recovery restarts it
|
||||
on the new version. Interactive runs only log "restart to apply".
|
||||
`dev` builds and builds without an embedded public key never update.
|
||||
- **Signing setup (one time)**: `openssl genpkey -algorithm ed25519 -out
|
||||
private.pem`; `openssl pkey -in private.pem -pubout -out public.pem`.
|
||||
Private key → repo secret `SIGNING_KEY`; public key → committed into
|
||||
`internal/update/pubkey.go`. CI signs release binaries with
|
||||
`openssl pkeyutl -sign -rawin`.
|
||||
|
||||
## Observability
|
||||
|
||||
- `GET /healthz` on both listeners: 200 with JSON
|
||||
@@ -171,11 +208,15 @@ start (`/api/version`, `/system_stats`); failure is logged, not fatal.
|
||||
|
||||
```
|
||||
gpu-turnstile/
|
||||
cmd/gpu-turnstile/main.go # wiring, config, listeners
|
||||
cmd/gpu-turnstile/main.go # wiring, config, listeners, service + updater
|
||||
internal/lock/lock.go # two-mode lock + tests
|
||||
internal/ollama/client.go # ps / unload / warm
|
||||
internal/comfy/client.go # history poll / free
|
||||
internal/proxy/ # handlers for both listeners
|
||||
internal/metrics/ # Prometheus exposition
|
||||
internal/config/ # env + .env file configuration
|
||||
internal/update/ # signed auto-updater (public key in pubkey.go)
|
||||
internal/service/ # Windows service integration
|
||||
Dockerfile
|
||||
.gitea/workflows/ci.yml
|
||||
README.md
|
||||
@@ -197,11 +238,16 @@ are new.
|
||||
held until `/free` was called.
|
||||
- Streaming test: fake Ollama emits chunks with delays; assert the client
|
||||
receives the first chunk before the last is sent (no buffering).
|
||||
- `internal/config`: env-file parsing, precedence, fail-fast values.
|
||||
- `internal/update`: fake Gitea releases API; staged update happy path,
|
||||
tampered signature rejected, older versions and dev builds skipped.
|
||||
|
||||
## Build and CI
|
||||
|
||||
- Go 1.23+, stdlib only. `CGO_ENABLED=0`, `-ldflags="-s -w"`, version from
|
||||
`git describe` injected via `-X main.version=`.
|
||||
- Go 1.23+, `golang.org/x/sys` is the only external dependency (Windows
|
||||
service integration; not used in the Linux build). `CGO_ENABLED=0`,
|
||||
`-ldflags="-s -w"`, version from `git describe` injected via
|
||||
`-X main.version=`.
|
||||
- Dockerfile: multi-stage, final image `gcr.io/distroless/static` (or
|
||||
`scratch`), non-root user, `EXPOSE 8188 11434`,
|
||||
`ENTRYPOINT ["/gpu-turnstile"]`.
|
||||
@@ -216,8 +262,12 @@ are new.
|
||||
Login uses the repo secret `REGISTRY_TOKEN` (an access token with
|
||||
`write:package` scope) because the automatic `GITEA_TOKEN` cannot push
|
||||
packages; the username is just `gitea.actor`.
|
||||
- Release: a git tag `vX.Y.Z` produces the versioned image; the Open WebUI
|
||||
compose pins that tag. No images are built from branches.
|
||||
3. on a version tag: also build the Windows binary, sign it with OpenSSL
|
||||
(`SIGNING_KEY` secret), and attach `gpu-turnstile.exe`, `.sig` and
|
||||
`.sha256` to a Gitea release for the auto-updater.
|
||||
- Release: a git tag `vX.Y.Z` produces the versioned image and the signed
|
||||
Windows binary; the Open WebUI compose pins that tag. No images or
|
||||
binaries are built from branches.
|
||||
|
||||
## Deployment (target)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user