WiseHosting

Configuration

Full schema for config.yaml — every section, key, type, and default.

WiseHosting reads config.yaml from $CONFIG_PATH (default ./config.yaml). All sections, keys, and types are listed below — these are the same fields validated by internal/config/config.go.

If you're new to YAML

The format is whitespace-sensitive: indentation marks nesting (use spaces, not tabs). Blank fields (secret: "") are valid YAML and mean "use the default" for some keys, "fail validation" for others — see the Validation rules section below for which keys must be set.

In production, don't put real secrets in this file. Use systemd's LoadCredential mechanism (covered at the bottom of the page) so the secrets live in /etc/credstore/ with mode 0400, not in a YAML file with shell-globbing-friendly permissions.

File layout

api_server:
  host: 0.0.0.0
  port: 8080
  url: https://hosting.example.com
  secret: "<32+ char secret>"        # required (>=16 chars)
  tls_cert: ""                        # optional; if set, enables TLS 1.3
  tls_key: ""

database:
  host: localhost
  port: 5432
  name: wisehosting
  user: wisehosting
  password: "<required>"
  sslmode: disable

webapp:
  url: https://hosting.example.com

# OAuth providers — each section optional
github:
  oauth_client_id: ""
  oauth_client_secret: ""

gitlab:    { oauth_client_id: "", oauth_client_secret: "" }
bitbucket: { oauth_client_id: "", oauth_client_secret: "" }
codeberg:  { oauth_client_id: "", oauth_client_secret: "" }
google:    { oauth_client_id: "", oauth_client_secret: "" }

platform:
  domain: hosting.example.com         # required
  name: WiseHosting
  support_url: https://...

# Worker-only block (ignored on control plane)
worker:
  name: worker-de-01
  ip: 5.45.109.72
  wg_ip: 10.50.0.2                  # WireGuard IP of this worker; required for proxy routing
  api_key: "<48 char key>"
  zone: route.uday.me               # base zone for app subdomains (e.g. slug.route.uday.me)
  region_name: "Frankfurt, Germany"
  capacity_cpu: 4
  capacity_memory: 4294967296

# Proxy server config (control plane only; read by /v1/traefik/proxy-config)
proxy:
  token: "<32+ hex chars>"          # Bearer token the proxy Traefik sends; generate with: openssl rand -hex 32
  subdomain_base: "route.uday.me"   # wildcard base; apps get <slug>.<subdomain_base>

job:
  poll_interval: 5s                   # default 5s
  build_timeout: 10m                  # default 10m
  container_start_timeout: 30s        # default 30s

container:
  cpu_limit: 1
  memory_limit: 536870912
  pids_limit: 256
  disk_limit: 1073741824
  bandwidth_limit: 10mbit
  port_range_start: 30000             # required, > 0
  port_range_end:   31000             # required, > start

plans:
  free:     { max_apps: 1, cpu_limit: 1, memory_mib: 512,  disk_mib: 1024,  bandwidth_limit: 10mbit }
  pro:      { max_apps: 5, cpu_limit: 2, memory_mib: 2048, disk_mib: 10240, bandwidth_limit: 100mbit }
  business: { max_apps: 0, cpu_limit: 4, memory_mib: 8192, disk_mib: 51200, bandwidth_limit: "" }

security:
  api_rate_limit: 120
  api_rate_window: 1m

internal_api:
  bind: "127.0.0.1:9091"
  token: ""                           # provision via LoadCredential=internal_api_token

logging:
  level: info
  format: text

What is `internal_api`?

A loopback HTTP listener that the wisehosting-admin sibling process uses to read live in-memory state (worker connections, log buffers, stats cache) from the running control plane. Bound to 127.0.0.1 so the kernel itself prevents external reach. Authenticated by a single bearer token. Empty token disables the listener entirely. See Admin subsystem for the full picture.

Worker config fields

