Configuration Guide
Rise backend uses YAML configuration files with environment variable substitution support.
Configuration Files
Section titled “Configuration Files”Configuration files are located in config/ and loaded in this order:
default.yaml- Shipped defaults (optional). Ships with the Rise image at/etc/rise/default.yamland carries built-in defaults such as the quickstart catalog.{RISE_CONFIG_RUN_MODE}.yaml- Environment-specific config (required)development.yamlwhenRISE_CONFIG_RUN_MODE=developmentproduction.yamlwhenRISE_CONFIG_RUN_MODE=production
- The local configuration layer (optional):
RISE_LOCAL_CONFIG_YAMLwhen the environment variable is presentlocal.yamlotherwise (not checked into git)
Later layers override earlier ones. RISE_LOCAL_CONFIG_YAML is parsed as YAML
and replaces the local.yaml source; Rise does not load both.
In container deployments, RISE_CONFIG_DIR is typically /etc/rise.
Environment Variable Substitution
Section titled “Environment Variable Substitution”Configuration values can reference environment variables using the syntax:
auth: client_secret: "${OIDC_CLIENT_SECRET:-rise-backend-secret}"registry: account_id: "${AWS_ACCOUNT_ID}"server: public_url: "https://${DOMAIN_NAME}:${PORT}"Syntax
Section titled “Syntax”${VAR_NAME}- Use environment variableVAR_NAME, or an empty string if it is not set${VAR_NAME:-default}- UseVAR_NAMEif set, otherwise usedefault
How It Works
Section titled “How It Works”- Configuration files are parsed as YAML
- String values are scanned for
${...}patterns - Patterns are replaced with environment variable values
- Resulting configuration is deserialized into Settings struct
This happens after YAML parsing but before deserialization, so:
- ✅ Works in all string values (including nested tables/maps and arrays)
- ✅ Preserves structure and types
- ✅ Clear error messages if required variables are missing
Configuration Precedence
Section titled “Configuration Precedence”Values are resolved in this order (later steps override earlier ones):
- The config files described in Configuration Files, in their loading order
- Environment variable substitution -
${VAR}patterns are replaced - DATABASE_URL special case - Overrides
[database] urlif set
Merge semantics
Section titled “Merge semantics”The config loader uses the config crate, which deep-merges maps but replaces arrays. Practically: if default.yaml defines quickstart.templates with four entries and local.yaml defines quickstart.templates with one entry, the resulting catalog contains only that one entry — the defaults are not preserved. To extend a shipped list, copy the entries from default.yaml into your override and add to the list.
Example:
# In production.yamlauth: client_secret: "${AUTH_SECRET}" # Required in production
# In local.yamlauth: client_secret: "my-local-secret" # Override: hardcoded valueSpecial Cases
Section titled “Special Cases”DATABASE_URL: The DATABASE_URL environment variable is used as a fallback when database.url is not set in the config file. If database.url is explicitly set (including via ${VAR} substitution), it takes precedence over DATABASE_URL.
# Option 1: Explicit value in config (takes precedence over DATABASE_URL env var)database: url: "postgres://rise:password@rds-endpoint:5432/rise"
# Option 2: Explicit substitution (recommended when you want a specific env var)database: url: "${DATABASE_URL}"
# Option 3: Leave unset — DATABASE_URL env var will be used as fallback# (omit the database.url key entirely)Note: DATABASE_URL is also required at compile time for SQLX query verification. See the Developer Guide for details.
Reserved project names
Section titled “Reserved project names”Project names become application host labels. To prevent an application route
from shadowing a control-plane service, Rise rejects project creation when the
name appears in the top-level reserved_project_names list. This applies to
both Docker and Kubernetes deployment controllers.
reserved_project_names: - rise - dex - registry - www - grafana # topology-specific control-plane hostnameWhen omitted, the list defaults to rise, dex, registry, and www. Config
arrays are replaced rather than merged, so an override that adds names must
repeat the defaults it still needs to reserve.
Examples
Section titled “Examples”Development (development.yaml)
Section titled “Development (development.yaml)”server: host: "0.0.0.0" port: 3000 public_url: "http://localhost:3000"
auth: issuer: "http://localhost:5556/dex" client_id: "rise-backend" client_secret: "${OIDC_CLIENT_SECRET:-rise-backend-secret}"Production with Environment Variables
Section titled “Production with Environment Variables”server: host: "0.0.0.0" port: "${PORT:-3000}" public_url: "${PUBLIC_URL}" # Required, no default cookie_secure: true
auth: issuer: "${OIDC_ISSUER}" client_id: "${OIDC_CLIENT_ID}" client_secret: "${OIDC_CLIENT_SECRET}" admin_users: - "${ADMIN_EMAIL}"
database: url: "${DATABASE_URL}"
registry: type: "ecr" region: "${AWS_REGION:-us-east-1}" account_id: "${AWS_ACCOUNT_ID}" push_role_arn: "${ECR_PUSH_ROLE_ARN}"Environment file:
PUBLIC_URL=https://rise.example.comOIDC_ISSUER=https://idp.example.comOIDC_CLIENT_ID=rise-productionOIDC_CLIENT_SECRET=very-secret-valueADMIN_EMAIL=admin@example.comAWS_ACCOUNT_ID=123456789012ECR_PUSH_ROLE_ARN=arn:aws:iam::123456789012:role/rise-backend-ecr-pushDATABASE_URL=postgres://rise:${DB_PASSWORD}@db.example.com/riseLocal Overrides (local.yaml)
Section titled “Local Overrides (local.yaml)”For local development, create local.yaml (not checked into git):
# Override just what you needauth: client_secret: "my-local-secret"
registry: type: "oci-client-auth" registry_url: "localhost:5000"Configuration Reference
Section titled “Configuration Reference”Server Settings
Section titled “Server Settings”server: host: "0.0.0.0" # Bind address port: 3000 # HTTP port public_url: "http://..." # Public URL (for OAuth redirects) cookie_secure: false # Set true for HTTPS jwt_signing_secret: "..." # JWT signing secret (base64-encoded, min 32 bytes) jwt_expiry_seconds: 86400 # JWT expiry duration in seconds (default: 24 hours) jwt_claims: ["sub", "email", "name"] # Claims to include from IdP rs256_private_key_pem: "..." # Optional: RS256 private key (persists JWTs across restarts) rs256_public_key_pem: "..." # Optional: RS256 public key (derived if not provided) docs_dir: "/var/rise/docs" # Optional: directory containing built static documentation trusted_proxies: # Optional: proxies whose forwarded-address headers are believed - "10.0.0.0/8"Documentation Serving (docs_dir):
- When set, the backend serves built static documentation from the specified directory at
/docs/* - In the container image, bundled user docs are copied to
/var/rise/docs - In development, build
docs/userand set this to the generateddocs/user/distdirectory if you want the backend to serve local docs - If not set, documentation endpoints return 404
Client address and trusted proxies (trusted_proxies):
- IP-keyed rate limits (device-login start, token exchange, OAuth extension endpoints, workload-token rejections) use the client address Rise resolves for each request. IPv6 clients are counted per /64, since a host with a /64 can rotate addresses within it; the device-login confirmation page still shows the full address.
X-Forwarded-ForandX-Real-IPare plain request headers that any caller can send. Rise honours them only when the TCP peer that delivered the request is insidetrusted_proxies; a request from any other peer is attributed to that peer and its forwarded headers are ignored.trusted_proxiesis a list of CIDRs (IPv4 or IPv6); a bare address means a single host. The default is loopback, RFC 1918 and IPv6 unique-local space:127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7. Arrays are replaced, not merged, so an explicit list drops the defaults;[]trusts no proxy.- For a request from a trusted peer that carries
X-Forwarded-For, the header is read right to left, skipping trusted addresses; the first untrusted address is the client. If every entry is trusted, the leftmost is used. Each comma-separated entry is judged on its own, so garbage to the left of an entry a trusted proxy appended cannot affect it. Blank entries are skipped; entries may carry a port or[v6]brackets. An entry that is not UTF-8 or not an IP address ends the walk where it stands and the client is the peer, as does a chain of more than 32 entries. IPv4-mapped IPv6 addresses are treated as IPv4. X-Real-IPis consulted only when there is noX-Forwarded-Forheader at all (and then only if it is a valid IP address); otherwise the peer is the client. It is never a fallback for an unusableX-Forwarded-For, because a proxy may pass a client-suppliedX-Real-IPthrough.- Typical chains resolve correctly with the default: ingress-nginx (
peer 10.x,X-Forwarded-For: client) and ALB -> Traefik -> Rise (X-Forwarded-For: client, 10.0.1.5), the latter provided Traefik is configured to trust the ALB and keep its forwarded headers (rise-deploy/rise#598). - If your reverse proxy reaches Rise from a public address, add that address to
trusted_proxies; otherwise every client is attributed to the proxy and shares one rate-limit bucket. - Co-tenant workloads are inside the default trust set. Anything that can open a connection to Rise’s port from a private address can set
X-Forwarded-Forand pick its rate-limit bucket:- Docker: deployed apps share the
rise_defaultnetwork withrise-backend, so any app can callrise-backend:3000from a172.xaddress. Narrowtrusted_proxiesto Traefik’s address (for example a single/32), or run Rise on a network deployed apps cannot join. - Kubernetes: by default the chart’s NetworkPolicy lets every pod in the cluster reach port 3000. Restrict
networkPolicy.httpIngressFromto the ingress controller (seehelm/rise/values.yaml), and narrowtrusted_proxiesif the ingress controller’s pod range is smaller than the private ranges.
- Docker: deployed apps share the
- Publishing Rise’s port directly to untrusted networks is not covered by this setting. A published port can present a private source address to Rise: Docker’s userland proxy (loopback traffic, rootless Docker, Docker Desktop, IPv6 without
ip6tables) shows the bridge gateway as the peer, whereas rootful iptables DNAT keeps the client’s source address. Put a proxy in front and narrowtrusted_proxiesto it. - The Metacontroller webhook and Traefik configuration listeners are separate and do not use this setting.
JWT Configuration:
jwt_signing_secret: Base64-encoded secret for HS256 JWT signing (generate withopenssl rand -base64 32)jwt_expiry_seconds: Duration in seconds before JWTs expire (default: 86400 = 24 hours)jwt_claims: Claims to include from IdP token in Rise JWTsrs256_private_key_pem: Optional pre-configured RS256 private key (prevents JWT invalidation on restart)rs256_public_key_pem: Optional RS256 public key (automatically derived from private key if omitted)
Auth Settings
Section titled “Auth Settings”auth: issuer: "http://..." # OIDC issuer URL client_id: "rise-backend" # OAuth2 client ID client_secret: "..." # OAuth2 client secret scopes: ["openid", "email", "profile"] # OAuth2 scopes requested at login (default shown). # offline_access is omitted by default (the CLI # does not use refresh tokens, and some providers # such as Google reject it); add it here if needed. idp_group_claim: "groups" # ID-token claim containing group names (default shown) # Use "cognito:groups" for AWS Cognito. idp_group_sync_prefixes: [] # Optional prefixes limiting groups mirrored as teams. idp_group_missing_claim_policy: refuse_login # Or treat_as_empty. admin_users: ["email@..."] # Default-organization admin emails (array) admin_idp_groups: ["..."] # IdP groups whose members are admins (array, optional) operator_users: ["ops@..."] # Operator role allowlist (array, optional) operator_idp_groups: ["..."] # IdP groups whose members are Operators (array, optional) allow_team_creation: true # Allow regular users to create teams (default: true) # When false, only admins can create teams allow_missing_email_verified: false # Accept ID tokens without `email_verified` (default: false)Verified email addresses. Users, admin_users, operator_users, and the
platform-access email allowlist all match on the ID token’s email, so a login
is refused unless the IdP marks that address email_verified: true (a boolean,
or the string "true"). An IdP that lets users type in an address it never
checks would otherwise let them sign in as someone else. Some IdPs omit the
claim entirely; set allow_missing_email_verified: true only if yours does and
every address it issues belongs to the user signing in (for example, addresses
its administrators assign). email_verified: false is refused either way.
Roles:
admin_users: Admins of the default organization. Admins do not implicitly receive the Operator role.operator_users: Operators have full access to generic resource storage and built-in resource management. Operators do not implicitly receive platform (typed CLI/UI) access — list the email inadmin_usersorplatform_access.allowed_user_emailsseparately if both are needed.
Both roles can also be granted by IdP group, so the IdP stays the source of truth and adding an admin does not require a config change and a restart:
auth: admin_idp_groups: - "platform-admins" operator_idp_groups: - "platform-operators"A user holds the role if their email is on the allowlist or they are in one
of the listed groups. Group names match case-insensitively. Users granted a role
by group bypass platform_access exactly as email-listed users do.
How group membership is resolved. Rise reads the ID-token claim configured
by auth.idp_group_claim (default: groups) at login and mirrors it into
IdP-managed teams (sync_user_groups, and the Entra active sync when enabled).
For AWS Cognito, set idp_group_claim: "cognito:groups". Those teams are what
the group checks read. Login sync accepts only canonical team names containing
lowercase ASCII letters, digits, and hyphens. Set idp_group_sync_prefixes
(for example, ["rise-"]) to mirror only matching groups; an empty list accepts
every canonical name. The prefix filter does not apply to Entra active sync.
Three consequences:
- Only IdP-managed teams count. A team a user creates themselves never grants a role, even if its name matches a configured group — otherwise self-service team creation would be a path to admin.
- Membership refreshes at login. Removing a user from a group in the IdP takes
effect on their next login (or on the next Entra active sync), not
immediately. Revoke access that must take effect at once in the IdP itself.
A login whose groups cannot be synced is refused (
503) rather than admitted with the memberships of an earlier login. Missing/null group claims followidp_group_missing_claim_policy: the defaultrefuse_loginrejects login, whiletreat_as_emptyremoves mirrored groups. Use the latter only when the IdP omits the claim to assert no memberships. - Invalid or prefix-excluded claim values never create teams. Existing memberships outside the filtered claim are removed at the next login.
The same resolution backs platform_access.allowed_idp_groups.
Controllers:
Controller identities are not configured here — they are Controller and ControllerTrustPolicy resources managed through the generic resource API. See Authentication & Tokens — Controllers for how to register one.
Team Creation Control:
allow_team_creation = true(default): All authenticated users can create teamsallow_team_creation = false: Only admin users can create teams (suitable for centrally-managed organizations)
Database Settings
Section titled “Database Settings”database: url: "postgres://..." # PostgreSQL connection string # Or use DATABASE_URL env varRegistry Settings
Section titled “Registry Settings”AWS ECR
Section titled “AWS ECR”[registry]type = "ecr"region = "us-east-1"account_id = "123456789012"repo_prefix = "rise/"push_role_arn = "arn:aws:iam::..."auto_remove = true# Optional: the registry host images are pushed to and deployed from, as# host[:port]. Defaults to <account_id>.dkr.ecr.<region>.amazonaws.com; set it# for another ECR endpoint (FIPS, dual-stack) or an ECR-compatible emulator.# The ECR API endpoint itself follows the AWS SDK (`AWS_ENDPOINT_URL`).# registry_host = "123456789012.dkr.ecr.us-east-1.localhost:4566"On ECS (config/ecs.yaml) this is RISE_ECR_REGISTRY_HOST, which the
rise-ecs module sets from ecr_registry_host.
OCI Registry (Docker, Harbor, Quay)
Section titled “OCI Registry (Docker, Harbor, Quay)”[registry]type = "oci-client-auth"registry_url = "registry.example.com"namespace = "rise-apps"# Optional: automatically authenticate trusted Rise users during deploy.username = "${RISE_REGISTRY_USERNAME}"password = "${RISE_REGISTRY_PASSWORD}"When username and password are omitted or empty, users must authenticate the
container CLI themselves with docker login. When set, Rise returns these static
credentials from the authenticated, deployment-scoped credentials endpoint and
uses them for controller-side pulls. Anyone allowed to deploy can receive the
credentials, so use this only where all Rise users are trusted.
GitLab Container Registry
Section titled “GitLab Container Registry”registry: type: gitlab gitlab_url: "https://gitlab.com" # GitLab instance URL registry_url: "registry.gitlab.com" # Registry host namespace: "my-org/my-group/rise-apps" # Image path prefix in the registry username: "${GITLAB_USERNAME}" token: "${GITLAB_TOKEN}" # Personal Access Token or Deploy Token mint_pull_secrets: true # Create K8s image pull secrets per project namespace # client_registry_url: ~ # Optional: override URL returned to CLI clientsHow it works:
- CLI pushes: the backend mints a short-lived (~15 min) scoped JWT from GitLab’s JWT auth endpoint (
GET /jwt/auth?service=container_registry&scope=repository:<path>:push,pull). The JWT is injected directly into the container CLI’s auth config file rather than viadocker login. - Kubernetes pull secrets: when
mint_pull_secrets: true, the controller creates a standardkubernetes.io/dockerconfigjsonsecret with the PAT in each project namespace. The container runtime uses the PAT to obtain its own JWT from GitLab on each pull. mint_pull_secrets: false: disable pull secret management when the cluster has its own image pull mechanism (e.g., pre-configured service account or node credentials).
Token requirements:
The GitLab token must have read_registry and write_registry scopes (or equivalent deploy token permissions).
JFrog Artifactory
Section titled “JFrog Artifactory”JFrog supports two token-issuing backends: Vault (via Rise’s vault-plugin-secrets-artifactory fork) and Direct (via JFrog’s access token API).
Vault mode (scope override — default):
registry: type: jfrog registry_host: "jfrog.example.com" docker_repo_key: "rise-docker-local" token_provider: type: vault # vault_addr: ~ # defaults to VAULT_ADDR env # vault_token: ~ # defaults to VAULT_TOKEN env # vault_token_file: ~ # alternative: read token from file (supports rotation) vault_mount_path: "artifactory" vault_role: "rise" # scope_override: true # default — Rise sends per-operation scopes # push_permissions: "r,w" # default # pull_permissions: "r" # default # push_token_ttl: 600 # push token lifetime in seconds (default: 600) # pull_token_ttl: 86400 # pull token lifetime in seconds (default: 86400 = 24h) # mint_pull_secrets: true # defaultConfigure Vault with the Rise fork so scope overrides are opt-in and restricted to the Docker repository:
vault write artifactory/config/admin \ url="https://jfrog.example.com" \ access_token="$JFROG_ADMIN_TOKEN" \ allow_scope_override="opt-in" \ use_expiring_tokens=true
vault write artifactory/roles/rise \ scope="artifact:rise-docker-local/**:r" \ default_ttl=600 \ max_ttl=86400 \ allow_scope_override=true \ allowed_scopes='["artifact:rise-docker-local/**:r","artifact:rise-docker-local/**:r,w"]'Replace rise-docker-local with your configured docker_repo_key. The role allowlist lets Rise request narrow per-project and per-tag scopes under that repository, while denying unrelated scopes such as applied-permissions/admin.
Vault mode (role scope):
registry: type: jfrog registry_host: "jfrog.example.com" docker_repo_key: "rise-docker-local" token_provider: type: vault vault_mount_path: "artifactory" vault_role: "rise" scope_override: false # use the scope configured on the Vault role # push_token_ttl: 600 # pull_token_ttl: 86400When scope_override: false, Rise omits the scope query parameter and the Vault role’s configured scope is used for all tokens. The push_permissions and pull_permissions settings are ignored in this mode. This is useful when the Vault admin wants full control over token scopes.
Note (Vault mode): The Vault role’s
max_ttlmust be >=pull_token_ttl. If the role’smax_ttlis lower, Vault silently clamps the token TTL and the cached credentials may expire earlier than expected.
Direct mode:
registry: type: jfrog registry_host: "jfrog.example.com" docker_repo_key: "rise-docker-local" token_provider: type: direct jfrog_url: "https://jfrog.example.com" admin_token: "${JFROG_ADMIN_TOKEN}" # applied-permissions/admin scoped token # client_registry_url: ~ # optional: override registry URL returned to CLI clients # push_token_ttl: 600 # push token lifetime in seconds (default: 600) # pull_token_ttl: 86400 # pull token lifetime in seconds (default: 86400 = 24h)How it works:
- CLI pushes: the backend mints a short-lived multi-scope token with
r,wpermissions scoped to the deployment tag, blob uploads, and content-addressed manifests. The token is used fordocker loginbefore push. - Kubernetes pull secrets: when
mint_pull_secrets: true, the controller creates image pull secrets with a long-lived read-only scoped token (artifact:{docker_repo_key}/{project}/**:r). Pull tokens are cached in memory and refreshed after 2/3 of their TTL to avoid minting a new token on every deploy.
See JFrog registry operations for scope details and troubleshooting.
Controller Settings (Optional)
Section titled “Controller Settings (Optional)”[controller]reconcile_interval_secs = 5health_check_interval_secs = 5termination_interval_secs = 5cancellation_interval_secs = 5expiration_interval_secs = 60secret_refresh_interval_secs = 3600Deployment Controller (Docker)
Section titled “Deployment Controller (Docker)”As an alternative to the Kubernetes controller, the Docker controller deploys apps as
containers on a single Docker host, routed by Traefik. Select it with
deployment_controller.type: "docker". Supported fields:
type: "docker"— required discriminator.traefik_network— Docker network Traefik watches (e.g.rise_default).traefik_entrypoint— Traefik entrypoint name (defaultweb).traefik_certresolver?— optional Traefik certresolver for TLS (omit for plain HTTP).production_ingress_url_template— host template for production deployments.staging_ingress_url_template?/environment_ingress_url_template?— host templates for staging / per-environment deployments.ingress_schema—httporhttps(defaulthttps). The scheme browsers use to reach apps; also used for sign-in redirects and/.risecallback URLs on app hosts.ingress_port?— external port apps are served on (e.g.80locally).controller_class_name— controller ownership class (defaultdefault).reconcile_interval_secs— in-process reconcile loop interval in seconds (default5).traefik_api_url?— base URL of Traefik’s API, read to drain old replicas via the top-levelserverStatusmap during a rolling cutover.deployment_constraints.max_replicas— upper bound on requested replicas (default10). The Docker controller additionally clamps every request to a hard backstop of50regardless of this value, so raising it above50silently clamps (with only a server-side log).access_classes— ingress access classes (keyed by identifier) defining authentication levels, mirroring the Kubernetes variant.auth_backend_url— internal URL Traefik uses to reach the Rise backend for the forwardAuth subrequest; required when any access class isAuthenticated/Member.auth_signin_url— browser-facing base URL for the login redirect (falls back to the serverpublic_urlwhen empty).publish_app_ports— dev-only. Publish each app container’s HTTP port to a random127.0.0.1host port so a host-run backend can health-probe it directly.app_backend_host_aliases/app_backend_ip— dev-only host-alias knobs that injectextra_hostsso app containers can reach the Rise backend at the issuer host.
See the Docker operator guide for the full controller configuration, including TLS/ACME and the rolling-cutover gate.
Kubernetes-only fields (namespace prefix, ingress annotations, network policies, host aliases, node selectors, etc.) do not apply to the Docker variant.
Example:
deployment_controller: type: "docker" traefik_network: "rise_default" traefik_entrypoint: "web" production_ingress_url_template: "{project_name}.rise.localhost" staging_ingress_url_template: "{deployment_group}--{project_name}.rise.localhost" environment_ingress_url_template: "{environment}--{project_name}.rise.localhost" ingress_schema: "http" ingress_port: 80 reconcile_interval_secs: 5 controller_class_name: "default"The configuration schema is regenerated via mise run config:schema:generate, and CI
verifies it is up to date on every PR.
Ingress hostname templates
Section titled “Ingress hostname templates”Every deployment controller takes three host templates. Rise refuses to start unless they keep each project’s hosts its own:
production_ingress_url_templatecontains{project_name}and no--, e.g.{project_name}.apps.example.com.staging_ingress_url_templatejoins{project_name}and{deployment_group}with exactly--and contains no other--.{project_name}must start or end its DNS label: a.or the template’s start/end on its other side. E.g.{deployment_group}--{project_name}.apps.example.comor{project_name}--{deployment_group}.preview.example.com.environment_ingress_url_templatefollows the same rule with{environment}, with{project_name}on the same side of the--as in the staging template.
Without a staging template, {project_name} in the production template
becomes {project_name}--{deployment_group}, and that derived template must
pass the same rule.
Project and environment names cannot contain --, so the project can always
be read back from a host. With a single hyphen ({project_name}-{deployment_group})
project a with group b-c and project a-b with group c would share
a-b-c.
Validation
Section titled “Validation”The backend validates configuration on startup:
- Required fields must be set
- Invalid values cause startup failure with clear error messages
- Environment variable substitution errors are reported
- Unknown configuration fields generate warnings (as of v0.9.0)
Checking Configuration
Section titled “Checking Configuration”Use the rise backend check-config command to validate backend configuration:
rise backend check-configThis command:
- Loads and validates backend configuration files
- Reports any unknown/unused configuration fields as warnings
- Exits with an error if configuration is invalid
- Useful for CI/CD pipelines and deployment validation
Example output:
Checking backend configuration...⚠️ WARN: Unknown configuration field in backend config: server.typo_field⚠️ WARN: Unknown configuration field in backend config: unknown_section✓ Configuration is validJSON Schema
Section titled “JSON Schema”Rise provides a JSON Schema for backend configuration, hosted alongside these operator docs:
Generate it with:
cargo run --features cli,backend -- backend config-schema > docs/engineering/public/schemas/backend-settings.schema.jsonCI verifies this file is up to date on every PR and push.
Unknown Field Warnings
Section titled “Unknown Field Warnings”Starting in v0.9.0, Rise warns about unrecognized configuration fields to help catch typos and outdated options:
Backend Configuration (YAML/TOML):
# Warnings appear in logs when starting server or using check-configWARN rise::server::settings: Unknown configuration field in backend config: server.unknown_fieldProject Configuration (rise.toml):
# Warnings appear when loading rise.toml (during build, deploy, etc.)WARN rise::build::config: Unknown configuration field in ./rise.toml: build.?.typo_fieldThese are warnings, not errors - your configuration will still load and work. The warnings help you:
- Catch typos in field names
- Identify outdated configuration options after upgrades
- Ensure your configuration is being used as intended
Run with RUST_LOG=debug to see configuration loading details:
RUST_LOG=debug cargo run --bin rise -- backend serverLog colour
Section titled “Log colour”Logs are coloured by default, including with nothing attached to a terminal:
whether that renders depends on what reads the stream. kubectl logs hands the
bytes to a terminal that renders them, so a Kubernetes install keeps colour;
the CloudWatch console prints them literally, so modules/rise-ecs sets
RISE_LOG_COLOR = "never" on the control plane. Being non-terminal is what
those two have in common, which is why Rise cannot decide it for you.
RISE_LOG_COLOR | Effect |
|---|---|
| unset (default) | Always colour |
always (1, true, yes) | Always colour |
never (0, false, no) | Never colour |
auto | Colour only a terminal — for a pipe or a file |
NO_COLOR set to anything non-empty also disables colour
(no-color.org); an explicit RISE_LOG_COLOR of
always/never wins over it.
Debugging Authentication and Token Claims
Section titled “Debugging Authentication and Token Claims”When a deploy or login is rejected with a permission or “claims do not match”
error, raise the backend log level to see the issuer, claims, and validation
decisions Rise makes for an incoming token. Keep general output at info and
turn up only the auth and workload-token paths so the relevant lines aren’t
buried:
RUST_LOG=info,rise::server::auth=debug,rise::server::workload_tokens=debug \ rise backend serverThis surfaces:
rise::server::auth::middleware— the issuer the backend peeked at and the JWKS-validation outcome for an incoming tokenrise::server::auth::context— per-service-account claim-mismatch reasons (SA <id> claim mismatch: ...); these log atinfo, so they show even without thedebugtargets aboverise::server::workload_tokens::handlers—Issued workload identity token(project / environment / audience / ttl) when an app mints a token through the token-exchange endpoint
RUST_LOG=rise=debug works as a catch-all but is far noisier.
To inspect claims from the client side — the token the rise CLI presents
(RISE_TOKEN, a RISE_TOKEN_COMMAND, or GitHub Actions OIDC) — see
Inspecting Token Claims in the user troubleshooting guide, or use the
rise::auth::idtoken target described below.
Decoded token claims
Section titled “Decoded token claims”To see the decoded header and payload of the tokens Rise handles during login,
enable the dedicated rise::auth::idtoken target at trace. It covers:
rise backend server: the IdP ID token received when a login completes, whether through the browser sign-in callback (UI login and device-flow approval) or therise loginPKCE code exchange. Machine-credential exchanges (service-account token exchange, workload and resource-API token exchange) are not traced.- CLI commands (
rise login,rise deploy, …): the token the CLI presents or receives — a stored login,RISE_TOKEN,RISE_TOKEN_COMMAND, GitHub Actions OIDC, or the login response.
RUST_LOG=info,rise::auth::idtoken=trace rise backend serverRUST_LOG=info,rise::auth::idtoken=trace rise loginThis is strictly opt-in: RUST_LOG=debug (or rise=debug) does not turn it on.
The payload contains personal data (email, name, groups, sub, IdP-specific
claims), so enable it only while debugging a login and keep the resulting logs
out of shared or long-retention sinks. The signature is never logged.
Quickstart Templates
Section titled “Quickstart Templates”Rise’s web UI surfaces a curated catalog of stateless container images that
end-users can deploy in one click (“quickstart templates”). The catalog lives
under the quickstart key in backend config:
quickstart: templates: - id: welcome # kebab-case, unique across the catalog display_name: "Welcome page" tagline: "Friendly landing page with pod/host info." description: > Longer description shown in the deploy dialog. icon: welcome # built-in name OR absolute URL/path image: paulbouwer/hello-kubernetes:1.10.1 http_port: 8080 learn_more_url: https://github.com/paulbouwer/hello-kubernetes tags: [demo, hello-world] warning: null # optional caveat shown in deploy modalThe icon field accepts either:
- A built-in icon name (e.g.
welcome,whoami,httpbin,excalidraw) — resolved against/assets/quickstart/<name>.svgshipped with Rise. - An absolute URL (
http://...,https://...) — used verbatim. - An absolute static path (
/some/path.svg) — used verbatim. Useful for operators serving custom icons from a mounted static dir.
Defaults and overrides
Section titled “Defaults and overrides”Rise ships a default.yaml config layer with four curated templates. The
config loader (default.yaml → <run_mode>.yaml → local layer) merges maps
but replaces arrays, so any quickstart.templates list in a later layer
replaces the defaults entirely. To extend the shipped catalog, copy
/etc/rise/default.yaml’s quickstart.templates into your local layer and add
to the list.
Validation
Section titled “Validation”The backend validates the catalog at startup and refuses to boot on:
- a template
idthat isn’t lowercase kebab-case - duplicate
ids within the catalog - empty
display_name,tagline,description,icon,image, orlearn_more_url http_portof0
Invariants for catalog entries
Section titled “Invariants for catalog entries”Operators adding custom templates should respect the following invariants — they aren’t enforced by code, but the deploy modal and “Redeploy from template” feature assume them:
- Tag-pinned: use a real tag (e.g.
:1.2.3) rather than:latest. Floating tags work, but the drift detector compares stored vs. catalogimage:tagstrings — when both sides are:latest, the “Redeploy from template” button can’t observe drift. Floating-tag entries should set awarningmentioning this. - Stateless: no volume mounts, no external databases, no user-supplied secrets required to start serving traffic.
- Restricted-PSS friendly: prefer images that run as a non-root user and
listen on a port
>= 1024. Entries that violate this still work on permissive clusters but must set awarningso the deploy modal surfaces the caveat.
Custom Domains
Section titled “Custom Domains”Rise supports custom domains for projects, allowing you to serve your applications from your own domain names instead of (or in addition to) the default project URL.
Primary Custom Domains
Section titled “Primary Custom Domains”Each project can designate one custom domain as primary. The primary domain is used as the canonical URL for the application and is exposed via the RISE_APP_URL environment variable.
RISE_APP_URL Environment Variable
Section titled “RISE_APP_URL Environment Variable”Rise automatically creates a RISE_APP_URL deployment environment variable containing the canonical URL for the application. This variable is determined at deployment creation time and persisted in the database:
- If a primary custom domain is set:
RISE_APP_URLcontains the primary custom domain URL (e.g.,https://example.com) - If no primary domain is set:
RISE_APP_URLcontains the default project URL (e.g.,https://my-app.rise.dev)
Since this is a deployment environment variable, you can view it via the API or CLI along with your other environment variables.
This environment variable is useful for:
- Generating absolute URLs in your application (e.g., for email links, OAuth redirects)
- Implementing canonical URL redirects (redirect all traffic to the primary domain)
- Setting the correct domain for cookies and CORS headers
Example usage in your application:
const canonicalUrl = process.env.RISE_APP_URL;
// Redirect to canonical domainapp.use((req, res, next) => { const requestUrl = `${req.protocol}://${req.get('host')}`; if (requestUrl !== canonicalUrl) { return res.redirect(301, `${canonicalUrl}${req.url}`); } next();});# Pythonimport os
canonical_url = os.environ.get('RISE_APP_URL')
# Flask: Set SERVER_NAMEapp.config['SERVER_NAME'] = canonical_url.replace('https://', '').replace('http://', '')Managing Custom Domains
Section titled “Managing Custom Domains”Via Frontend:
- Navigate to your project’s Domains tab
- Add custom domains using the “Add Domain” button
- Click the star icon next to a domain to set it as primary
- The primary domain will show a filled yellow star and a “Primary” badge
Via API:
# List custom domainscurl https://rise.dev/api/projects/my-app/domains
# Add a custom domaincurl -X POST https://rise.dev/api/projects/my-app/domains \ -H "Authorization: Bearer $TOKEN" \ -d '{"domain": "example.com"}'
# Set domain as primarycurl -X PUT https://rise.dev/api/projects/my-app/domains/example.com/primary \ -H "Authorization: Bearer $TOKEN"
# Unset primary statuscurl -X DELETE https://rise.dev/api/projects/my-app/domains/example.com/primary \ -H "Authorization: Bearer $TOKEN"DNS Configuration
Section titled “DNS Configuration”Before adding a custom domain, you must configure your DNS to point to your Rise deployment:
# A record for root domainexample.com. IN A <rise-ingress-ip>
# CNAME for subdomainwww.example.com. IN CNAME <rise-ingress-hostname>Custom domains are added to the ingress for the default deployment group only.
TLS/SSL
Section titled “TLS/SSL”Custom domains use the same TLS configuration as the default project URL:
- If your Rise deployment uses a wildcard certificate, custom domains will use HTTP unless configured with per-domain TLS
- Configure
custom_domain_tls_modein the Kubernetes controller settings for automatic HTTPS on custom domains
Behavior
Section titled “Behavior”- Automatic reconciliation: Setting or unsetting a primary domain triggers reconciliation of the active deployment to update the
RISE_APP_URLenvironment variable - Deletion protection: You can delete a primary domain;
RISE_APP_URLwill fall back to the default project URL - Multiple domains: You can add multiple custom domains to a project, but only one can be primary
- Environment variable list: All custom domains (primary and non-primary) are also available in the
RISE_APP_URLSenvironment variable as a JSON array