Blockyard reads its configuration from a TOML file. The default path is blockyard.toml in the working directory. Override it with the --config CLI argument:

blockyard --config /etc/blockyard/config.toml

Environment variable overrides#

Every configuration field can be set via an environment variable. The naming convention is:

BLOCKYARD_<SECTION>_<FIELD>

All uppercased. For example, [server] bind becomes BLOCKYARD_SERVER_BIND.

Environment variables take precedence over values in the TOML file.

[server]#

[server]
bind             = "127.0.0.1:8080"
shutdown_timeout = "30s"
# management_bind = "127.0.0.1:9100"
# log_level      = "info"
# session_secret = "random-secret"   # required when [oidc] is configured
# external_url   = "https://blockyard.example.com"
# trusted_proxies = ["10.0.0.0/8"]
FieldTypeDefaultRequiredDescription
bindstring127.0.0.1:8080NoSocket address to listen on
management_bindstringNoSeparate listener for /healthz, /readyz, /metrics. See Management listener.
shutdown_timeoutduration30sNoGrace period for draining requests on shutdown
log_levelstringinfoNoLog verbosity. One of trace, debug, info, warn (or warning), error.
session_secretstringWhen [oidc] is set without [openbao]Secret for signing session cookies. Supports vault references. Auto-generated and stored in vault when [openbao] is configured.
external_urlstringNoPublic-facing URL of the server (used for OIDC redirect URIs)
trusted_proxiesstring[]NoCIDRs whose X-Forwarded-For headers to trust (e.g. ["10.0.0.0/8"]). Each entry must be a valid CIDR. Set via env as comma-separated: BLOCKYARD_SERVER_TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12.
bootstrap_tokenstringNoOne-time token that can be exchanged for a real PAT via POST /api/v1/bootstrap. Requires oidc.initial_admin to be set. Intended for dev/CI bootstrapping — do not use in production. See Bootstrap tokens.

API authentication uses Personal Access Tokens (for CLI/CI access) or OIDC session cookies (for browser access). The v0 static bearer token (server.token) has been removed.

[docker]#

[docker]
socket          = "/var/run/docker.sock"
image           = "ghcr.io/rocker-org/r-ver:4.4.3"
shiny_port      = 3838
pak_version     = "stable"
# service_network      = ""
# store_retention      = "0"
# default_memory_limit = "2g"   # fallback per worker; omit = unlimited
# default_cpu_limit    = 4.0    # fallback per worker; 0 = unlimited
FieldTypeDefaultRequiredDescription
socketstring/var/run/docker.sockNoPath to Docker or Podman socket
imagestringYesBase image for worker and build containers
shiny_portinteger3838NoPort Shiny listens on inside containers
pak_versionstringstableNopak release channel (stable, rc, or devel)
service_networkstringNoDocker network whose containers are made reachable from workers. Used when apps need access to sidecar services (e.g. PocketBase, PostgREST).
store_retentionduration0NoHow long to keep unused entries in the shared package store. 0 (default) disables eviction — the store grows indefinitely.
default_memory_limitstringNoFallback memory limit for workers when no per-app limit is set (e.g. "2g"). Empty or omitted means unlimited.
default_cpu_limitfloat0NoFallback CPU limit for workers when no per-app limit is set (e.g. 4.0). 0 means unlimited.

[storage]#

[storage]
bundle_server_path    = "/data/bundles"
bundle_worker_path    = "/app"
bundle_retention      = 50
max_bundle_size       = 104857600
# soft_delete_retention = "720h"   # 30 days; omit or 0 = immediate hard delete
FieldTypeDefaultRequiredDescription
bundle_server_pathpath/data/bundlesNoDirectory for storing uploaded bundles. Must be writable.
bundle_worker_pathpath/appNoMount point inside worker containers
bundle_retentioninteger50NoMax bundles kept per app (oldest pruned first)
max_bundle_sizeinteger104857600NoMaximum bundle upload size in bytes (default 100 MB)
soft_delete_retentionduration0NoHow long to keep soft-deleted apps before permanent removal. When 0 (default), DELETE is an immediate hard delete. When set (e.g. "720h" for 30 days), deleted apps are recoverable during the retention window and purged automatically afterwards.

