Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration

Ruscker has two layers of configuration, and it helps to keep them apart:

  • Portal content — your apps (specs), the landing page, users, credentials, and media. This lives in the database and is managed from the admin panel; you don’t write any YAML for it. On a fresh --db the portal even seeds a set of showcase apps for you.
  • Runtime / deploymenthow and where Ruscker runs: the address it binds, whether it sits at the site root or under a subpath, the Docker backend, the database, secrets, and HA. These are CLI flags and environment variables, set once at startup.

The application.yml file is the bootstrap + migration format: it must exist (it can be two lines), it carries the runtime proxy: settings that aren’t flags, and it’s how you import an existing ShinyProxy config. If you prefer to drive everything from YAML (GitOps), you still can — but for a normal install the admin panel is where you manage apps, and the YAML stays small.

Secrets are never written in the YAML — use ${VAR} interpolation and set the variables in the environment (or /etc/ruscker/ruscker.env).

Where each setting lives

You want to…Set it in
Add / edit apps, APIs, linksAdmin panel → Apps (or proxy.specs to import)
Customise the landing (title, colours, logos, SEO, blocks)Admin panel → Portal
Manage users, roles, group membershipAdmin panel → Users
Store registry credentialsAdmin panel → Credentials
Bind address / port--bind (or proxy.bind-address / proxy.port)
Serve at the root or a subpath--base-path (or proxy.server.context-path)
Enable / disable the Docker backendauto · --docker · --no-docker
Database (catalog, users, sessions)--db <file> · --config-db-url (Postgres/HA)
Admin token + crypto keysRUSCKER_ADMIN_TOKEN, RUSCKER_MASTER_KEY, RUSCKER_COOKIE_KEY
Log format--log-format text|json

Deployment settings

The decisions you make at startup. Most have both a CLI flag and an env var; see Deploying in production for the full systemd

  • nginx walkthrough.

Served at the root, or under a subpath?

