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

VariablePurpose
DOMAINPublic hostname (used by the bundled Caddy compose file for its certificate).
NUXT_SESSION_PASSWORDSession-cookie sealing secret, ≥32 chars. Generate with openssl rand -hex 32.
POSTGRES_PASSWORDPassword for the bundled PostgreSQL container.
KVELLMAN_IMAGEA prebuilt image to pull instead of building on the host.
TELEMETRY_ENABLED / TELEMETRY_RETENTION_DAYSUsage telemetry, off by default.
CATALOG_SYNC_ENABLED / CATALOG_SYNC_INTERVAL_HOURSScheduled upstream-catalog sync, off by default.
GITHUB_TOKENRaises the winget-pkgs import rate limit.
TRAEFIK_NETWORK / TRAEFIK_ENTRYPOINT / TRAEFIK_CERTRESOLVEROnly 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.md in the open-core repository — this page covers the shape of it.