KeyRequiredDescription
worker.nameYesUnique name for this worker (e.g. worker-de-01). Shown in the dashboard.
worker.ipYesPublic IP of the worker host. Used during registration.
worker.wg_ipYesWireGuard IP of this worker (e.g. 10.50.0.2). The control plane includes this in the proxy config so Traefik can forward requests to the right host.
worker.api_keyYesLong-lived API key (48 hex chars). Stored hashed on the control plane.
worker.zoneYesBase DNS zone for app subdomains. Apps get <slug>.<zone> (e.g. myapp.route.uday.me).
worker.region_nameNoHuman-readable region shown in the dashboard.
worker.capacity_cpuYesNumber of vCPUs available for scheduling.
worker.capacity_memoryYesAvailable memory in bytes for scheduling.

Proxy config fields

The proxy: block is read by the control plane to authenticate and respond to the proxy Traefik's HTTP provider polls.

KeyRequiredDescription
proxy.tokenYes32+ hex character bearer token. The proxy Traefik sends this in its Authorization: Bearer <token> header when polling /v1/traefik/proxy-config. Generate with openssl rand -hex 32.
proxy.subdomain_baseYesBase domain for app subdomains (e.g. route.uday.me). Apps get <slug>.<subdomain_base>. A wildcard A record *.<subdomain_base> must point to the proxy server's public IP.

Rotate the proxy token like any other secret

Anyone with the proxy token can query the full routing config (all app slugs, custom domains, worker IPs). Treat it like api_server.secret. Use LoadCredential in production.

Validation rules

config.Validate() enforces:

  • database.password is non-empty
  • api_server.secret is set and at least 16 characters
  • container.port_range_start > 0 and port_range_end > port_range_start
  • platform.domain is non-empty

Plans

The built-in plan registry has three tiers; YAML plans: overrides them at startup.

PlanMax appsCPUMemoryDiskBandwidth
free11512 MiB1 GiB10mbit
pro522 GiB10 GiB100mbit
businessunlimited (0)48 GiB50 GiBunlimited ("")

max_apps: 0 means no cap. Setting bandwidth_limit: "" disables the egress cap for that plan.

Environment variables

VarPurpose
CONFIG_PATHPath to config file (default config.yaml)
WISEHOSTING_*Used by scripts/worker-setup.sh for non-interactive worker installs

Secrets via systemd LoadCredential

In production every long-lived secret is provisioned through systemd's LoadCredential= mechanism rather than written into config.yaml. The control-plane unit ships these lines:

LoadCredential=api_server_secret:/etc/credstore/wisehosting/api_server_secret
LoadCredential=database_password:/etc/credstore/wisehosting/database_password
LoadCredential=google_oauth_client_secret:/etc/credstore/wisehosting/google_oauth_client_secret
LoadCredential=github_oauth_client_secret:/etc/credstore/wisehosting/github_oauth_client_secret
LoadCredential=gitlab_oauth_client_secret:/etc/credstore/wisehosting/gitlab_oauth_client_secret
LoadCredential=bitbucket_oauth_client_secret:/etc/credstore/wisehosting/bitbucket_oauth_client_secret
LoadCredential=codeberg_oauth_client_secret:/etc/credstore/wisehosting/codeberg_oauth_client_secret

config.go's loadCredential(name) reads $CREDENTIALS_DIRECTORY/<name>. For each credential present, the loader overrides the YAML value — so api_server.secret, database.password, and the OAuth *_client_secret fields can be left blank in config.yaml and the on-disk file holds no secrets. Files in /etc/credstore/wisehosting/ should be 0400 and owned by root so only systemd can read them at unit-load time.

Secret encryption at rest

internal/database/secret.go derives 256-bit AES-GCM keys from api_server.secret using HKDF-SHA256 with per-purpose info strings, so keys for unrelated columns can never collide:

Purpose stringUsed by
wisehosting-aes-v1newSecretCipher — encrypts git tokens, env vars, job payloads, TOTP secrets
wisehosting-oauth-state-v1OAuth state-cookie HMAC
wisehosting-oauth-bind-v1OAuth state→user binding
wisehosting-completion-v1Sign-up completion tokens
wisehosting-worker-jwt-v1HS256 worker JWT signing/verification

Ciphertext is stored as v1:<base64url-no-pad>. Plaintext entries written before encryption was enabled keep working — Decrypt passes them through unchanged. This makes key rotation possible without downtime: re-encrypting a row produces a v1: blob alongside legacy plaintext, and the next read normalises it.

On this page