Deployment
Run the origin behind any HTTPS-terminating reverse proxy — the requirement, and three worked examples.
What the application needs
- PostgreSQL — the origin's entire state.
- A persistent volume — for installers you choose to store on the origin.
- Something in front that terminates HTTPS. The application itself is a plain HTTP server on port 3000; it does not handle TLS.
HTTPS is not optional
winget refuses a REST source that isn't served over HTTPS with a certificate the client trusts —
including localhost on recent winget versions. An internal CA is fine as long as your clients
trust it; an untrusted self-signed certificate is not.
The reverse proxy
kvellman listens on port 3000 and speaks plain HTTP. It does not terminate TLS, and it does not care what sits in front of it. Anything that can terminate HTTPS and forward to an HTTP upstream works: nginx, HAProxy, Apache, an appliance, your existing ingress.
Whatever you use must:
- forward to the application's port 3000
- serve a certificate the clients trust
- pass the original host and protocol through (
Host,X-Forwarded-Proto) - not buffer or limit response bodies — installers are large
- allow long-running downloads (generous read/send timeouts)
That's the entire integration surface.
Worked examples
A compose stack with a bundled proxy
The default docker-compose.yml bundles Caddy, which obtains a Let's Encrypt certificate
automatically. Use it when the host is publicly reachable and you don't already run a proxy.
cp .env.deploy.example .env.deploy # set DOMAIN, NUXT_SESSION_PASSWORD, POSTGRES_PASSWORD
docker compose --env-file .env.deploy up -d
Routing through an ingress you already run
docker-compose.traefik.yml ships without a bundled proxy — the application joins your existing
Traefik network and is routed by labels. Use it when ports 80/443 already belong to something
else.
docker compose -f docker-compose.traefik.yml --env-file .env.deploy up -d
Any other proxy
The application doesn't care. Point any HTTPS-terminating proxy at port 3000 — for example nginx:
server {
listen 443 ssl;
server_name your-domain;
location / {
proxy_pass http://kvellman:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_request_buffering off;
client_max_body_size 0;
}
}
Configuration
| Variable | Purpose |
|---|---|
DOMAIN | Public hostname (used by the bundled Caddy compose file for its certificate). |
NUXT_SESSION_PASSWORD | Session-cookie sealing secret, ≥32 chars. Generate with openssl rand -hex 32. |
POSTGRES_PASSWORD | Password for the bundled PostgreSQL container. |
KVELLMAN_IMAGE | A prebuilt image to pull instead of building on the host. |
TELEMETRY_ENABLED / TELEMETRY_RETENTION_DAYS | Usage telemetry, off by default. |
CATALOG_SYNC_ENABLED / CATALOG_SYNC_INTERVAL_HOURS | Scheduled upstream-catalog sync, off by default. |
GITHUB_TOKEN | Raises the winget-pkgs import rate limit. |
TRAEFIK_NETWORK / TRAEFIK_ENTRYPOINT / TRAEFIK_CERTRESOLVER | Only for docker-compose.traefik.yml. |
Running it without the compose files
Bringing your own PostgreSQL and orchestration is fine: run the image, point DATABASE_URL at your
database, and expose port 3000 to your proxy. Migrations run automatically at startup — there's no
separate migration step to script.
Backups and upgrades
An installation's entire state is the PostgreSQL database plus the installer volume — back up both. Upgrading is pulling a newer image tag and restarting; migrations run on start.
Telemetry
Off by default, enabled by environment variable. Records searches, manifest fetches and installer downloads, rolled up hourly inside the application process itself, with raw events purged after the retention period you configure. No queue, cache, or additional service is required to run it. The rolled-up numbers surface in the admin UI as dashboards, broken down by site and by winget client version.
Air-gapped operation
The application makes no outbound calls to run, and license verification is entirely local, so it
operates fully disconnected. The upstream-catalog sync and manifest import from GitHub do need
internet access — leave CATALOG_SYNC_ENABLED off and import manifests by hand if the host has
none.
The full step-by-step guide (origin, edge node under WSL, TLS for LAN/localhost, the registry workflow) is in
DEPLOYMENT.mdin the open-core repository — this page covers the shape of it.