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"
# backend              = "docker"       # "docker" (default) or "process"
# skip_preflight       = false
# default_memory_limit = "2g"           # fallback per worker; empty = unlimited
# default_cpu_limit    = 4.0            # fallback per worker; 0 = unlimited
# 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
backendstringdockerNoWorker backend: docker or process. See Backend Security for the trade-offs. process requires a [process] section.
skip_preflightbooleanfalseNoSkip backend-specific preflight checks at startup. Use for development or when you are certain the environment is correctly configured.
default_memory_limitstringNoFallback memory limit for workers when no per-app limit is set (e.g. "2g"). Empty means unlimited. Enforced by the Docker backend via cgroups; the process backend emits a warning and does not enforce.
default_cpu_limitfloat0NoFallback CPU limit for workers when no per-app limit is set (e.g. 4.0). 0 means unlimited. Enforced by the Docker backend via cgroups; the process backend emits a warning and does not enforce.
management_bindstringNoSeparate listener for /healthz, /readyz, /metrics. See Management listener.
shutdown_timeoutduration30sNoGrace period for draining requests on shutdown
drain_timeoutdurationNoMaximum time the old server will wait for sessions to end during a rolling update drain. See the process backend rolling update walkthrough.
log_levelstringinfoNoLog verbosity. One of trace, debug, info, warn (or warning), error.
session_secretstringWhen [oidc] is set without [vault]Secret for signing session cookies. Supports vault references. Auto-generated and stored in vault when [vault] 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.
worker_envmap[string]stringNoExtra environment variables injected into every worker. Common use: point workers at an OTLP collector (OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL) for hosted-app tracing. Blockyard-managed keys (BLOCKYARD_API_URL, SHINY_HOST, VAULT_ADDR, …) cannot be overridden. See Tracing hosted Shiny apps.

server.skip_docker_preflight is deprecated and has been renamed to skip_preflight. The old name is still accepted for one release with a deprecation warning.

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]#

Required when [server] backend = "docker" (the default). Configures the Docker/Podman runtime used for worker and build containers.

[docker]
socket          = "/var/run/docker.sock"
image           = "ghcr.io/cynkra/blockyard-worker:4.4.3"
shiny_port      = 3838
pak_version     = "stable"
# service_network  = ""
# runtime          = ""          # OCI runtime; empty = Docker daemon default
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).
runtimestringNoDefault OCI runtime for worker containers (e.g. kata-runtime for stronger isolation). Empty means the Docker daemon’s default.
runtime_defaultsmapNoPer-access-type runtime defaults (e.g. { public = "kata-runtime" }). Overrides runtime for apps matching the access type.

In earlier releases default_memory_limit, default_cpu_limit, and store_retention lived in [docker]. They have moved to [server] and [storage] respectively because they are backend-neutral. The old names are still parsed for one release with a deprecation warning.

[process]#

Required when [server] backend = "process". Configures the bubblewrap-based worker sandbox. See the Process Backend (Native) and Process Backend (Containerized) guides for deployment walkthroughs, and Backend Security for the trade-offs compared to the Docker backend.

[process]
bwrap_path             = "/usr/bin/bwrap"
r_path                 = "/usr/bin/R"
# seccomp_profile        = "/etc/blockyard/seccomp.bpf"  # empty = no seccomp
port_range_start       = 10000
port_range_end         = 10999
worker_uid_range_start = 60000
worker_uid_range_end   = 60999
worker_gid             = 65534
# skip_metadata_check    = false
FieldTypeDefaultRequiredDescription
bwrap_pathpath/usr/bin/bwrapNoPath to the bubblewrap binary on the host.
r_pathpath/usr/bin/RNoPath to the R binary.
seccomp_profilepathNoPath to a compiled BPF seccomp profile applied to the worker R process via bwrap --seccomp. The blockyard and blockyard-process images ship a profile at /etc/blockyard/seccomp.bpf and set this via BLOCKYARD_PROCESS_SECCOMP_PROFILE. Empty disables in-sandbox seccomp filtering (the outer namespace and capability drops still apply).
port_range_startinteger10000NoFirst localhost port allocated to workers (inclusive).
port_range_endinteger10999NoLast localhost port allocated to workers (inclusive).
worker_uid_range_startinteger60000NoFirst host UID assigned to worker sandboxes (inclusive). Must be sized to at least the port range.
worker_uid_range_endinteger60999NoLast host UID assigned to worker sandboxes (inclusive).
worker_gidinteger65534NoShared host GID for all workers. Used as the match key for iptables owner-match egress rules.
skip_metadata_checkbooleanfalseNoSuppress the cloud_metadata preflight check, which fails startup with Error when 169.254.169.254:80 is reachable from blockyard itself (and therefore from every worker). Set to true only when blockyard legitimately needs cloud metadata access (e.g., using the VM’s IAM role for S3 storage); opting in accepts that a compromised worker can read instance credentials.