[database]#

[database]
driver = "sqlite"
path   = "/data/db/blockyard.db"
# url  = ""   # PostgreSQL connection string (when driver = "postgres")
FieldTypeDefaultRequiredDescription
driverstringsqliteNoDatabase driver: sqlite or postgres
pathpath/data/db/blockyard.dbWhen driver = "sqlite"Path to the SQLite database file (created if missing). The parent directory must be writable.
urlstringWhen driver = "postgres"PostgreSQL connection string (e.g. postgres://user:pass@host/dbname)

[proxy]#

[proxy]
ws_cache_ttl         = "60s"
health_interval      = "15s"
worker_start_timeout = "60s"
max_workers          = 100
log_retention        = "1h"
# session_idle_ttl     = "0"   # idle timeout for sessions and WebSocket connections; 0 = disabled
idle_worker_timeout  = "5m"
# http_forward_timeout  = "5m"
# max_cpu_limit         = 16.0
# transfer_timeout      = "60s"
# session_max_lifetime  = "0"    # hard cap on session duration; 0 = unlimited
FieldTypeDefaultRequiredDescription
ws_cache_ttlduration60sNoTime to keep a backend WebSocket alive after client disconnects
health_intervalduration15sNoHow often workers are health-checked
worker_start_timeoutduration60sNoMax time to wait for a new worker to become healthy
max_workersinteger100NoGlobal cap on concurrent worker containers
log_retentionduration1hNoHow long to keep worker log entries before cleanup
session_idle_ttlduration0NoIdle timeout for sessions and WebSocket connections. When non-zero, WebSocket connections with no application-level messages for this duration are closed, and stale session records are swept. 0 (default) means disabled.
idle_worker_timeoutduration5mNoTime before an idle worker container is stopped
http_forward_timeoutduration5mNoTimeout for forwarding HTTP requests to worker containers
max_cpu_limitfloat16.0NoMaximum CPU limit that can be set per app (caps the cpu_limit field on PATCH /api/v1/apps/{id})
transfer_timeoutduration60sNoTimeout for transferring bundle files to worker containers
session_max_lifetimeduration0NoHard cap on session duration regardless of activity. 0 (default) means unlimited — sessions only end via idle timeout or worker shutdown.

[oidc] (optional)#

Enable OIDC-based authentication. When this section is present, server.session_secret is required unless [openbao] is also configured (in which case it can be auto-generated).

[oidc]
issuer_url           = "https://idp.example.com/realms/myapp"
# issuer_discovery_url = ""      # optional: internal URL for OIDC discovery
client_id            = "blockyard"
client_secret        = "oidc-client-secret"
cookie_max_age       = "24h"
initial_admin        = "google-oauth2|abc123"
FieldTypeDefaultRequiredDescription
issuer_urlstringYesOIDC provider issuer URL (must match the iss claim in tokens)
issuer_discovery_urlstringNoInternal URL for OIDC discovery and server-side requests. Use when the IdP is reachable at a different address from the server than from browsers (e.g. Docker DNS). See Split-URL OIDC.
client_idstringYesOIDC client ID
client_secretstringYesOIDC client secret. Supports vault references.
cookie_max_ageduration24hNoMaximum lifetime of session cookies
initial_adminstringNoOIDC sub of the first admin user. Checked only on first login. See First Admin Setup.

When OIDC is configured, the proxy routes (/app/{name}/) enforce authentication. Users must log in before accessing apps (except for apps with public visibility).

Split-URL OIDC#

In Docker or Kubernetes deployments, the OIDC provider (e.g. Dex, Keycloak) is often reachable at a different address from inside the cluster than from the user’s browser. For example:

  • Browser reaches the IdP at http://localhost:5556
  • Server container reaches the IdP at http://dex:5556 (Docker DNS)

Set issuer_discovery_url to the internal address. Blockyard will:

  1. Perform OIDC discovery against the internal URL
  2. Route all server-side requests (token exchange, JWKS fetch, refresh) to the internal URL
  3. Keep the public issuer_url for browser-facing redirects and token validation
[oidc]
issuer_url           = "http://localhost:5556"       # public: what the browser sees
issuer_discovery_url = "http://dex:5556"             # internal: Docker DNS
client_id            = "blockyard"
client_secret        = "oidc-client-secret"

The corresponding environment variables are BLOCKYARD_OIDC_ISSUER_URL and BLOCKYARD_OIDC_ISSUER_DISCOVERY_URL.

[openbao] (optional)#

Enable OpenBao (Vault-compatible) credential management. Requires [oidc] to also be configured.

[openbao]
address       = "http://openbao:8200"
role_id       = "blockyard-server"    # AppRole role identifier (recommended)
# admin_token = "vault-admin-token"   # deprecated: use role_id instead
token_ttl     = "1h"
jwt_auth_path = "jwt"
FieldTypeDefaultRequiredDescription
addressstringYesOpenBao server address (must start with http:// or https://)
role_idstringOne of role_id or admin_tokenAppRole role identifier. The secret_id is delivered via the BLOCKYARD_OPENBAO_SECRET_ID env var at bootstrap.
admin_tokenstringOne of role_id or admin_tokenDeprecated. Static admin token. Supports vault references. Use role_id with AppRole auth instead.
token_ttlduration1hNoTTL for issued credential tokens
jwt_auth_pathstringjwtNoAuth method mount path in OpenBao
skip_policy_scope_checkbooleanfalseNoSkip the policy scope check during OpenBao bootstrap. Useful when the OpenBao policy format differs from what Blockyard expects.

With AppRole auth (role_id), the server authenticates to OpenBao using a one-time secret_id (via BLOCKYARD_OPENBAO_SECRET_ID) and then renews its own token indefinitely. After initial bootstrap, the env var is no longer needed — the token is persisted to disk and reused across restarts. session_secret is also auto-generated and stored in vault.

admin_token and role_id are mutually exclusive — setting both is a configuration error.

[[openbao.services]]#

Define third-party services whose API keys users can enroll via OpenBao. Each entry must have id and label. Service IDs must be unique.

Credentials are stored at secret/data/users/{sub}/apikeys/{id}.

[[openbao.services]]
id    = "openai"
label = "OpenAI"
FieldTypeDefaultRequiredDescription
idstringYesUnique identifier for the service (also used as the vault path segment)
labelstringYesHuman-readable label shown to users

[board_storage] (optional)#

Enable board storage via PostgREST. Requires database.driver = "postgres" and [openbao] (for vault Identity OIDC tokens that PostgREST uses to enforce row-level security).

[board_storage]
postgrest_url = "http://postgrest:3000"
FieldTypeDefaultRequiredDescription
postgrest_urlstringYesURL of the PostgREST instance serving the board tables

When configured, workers receive a POSTGREST_URL environment variable pointing to this URL, allowing Shiny apps to store and retrieve board state.

[audit] (optional)#

Enable append-only audit logging to a JSONL file.

[audit]
path = "/data/audit/blockyard.jsonl"
FieldTypeDefaultRequiredDescription
pathpathYesPath to the JSONL audit log file

[telemetry] (optional)#

Enable observability features: Prometheus metrics and OpenTelemetry tracing.

[telemetry]
metrics_enabled = true
otlp_endpoint   = "http://otel-collector:4317"
FieldTypeDefaultRequiredDescription
metrics_enabledbooleanfalseNoExpose a /metrics endpoint for Prometheus scraping
otlp_endpointstringNoOpenTelemetry collector gRPC endpoint for distributed tracing

Vault references#

Any secret field in the configuration can reference a value stored in OpenBao instead of containing the literal secret. Use the vault: prefix:

[oidc]
client_secret = "vault:secret/data/blockyard/oidc#client_secret"

Format: vault:{kv_v2_data_path}#{key}

  • {kv_v2_data_path} — the full KV v2 data path (e.g. secret/data/blockyard/oidc)
  • {key} — the JSON key within the secret’s data map

At startup, blockyard resolves all vault references before initializing other subsystems. If a reference cannot be resolved (vault unreachable, path missing, key missing), the server exits with a clear error naming the field and path.

Values without the vault: prefix are treated as literals, unchanged from current behavior.