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: textWhat 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
| Key | Required | Description |
|---|---|---|
worker.name | Yes | Unique name for this worker (e.g. worker-de-01). Shown in the dashboard. |
worker.ip | Yes | Public IP of the worker host. Used during registration. |
worker.wg_ip | Yes | WireGuard 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_key | Yes | Long-lived API key (48 hex chars). Stored hashed on the control plane. |
worker.zone | Yes | Base DNS zone for app subdomains. Apps get <slug>.<zone> (e.g. myapp.route.uday.me). |
worker.region_name | No | Human-readable region shown in the dashboard. |
worker.capacity_cpu | Yes | Number of vCPUs available for scheduling. |
worker.capacity_memory | Yes | Available 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.
| Key | Required | Description |
|---|---|---|
proxy.token | Yes | 32+ 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_base | Yes | Base 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.passwordis non-emptyapi_server.secretis set and at least 16 characterscontainer.port_range_start> 0 andport_range_end>port_range_startplatform.domainis non-empty
Plans
The built-in plan registry has three tiers; YAML plans: overrides them at startup.
| Plan | Max apps | CPU | Memory | Disk | Bandwidth |
|---|---|---|---|---|---|
free | 1 | 1 | 512 MiB | 1 GiB | 10mbit |
pro | 5 | 2 | 2 GiB | 10 GiB | 100mbit |
business | unlimited (0) | 4 | 8 GiB | 50 GiB | unlimited ("") |
max_apps: 0 means no cap. Setting bandwidth_limit: "" disables the egress cap for that plan.
Environment variables
| Var | Purpose |
|---|---|
CONFIG_PATH | Path 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_secretconfig.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 string | Used by |
|---|---|
wisehosting-aes-v1 | newSecretCipher — encrypts git tokens, env vars, job payloads, TOTP secrets |
wisehosting-oauth-state-v1 | OAuth state-cookie HMAC |
wisehosting-oauth-bind-v1 | OAuth state→user binding |
wisehosting-completion-v1 | Sign-up completion tokens |
wisehosting-worker-jwt-v1 | HS256 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.