Per-worker resource limits (server.default_memory_limit, server.default_cpu_limit, per-app overrides) are not enforced by the process backend. Setting them produces a preflight warning. Use systemd MemoryMax= / CPUQuota= or the outer container’s cgroups for a shared ceiling.

[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
# store_retention       = "0"      # R library cache eviction; 0 = disabled

# [[storage.data_mounts]]
# name = "datasets"
# path = "/srv/shared/datasets"
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.
store_retentionduration0NoHow long to keep unused entries in the shared R package store. 0 (default) disables eviction — the store grows indefinitely. Moved from [docker] in a recent release; the old location is still parsed with a deprecation warning.
data_mountsarrayNoAdmin-approved host directories that apps can mount read-only or read-write. Each entry has name (referenced by apps) and path (host-side location).

[database]#

[database]
driver = "sqlite"
path   = "/data/db/blockyard.db"
# url  = ""   # PostgreSQL connection string (when driver = "postgres")

# Vault-managed Postgres credentials (optional; postgres only):
# vault_mount = "database"
# vault_role  = "blockyard_admin"
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). Userinfo is ignored when vault_role is set.
vault_mountstringdatabaseNoVault database secrets-engine mount path. Requires [vault] and driver = "postgres".
vault_rolestringNoVault static-role name. When set, Blockyard reads {vault_mount}/static-creds/{vault_role} at startup and uses those credentials instead of any user/password in url. Requires [vault] and driver = "postgres".

Vault-managed Postgres credentials#

When database.vault_role is set, Blockyard obtains its Postgres credentials from the vault database secrets engine on every startup and whenever the cached password stops working. The role’s password is owned by the vault (rotated on the schedule the operator configures on the role), not by the token that created the lease — so Blockyard restarts, deploy pipelines, and token renewals do not affect database access.

One-time operator setup:

  1. Create a PostgreSQL role for Blockyard with the privileges it needs (at minimum LOGIN plus CREATE and USAGE on the target database). A typical setup uses a dedicated blockyard_admin role:

    CREATE ROLE blockyard_admin LOGIN PASSWORD '<temp>' CREATEROLE;
    GRANT ALL PRIVILEGES ON DATABASE blockyard TO blockyard_admin;
  2. In the vault, register the role as a static-role on the database secrets engine. This tells the vault to adopt the role and manage its password:

    bao write database/static-roles/blockyard_admin \
        db_name=postgresql \
        username=blockyard_admin \
        rotation_period=24h

    The vault immediately rotates the password; subsequent reads of database/static-creds/blockyard_admin return the current one.

  3. Grant Blockyard’s AppRole policy read access to the static-creds endpoint:

    path "database/static-creds/blockyard_admin" {
      capabilities = ["read"]
    }
  4. Configure Blockyard:

    [database]
    driver     = "postgres"
    url        = "postgres://postgres.internal/blockyard?sslmode=verify-full"
    vault_role = "blockyard_admin"

    The url does not need (and should not include) a username or password — Blockyard injects the vault-issued credentials on every connection.

At runtime, Blockyard re-reads the static-creds endpoint on any Postgres authentication failure, so an out-of-band password rotation heals automatically on the next health poll.