The most common deployment question. By default Ruscker serves the portal at the site root (https://apps.example.org/). If you can’t dedicate a subdomain and need it under a path (https://example.org/apps/), set a base path:

ruscker serve --config application.yml --bind 127.0.0.1:8080 --db ruscker.db \
  --base-path /apps

(Equivalently proxy.server.context-path: /apps in the YAML, or the RUSCKER_BASE_PATH env var.) Ruscker then emits every URL — landing, admin, assets, and the /app proxy — under /apps, and rewrites app responses so unmodified Shiny / Streamlit / Jupyter apps work behind the prefix. Point your reverse proxy’s /apps/ location at Ruscker. Full nginx example: Mounting under a base path.

Bind address, Docker, database

  • --bind <addr:port> — where Ruscker listens (overrides proxy.bind-address / proxy.port). Behind nginx, bind to localhost.
  • Docker backend — auto-connects when the daemon socket is reachable. --no-docker runs landing-only (the /app proxy returns 503); --docker makes a failed connect fatal (e.g. a remote daemon you require).
  • --db <file> — the SQLite catalog (apps, users, landing, audit, sessions); required for /admin/*. For active-active HA use --config-db-url postgres://… (a shared catalog) instead.

Secrets

Set these in the environment (the .deb puts them in /etc/ruscker/ruscker.env):

  • RUSCKER_ADMIN_TOKEN — unlocks /admin and is the break-glass login. Without it, admin routes return 503.
  • RUSCKER_MASTER_KEY — AES-256 key for the encrypted credentials store.
  • RUSCKER_COOKIE_KEY — HMAC key for sticky-session cookies. Set it explicitly in production so sessions survive restarts (and are valid cross-instance in HA); without it a random key is generated per process.

High availability

Running more than one instance behind a load balancer? Share the catalog (--config-db-url), the proxy session store (--session-store-url), the admin session store (--admin-session-store-url), and the same RUSCKER_COOKIE_KEY across instances, so any node can serve any request. See Shared admin sessions.

Per-user access

access-groups / access-users on a spec scope who can see the card on the landing and reach the upstream at /app / /api. A spec with neither key is open — visible to everyone, including anonymous visitors. Otherwise:

  • An admin session sees everything.
  • A signed-in user sees a restricted spec when their username is in access-users or one of their groups is in access-groups.
  • An anonymous visitor only sees open specs.

Enforcement is real — the /app and /api guards reject unauthorized requests (anonymous on /app → redirected to login; otherwise 403), not just hide the landing card.

You set both keys on the spec form (admin panel → Apps), and group membership per user on the admin Users page. The same user record drives both portal visibility and admin role (Admin / Editor / Viewer) — see The admin panel. In YAML the keys look like:

proxy:
  specs:
    - id: open-app
      display-name: Open App
      container-image: demo/img        # no access keys ⇒ open
    - id: analysts-app
      display-name: Analysts App
      container-image: demo/img
      access-groups: [analysts]
    - id: vip-app
      display-name: VIP App
      container-image: demo/img
      access-users: [carol]

The full YAML reference

Everything below is the complete application.yml schema (the same docs/YAML_SCHEMA.md shipped in the repo). Reach for it to migrate an existing ShinyProxy config, or to drive specs and landing from YAML instead of the admin panel — not for a normal, admin-panel-managed install.

YAML schema reference

Reference for every field Ruscker understands in application.yml.

For ShinyProxy users: this document marks which ShinyProxy features are supported, extended by Ruscker, deferred to later phases, or not supported.

Top level

server: { ... }      # Optional. Spring Boot-style server config
proxy: { ... }       # The main Ruscker config
logging: { ... }     # Optional. Logging config

server block

Supported:

server:
  useForwardHeaders: true             # Honor X-Forwarded-* headers
  forward-headers-strategy: native    # 'native' | 'framework' | 'none'
  secure-cookies: true                # Set Secure flag on cookies
  servlet.session.timeout: 3600       # Spring flat-key form
  # OR equivalently:
  servlet:
    session:
      timeout: 3600
  context-path: /apps                  # Mount portal under a subpath (see below)
  # ShinyProxy's nested form is also accepted:
  servlet.context-path: /apps

Resolved via Server::session_timeout_secs() — either form works.

server.context-path — subpath mounting

Serves the whole portal under a URL prefix when you can’t dedicate a subdomain (e.g. example.org/apps/ instead of box.example.org/). Normalized form: leading slash, no trailing slash ("box", "/apps/", and "/apps" all become /apps). The CLI flag --base-path /apps overrides the YAML.

ShinyProxy emits this as server.servlet.context-path; the flat server.context-path form is also accepted. Empty / absent ⇒ served at the root (the default).

Operational notes:

  • The health probes /healthz and /readyz stay at the root regardless of the prefix — load balancers don’t need to know it.
  • The chrome’s root-absolute URLs (/admin/..., /assets/..., redirects) are rewritten on the response so they all carry the prefix; runtime fetches (fetch/XMLHttpRequest/WebSocket/ EventSource) get a small shim that prefixes them too.
  • Cookies set Path=/ (already sent by browsers for {base}/...).
  • Configure your reverse proxy to pass requests through under the same prefix — see the Mounting under a base path section of book/src/deploying.md.

Other server.* fields from Spring Boot are accepted by serde but ignored by Ruscker.

proxy block

Top-level proxy fields

FieldTypeDefaultNotes
titlestring"Ruscker"Browser tab title
landing-pagestring"/"Where the portal is served
hide-navbarboolfalseSuppress default navbar
template-pathpathnoneOverride template directory
heartbeat-ratems10000Client heartbeat interval
heartbeat-timeoutms3600000Session expiry; -1 = never
container-wait-timems60000Max wait for container Ready
shutdown-grace-msms30000Drain window on SIGTERM/Ctrl-C before forced exit; /readyz reports draining during it. Ruscker extension
max-body-sizesizenoneGlobal cap on proxied request bodies ("10m", "1g", bytes); over → 413. Per-spec max-body-size overrides. Ruscker extension
metrics-enabledboolfalseExpose a Prometheus /metrics endpoint (unauthenticated when on — firewall it). Ruscker extension
metrics-intervals5How often the dashboard polls the backend for per-replica CPU/mem. A busy host can slow it (1015) to ease the Docker daemon. 0 ⇒ default. Ruscker extension
hostslist[]Docker hosts for multi-host scheduling (Phase 6). Empty ⇒ the local daemon. Ruscker extension

proxy.hosts — multi-host scheduling (Phase 6)

Empty (the default) means the single local Docker daemon — today’s behaviour. List hosts to spawn app containers across several daemons:

proxy:
  hosts:
    - id: ssh-1
      address: ssh://ops@10.0.0.11      # Docker over SSH (simplest)
    - id: tcp-1
      address: tcp://10.0.0.12:2376     # Docker over TLS
      tls: { ca: /etc/ruscker/ca.pem, cert: /etc/ruscker/cert.pem, key: /etc/ruscker/key.pem }
      max-containers: 40                # optional cap
      weight: 2                         # optional, for spread placement
    - id: local
      address: unix:///var/run/docker.sock

Address schemes: ssh://user@host[:port], tcp://host:port (needs tls), http://host:port (plain TCP — trusted networks only), unix:///path. validate flags empty/duplicate ids, unknown schemes, and tls mismatched with the scheme.

Per-host: max-containers caps how many containers a host runs; weight (default 1) biases spread placement toward bigger hosts.

Per-spec placement controls how a spec’s replicas land across hosts:

- id: heavy-shiny
  placement: spread          # spread (default) | bin-pack
  anti-affinity: true        # keep replicas on distinct hosts (best-effort)
  • spread distributes replicas (weighted least-loaded) for fault isolation; bin-pack fills one host before using the next.
  • anti-affinity: true prefers hosts not already running the spec, falling back to the strategy above if every eligible host does (so scaling never stalls). Hosts at max-containers are skipped; if all are full, the spawn fails (the scaler retries). | container-log-path | path | none | Directory for per-container logs | | port | u16 | 8080 | HTTP listener port | | bind-address | string | "0.0.0.0" | Listener interface | | authentication | enum | none | none (MVP) / openid / ldap / saml / simple | | landing-customization | block | {} | Branding, SEO/social meta, analytics, custom HTML blocks, sign-in visibility — see § proxy.landing-customization. Ruscker extension | | specs | array | [] | List of apps/links/APIs |

Authentication

MVP only supports none. The other variants are accepted by the parser (so existing YAML doesn’t fail) but Ruscker will warn at boot that auth is unimplemented and continue as if none. Phase 8 adds real auth support.

For applications that handle their own auth internally (a common case), none is the correct choice — Ruscker just routes traffic.

proxy.landing-customization

Branding, SEO, analytics, and custom-HTML overrides for the public landing. Every subfield is optional; an empty landing-customization block (the default) renders the stock landing.

proxy:
  landing-customization:
    # Branding — CSS colors applied to the landing header
    header-bg: "#0f6e56"               # any CSS color
    header-fg: "#ffffff"               # contrast override when bg is dark

    # Intro paragraph between header and filters
    intro: "Welcome to the portal."    # single-language fallback
    intro-locales:                     # per-language overrides; locale code → text
      pt: "Bem-vindo ao portal."
      en: "Welcome to the portal."
      es: "Bienvenido al portal."
      fr: "Bienvenue sur le portail."

    # SEO / social-share meta tags injected into the landing `<head>`
    seo-title: "Portal — Org Name"     # overrides `proxy.title` for <title>
    seo-description: "Internal apps."  # <meta name="description"> + og:description
    og-image: /assets/img/og.png       # path or absolute URL for og:image

    # Analytics — admin-trusted, injected verbatim into landing <head>
    analytics-html: |
      <script defer src="https://plausible.io/js/script.js"
              data-domain="example.org"></script>
    analytics-origins: "https://plausible.io"   # space-separated; widens landing CSP

    # Operator CSS — injected as a <style> late in the landing <head>,
    # so it can override the built-in styles
    custom-css: ".rk-card { border-radius: 0; }"

    # Header / footer logos (admin-managed in the live editor; see below)
    logos:
      - url: /assets/img/org-mark.png   # uploaded, built-in, or absolute URL
        slot: header                    # `header` | `footer`
        align: left                     # `left` | `center` | `right`
        link: https://org.example       # optional click-through
        height: 40                      # render height in px
        margin: 12                      # optional outer margin in px

    # Sign-in visibility (anonymous viewers only)
    show-admin-link: true              # default true; false hides the entrance

    # Custom HTML blocks (admin-managed in the live editor; see note below)
    blocks:
      - slot: top                       # `top` (after header) | `bottom` (after grid)
        title: "Maintenance banner"     # internal label, not shown publicly
        html: '<div class="...">Scheduled downtime Sunday 02:00 UTC.</div>'
        csp-origins: ""                 # space-separated origins this block needs
        enabled: true                   # default true

Field reference:

FieldTypeDefaultNotes
header-bgCSS colornoneHeader background. Match your brand’s primary color.
header-fgCSS colornoneHeader text — set when header-bg is dark and the default loses contrast.
introstringnonePlain text (no HTML); single-language fallback.
intro-localesmap{}Locale code → intro string. Wins over intro for matching locales.
seo-titlestringproxy.titleOverride for <title>.
seo-descriptionstringresolved intro<meta name="description"> + og:description.
og-imagepath / URLnoneog:image for social-share.
analytics-htmlstringnoneTrusted raw HTML, injected verbatim into landing <head>.
analytics-originsstringnoneSpace-separated origins added to the landing CSP (script-src/connect-src/img-src).
custom-cssstringnoneTrusted raw CSS, injected as a <style> late in the landing <head> so it overrides the built-in styles.
show-admin-linkbooltrueWhen false, anonymous visitors don’t see the “Sign in” entrance. Logged-in users still see their panel link.
show-highlightsbooltrueShow the “Featured” carousel above the filters. The carousel still only renders when at least one spec is featured.
logos[]list[]Header/footer logos (see below).
blocks[]list[]Custom HTML blocks (see below).

logos[] subfields:

FieldTypeDefaultNotes
urlstringrequiredImage URL — /assets/img/... (uploaded), a built-in (/assets/showcase/..., /assets/brand/...), or an absolute URL.
slotenumrequiredheader or footer — where the logo renders.
alignenumrequiredleft, center, or right within the slot. left/right integrate into the chrome: header-left replaces the Ruscker mark, header-right trails the buttons, footer-left sits far-left, footer-right trails the version+mark. center renders in a separate bar. Logos sharing a slot+alignment render side by side.
linkURLnoneOptional click-through — when set, the logo becomes an <a>.
heightpxdefaultPer-logo render height in pixels; falls back to a built-in default when unset.
marginpxnoneOptional outer margin in pixels around the logo, for spacing from adjacent chrome or a neighbouring logo.

blocks[] subfields:

FieldTypeDefaultNotes
slotenumrequiredtop or bottom — render position on the landing.
titlestringrequiredAdmin-only label; not rendered publicly.
htmlstringrequiredTrusted raw HTML, injected verbatim into the chosen slot.
csp-originsstring""Space-separated origins this block’s content needs, folded into the landing CSP.
enabledbooltrueToggle without deleting.

Trust model: analytics-html, custom-css, and blocks[].html are rendered unescaped. Only set them from a trusted source. Anything the snippet loads from outside Ruscker’s origin must also be listed in the matching *-origins field, otherwise the landing’s CSP blocks it.

custom-css, logos[], and blocks[] are admin-managed in the live landing editor (the logos picker and blocks editor live on the Portal page). Blocks are stored in their own DB table; the blocks[] slot in this YAML schema exists so a future ruscker export round-trip can serialize them, but at the moment the import/export path does not populate it — operators edit blocks from the admin UI. logos[] does round-trip through import/export. SEO, analytics, and show-admin-link are deploy-level policy and live only in this block.

Specs

A spec describes one app, API, or external link. Every spec has an id and lives in proxy.specs[].

Common fields (all spec types)

- id: my_app                          # required, kebab-case
  display-name: "My App"              # shown on the card
  description: "What it does"         # HTML inline allowed
  template-properties:                # free-form bag for the landing
    logo: "/assets/img/myapp.png"
    icon: lock                        # 'lock' | 'lock_open'
    type: app                         # 'app' | 'package' | 'talk' | 'report' | 'api'
    updated: "18/05/2025"
    state: active                     # 'active' | 'inactive'
    link: https://external.example    # optional explicit URL

Containerized specs (Shiny, Streamlit, Dash, Voilà, API)

- id: my_app
  container-image: org/repo:tag       # required for containerized
  type: shiny                         # optional, default 'shiny' if image set
  container-port: 8501                # port the app listens on inside the
                                      #   container; default 3838 (Shiny).
                                      #   Set for Streamlit (8501), Dash
                                      #   (8050), … . ShinyProxy `port:`
                                      #   is accepted as an alias.
  seats-per-container: 10             # sessions per replica
  max-lifetime: 360                   # minutes — hard recycle (enforced, #334)
  container-lifetime: 360             # minutes — soft recycle when idle (enforced, #334)
  heartbeat-timeout: 3600000          # ms — per-spec override (enforced)
  stop-on-logout: false               # end a user's sessions on logout (enforced, #337)
  docker-registry-username: acme
  docker-registry-password: ${DOCKER_REGISTRY_PASSWORD}   # use env vars!
  docker-registry-domain: docker.io
  docker-registry-credential: dockerhub-acme   # OR reference a stored
                                      #   credential by name (Ruscker
                                      #   extension; see below)
  volumes:                            # bind mounts (ShinyProxy-compatible)
    - /srv/myapp/data:/data           #   persistent data
    - /srv/myapp/www:/www:ro          #   static assets, read-only
  container-env:                      # env vars injected into the container
    DB_HOST: db.internal              #   (ShinyProxy-compatible)
    DB_PASSWORD: ${DB_PASSWORD}       #   ${VAR} resolved at spawn, not at parse —
                                      #   the literal is stored; secret never hits the DB
  container-cmd:                      # override the image's CMD (argv list)
    - R
    - -e
    - shiny::runApp('/app', port=3838, host='0.0.0.0')
  access-groups: [staff, ops]         # who may see/reach this app
  access-users: [alice]               #   (ShinyProxy-compatible)

volumes is a list of Docker bind specs (/host:/container, optionally :ro/:rw), mapped to the container’s HostConfig.binds. Add as many as needed; editable in the admin Advanced form (one per line). Bind-mounting host paths is root-equivalent and admin-only — see SECURITY.md.

container-env is a NAME: value map injected into the container as environment variables (Docker Config.Env). ${VAR} / ${VAR:-default} references in the values are resolved at spawn, not at parse: the ${VAR} literal is what gets stored (config / DB / export), and the real value is only ever materialized when the container is created — so an app secret passed this way never lands in the database. container-cmd is an argv list that overrides the image’s baked CMD (Docker Config.Cmd); omit it to keep the image default. Both are ShinyProxy-compatible and editable per spec.

access-groups / access-users (ShinyProxy-compatible) scope who can see an app on the landing and reach it at /app / /api. A spec with neither is open — visible to everyone, including anonymous visitors. Otherwise: a logged-in user sees it when their username is in access-users or one of their groups is in access-groups; an admin always sees everything; an anonymous visitor sees only open apps. Group membership is set per user in the admin panel. Enforcement is real (not just hiding the card). See the Roadmap Phase 8.

Registry credentials — inline vs. named (docker-registry-credential)

A spec can authenticate to a private registry two ways:

  1. Inline docker-registry-username / docker-registry-password / docker-registry-domain — ShinyProxy-compatible. Always use ${ENV_VAR} for the password (never a literal in YAML).
  2. Named docker-registry-credential: <name> — a Ruscker extension that references an entry in the admin’s encrypted credential store (managed under Credentials in the admin panel). The username, password, and registry are resolved from the store at pull time (decrypted with RUSCKER_MASTER_KEY), so no secret — not even a ${VAR} reference — needs to live in the spec at all.

When docker-registry-credential is set, it takes precedence over the inline docker-registry-* fields for that spec. Leave it unset to use the inline fields. The spec form’s Registry section is a picker over stored credential names.

Smart routing — sub-path context for the upstream

Ruscker mounts each app under a sub-path (/app/{id}, /api/{id}) but the container usually assumes it lives at the server root. Two mechanisms bridge that gap:

  1. Forwarded-prefix headers (always on). Every proxied request carries the public mount context to the upstream so a prefix-aware app can self-route and emit correct absolute links:

    HeaderValueConsumed by
    X-Forwarded-Prefix/app/{id} (no trailing slash)Spring, Traefik, FastAPI root_path
    X-Script-Namesame mount pathWSGI, Dash, Plumber
    X-Forwarded-Protohttp / https as seen by the clientabsolute-URL builders behind TLS
    X-Forwarded-Hostthe public Hostabsolute-URL builders

    Apps that ignore these headers are unaffected. To honour them you typically point the framework at the prefix — e.g. uvicorn --root-path /app/my-api, or Dash requests_pathname_prefix.

  2. HTML rewriting (inject-base-href, default true). For apps that can’t be told their prefix, Ruscker injects <base href> and rewrites root-relative URLs in /app/{id} HTML responses (see the rewriter in routes::rewrite). This is the safe default and covers Shiny out of the box. Set it to false per spec once the app self-routes from the headers above — then the rewriting is redundant and best disabled:

    - id: my_api
      container-image: org/fastapi:tag
      type: api
      inject-base-href: false   # app reads X-Forwarded-Prefix itself
    

    inject-base-href only affects /app/{id} responses; /api/{id} responses are never rewritten. Editable in the admin Advanced form under Routing.

  3. The #{publicPath} token (opt-in). For an app that needs its own base-url told to it as a config value — Jupyter’s --ServerApp.base_url, for instance — drop the literal token #{publicPath} into any container-cmd argument or container-env value. At spawn it’s substituted with the spec’s public mount path with a trailing slash (e.g. /app/{id}/, or /apps/app/{id}/ under a base path; /api/{id}/ for APIs). This is Ruscker’s analog of ShinyProxy’s SHINYPROXY_PUBLIC_PATH, but it is never injected automatically — Ruscker strips the mount prefix before forwarding, so most apps should serve at root and rely on the rewriting above. Reach for the token only when an app genuinely needs the public path in its own configuration:

    - id: jupyter
      container-image: org/jupyter:tag
      type: app
      container-cmd:
        - jupyter
        - lab
        - --ServerApp.base_url=#{publicPath}
    

    Note #{publicPath} is resolved at spawn, distinct from the parse-time ${VAR} env interpolation, which never sees this runtime value.

- id: my_pkg
  display-name: "My Package"
  description: "An R package"
  template-properties:
    type: package
    link: https://pkg.example          # the destination URL
    logo: "/assets/img/mypkg.png"
    icon: lock_open
    state: active

Just omit container-image and provide template-properties.link. Ruscker won’t try to orchestrate anything — clicking the card navigates to the link.

API specs (Plumber2 / FastAPI / etc.)

- id: my_api
  type: api                            # explicit type, overrides auto-detect
  container-image: org/my-api:latest
  api:
    port: 8080                         # container port
    docs-path: /__docs__               # OpenAPI/Swagger UI
    health-path: /__healthz__          # readiness check
    rate-limit: 100/min                # per-IP rate limit at proxy
    cors: true                         # permissive CORS headers
  min-replicas: 1
  max-replicas: 3
  concurrent-requests-per-replica: 100
  routing-strategy: round-robin        # APIs don't need sticky

api.rate-limit — per-client throttling

Enforced at the proxy, before any container is spawned or woken, so a throttled caller costs nothing downstream. Format is N/unit where unit is one of s/sec/second(s), m/min/minute(s), or h/hr/hour(s) (case-insensitive):

rate-limit: 100/min      # at most 100 requests per client per minute
rate-limit: 5/s
rate-limit: 1000/hour

A request over the limit gets 429 Too Many Requests with a Retry-After header. The window is a sliding one, per (spec, client).

Client identity. The “client” is the caller’s IP. When the operator opts into forwarded headers (server.useForwardHeaders: true, or a forward-headers-strategy other than none), the left-most X-Forwarded-For address is used — the right choice when Ruscker sits behind a reverse proxy. Otherwise the real TCP peer is used: X-Forwarded-For is not trusted unless opted in, since a direct client could otherwise spoof it to dodge the limit.

A malformed rate-limit value is ignored (no limit applied) and flagged by ruscker validate.

api.cors — permissive CORS headers

cors: true makes the proxy add permissive CORS headers (Access-Control-Allow-Origin: *, common methods, * headers) to every response for that API spec, and answer OPTIONS preflight requests itself (204) without touching the container. Headers an upstream app already set are never overwritten — an API that does its own CORS wins. CORS applies only to the /api/ route family.

max-body-size — cap proxied request bodies

Limits how large a request body the proxy will forward, for both /app/ and /api/ routes. Set it globally on proxy.max-body-size and/or override it per spec:

proxy:
  max-body-size: 10m          # global default
  specs:
    - id: upload_api
      container-image: org/api:1
      type: api
      max-body-size: 100m     # this spec accepts larger uploads

Format is the Docker-style size string used elsewhere ("512" bytes, "10m", "1g"; binary units). The effective limit is the spec’s own value if set, otherwise the global default; unset everywhere means no limit (the default, preserving prior behaviour).

A request whose Content-Length exceeds the limit is rejected with 413 Payload Too Large before any container is touched. A chunked or under-declared body that grows past the cap mid-stream is also stopped (it surfaces as a 502). A malformed size string is ignored (no limit applied) and flagged by ruscker validate.

Load-balancing fields (any containerized spec)

FieldTypeDefaultNotes
min-replicasu321Always running
max-replicasu325 (≥ min-replicas)Per-container apps auto-scale to up to 5 independent replicas by default; set explicitly to raise/lower
scale-up-thresholdfloat0.8scale up when pool utilization exceeds this (enforced, #333)
scale-down-thresholdfloat0.3only retire idle replicas while utilization is below this (enforced, #333)
scale-down-graces300idle-grace before retiring a replica (enforced, #333)
drain-timeouts60grace for in-flight sessions on a max-lifetime recycle (enforced, #335)
routing-strategyenumvariesSee below
concurrent-requests-per-replicau32100API-only — per-replica in-flight cap the scaler scales on (enforced, #336)

Autoscaling knobs (#326). By default the scaler scales on seat saturation (sessions_active vs sessions_max) with built-in grace ticks. All the per-spec scaling/lifecycle knobs are now enforced (opt-in where noted): scale-up-threshold / scale-down-threshold / scale-down-grace (#333 — pool-utilization-driven scale-up + a conservative scale-down gate + per-spec idle grace; unset ⇒ the default rules), max-lifetime / container-lifetime (#334 — recycle past the age cap), drain-timeout (#335 — grace for a busy max-lifetime recycle), stop-on-logout (#337 — a signed-in user’s sticky sessions end immediately on logout), and concurrent-requests-per-replica (#336 — API specs have no sticky sessions, so the scaler meters their capacity by in-flight requests against this per-replica cap). ruscker validate --strict-compat no longer flags any of them.

Routing strategies

  • least-connections — pick replica with most free seats. Default for Shiny, Streamlit, Dash, Voilà.
  • round-robin — cycle through replicas. Default for API.
  • weighted-random — random with weights = remaining seats. Not yet implemented (falls back to round-robin).
  • resource-aware — pick based on CPU/mem load. Requires phase 4 metrics. Falls back to least-connections.

Spec kind dispatch

The effective kind drives runtime behavior:

  • Explicit type field wins if set
  • Otherwise: container-image set → shiny, unset → external

The kind controls:

  • Whether sticky session cookies are issued (shiny, streamlit, dash, voila — yes; api, external — no)
  • Default routing strategy
  • Default seats-per-container (10 for web-framework apps — Shiny, Streamlit, Dash, Voilà — which serve many sessions per process; 100 for API; 0 for external). A single-user IDE (RStudio, Jupyter) should set seats-per-container: 1 so each visitor gets an isolated container.
  • Whether WebSocket forwarding is attempted

Environment variable interpolation

Any string value can use ${VAR_NAME} or ${VAR_NAME:-default}:

docker-registry-password: ${DOCKER_REGISTRY_PASSWORD}
heartbeat-rate: ${HEARTBEAT_RATE:-10000}

Rules:

  • Variable names: [A-Z_][A-Z0-9_]*
  • Missing variable without default: hard error at parse time
  • Missing variable with default: substituted with the default
  • Comments (lines starting with #) are not interpolated

This applies to the whole YAML file, not just credentials. Use it for any value that varies between environments.

template-properties

Free-form key-value bag. The current landing template uses:

KeyTypeNotes
logostringPath or URL to card image
coverstring (CSS)Card-cover background — a solid color or gradient. Empty ⇒ a per-kind tint
iconlock | lock_openAccess level
typeapp | package | talk | report | apiBadge category
subjectstringSubject/topic of the app — drives the Subject filter facet on the landing

featured (a top-level spec field, not a template-property): set featured: true to highlight the app in the landing’s Featured carousel above the filters. The carousel shows only when landing-customization.show-highlights is on (the default) and at least one spec is featured. Default false. | updated | string | Display date (DD/MM/YYYY) | | state | active | inactive | Whether to enable card | | link | URL | External URL for non-container specs |

You can add custom keys — they’re ignored unless the template uses them. Useful for future custom templates.

logging block

logging:
  file:
    name: logs/ruscker.log

Accepted for ShinyProxy compat. Ruscker uses tracing for logging and respects the RUST_LOG env var as well.

Not supported (MVP)

These ShinyProxy fields are accepted by the parser but currently ignored:

  • proxy.specs[*].kubernetes-* — Kubernetes backend, phase 6
  • proxy.specs[*].port — explicit upstream port (Ruscker uses api.port for APIs, auto-detects for Shiny)
  • proxy.specs[*].minimum-seats-available — pre-warm pool (planned)
  • proxy.specs[*].labels, proxy.specs[*].network-connections — phase 3.5
  • proxy.docker.* — global docker config (use defaults or env vars)

(proxy.specs[*].volumes and container-env / container-cmd are now supported — see “Containerized specs” above.)

Setting any of these will produce a startup warning but not an error. Run ruscker validate --strict-compat <config> to list every unsupported feature a config uses (and exit non-zero if any are found) — the recommended pre-flight check when migrating from ShinyProxy.