[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.

[redis] (optional)#

Enables Redis-backed shared state for the session store, worker registry, and the process backend’s port/UID allocators. Required for rolling updates via by admin update — the old and new server processes use Redis as the cross-process coordination layer. Single-node deployments without rolling updates can omit this section and the in-memory implementation is used.

[redis]
url        = "redis://localhost:6379"
# key_prefix = "blockyard:"
FieldTypeDefaultRequiredDescription
urlstringYes (when section is present)Redis connection URL, e.g. redis://[:password@]host:port[/db].
key_prefixstringblockyard:NoKey prefix for every Redis operation. Useful when multiple blockyard deployments share a Redis instance.

[update] (optional)#

Configures the rolling-update orchestrator driven by by admin update. The orchestrator has two variants, picked automatically based on the configured backend:

  • Docker variant — clones the blockyard container next to the old one. Uses only schedule, channel, and watch_period.
  • Process variant — forks a new blockyard process on an alternate bind port. Uses schedule, channel, watch_period, plus alt_bind_range and drain_idle_wait. Requires [redis].

See the process backend rolling update walkthrough for the containerized vs. native rules.

[update]
# schedule        = "0 3 * * 0"     # cron; empty = no scheduled updates
# channel         = "stable"        # "stable" or "main"
# watch_period    = "15m"           # post-update health monitoring
# alt_bind_range  = "8090-8099"     # process variant: alternate bind pool
# drain_idle_wait = "5m"            # process variant: session drain timeout
FieldTypeDefaultRequiredDescription
schedulestringNoCron expression (5 fields) for automatic rolling updates. Empty disables the scheduler.
channelstringstableNoRelease channel to pull from: stable or main.
watch_perioddurationNoTime the orchestrator monitors the new server’s health after activation. An unhealthy signal triggers automatic rollback (Docker variant only).
alt_bind_rangestring8090-8099NoPort range the process orchestrator picks an alternate bind from when spawning the new server. Must not overlap [process] port_range_start..end. Ignored by the Docker variant.
drain_idle_waitduration5mNoMaximum time the old server waits for active sessions to end during a rolling drain. Ignored by the Docker variant, which relies on the reverse proxy to drain in-flight requests.

[oidc] (optional)#

Enable OIDC-based authentication. When this section is present, server.session_secret is required unless [vault] 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"
default_role         = "viewer"
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.
default_rolestringviewerNoRole assigned to new users on first OIDC login. Must be viewer or publisher. Set to publisher when the IdP itself is the access gate and every authenticated user should be trusted to deploy. admin is rejected — bootstrap admins via initial_admin.

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

Blockyard requests openid profile email offline_access from the IdP so that long-lived sessions can refresh access tokens without forcing the user to log in again. The IdP must advertise offline_access in scopes_supported and grant it to the client; otherwise the authorize request fails with invalid_scope. For Authentik this means adding the bundled offline_access scope to the provider’s property mappings. Dex grants refresh tokens by default and needs no extra configuration.

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.

[vault] (optional)#

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

[vault]
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"
# secret_id_file    = "/run/secrets/vault_secret_id"   # opt-in: re-read secret_id on each login for rotation
# secret_id_wrapped = true                             # opt-in: secret_id_file holds a response-wrap token to unwrap at login
FieldTypeDefaultRequiredDescription
addressstringYesVault server address (must start with http:// or https://)
role_idstringOne of role_id or admin_tokenAppRole role identifier. The secret_id is delivered via BLOCKYARD_VAULT_SECRET_ID or (opt-in) secret_id_file.
admin_tokenstringOne of role_id or admin_tokenDeprecated. Static admin token. Supports vault references. Use role_id with AppRole auth instead.
token_ttlduration1hNoTTL hint; the actual TTL is whatever vault returns on login. Shorten it to make rotation propagate faster.
jwt_auth_pathstringjwtNoAuth method mount path in the vault
secret_id_filestringNoPath to a file containing the AppRole secret_id (or, with secret_id_wrapped, a response-wrap token). When set, the file is re-read on every login so rotations on disk take effect without restarting Blockyard. Takes precedence over BLOCKYARD_VAULT_SECRET_ID.
secret_id_wrappedbooleanfalseNoTreat secret_id_file contents as a vault response-wrap token; Blockyard calls sys/wrapping/unwrap to fetch the real secret_id. Gives time-bounded on-disk exposure and tamper detection. Requires secret_id_file.
ca_certstringNoPath to a PEM-encoded CA bundle used to verify the vault server’s TLS certificate. When set, replaces the system CA bundle for vault HTTP calls (matches VAULT_CACERT semantics). Overridable via BLOCKYARD_VAULT_CA_CERT.
skip_policy_scope_checkbooleanfalseNoSkip the policy scope check during vault bootstrap. Useful when the vault policy format differs from what Blockyard expects.

With AppRole auth (role_id), Blockyard logs in against the vault and re-logs in shortly before each token expires; a 403 on any admin call also triggers an immediate re-login and retry. Point secret_id_file at a path written by Vault Agent (or any rotation tool) to rotate the secret_id without restarting the server. When no file is configured, the secret_id is read once from BLOCKYARD_VAULT_SECRET_ID at startup. session_secret is also auto-generated and stored in vault.

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

[[vault.services]]#

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

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

[[vault.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 [vault] (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 the vault 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.