Skip to content

Every parameter

Every field a configuration document may carry, with its type, its bounds and an example. Generated from the schema, which is generated from the Rust types that enforce the rules -- so a bound stated here is the bound the server applies.

The dashboard links here: the (i) beside a field opens that field's section.

For how the pieces fit together, read Configuration reference; for how to use them, the guides under Usage.


admin

The admin API's listener, its tokens, and who may do what.

Type object
Required yes
Rules no unknown keys
admin:
  host: 127.0.0.1
  port: 8080
  upload:
    limit: 1048576

admin.access

Who may perform each admin action. Every action defaults to the admin group, reads included.

Type object
Required no
Rules no unknown keys

admin.access.create

Add a proxy. Refused for public by rule V34.

Type string or list of string
Required no
admin:
  access:
    create: public

admin.access.delete

Remove a proxy. Refused for public by rule V34.

Type string or list of string
Required no
admin:
  access:
    delete: public

admin.access.list

List the proxies. A listing exposes upstream URLs and injected headers.

Type string or list of string
Required no
admin:
  access:
    list: public

admin.access.read

Read one proxy document, credentials in its url included.

Type string or list of string
Required no
admin:
  access:
    read: public

admin.access.update

Replace a proxy. Refused for public by rule V34.

Type string or list of string
Required no
admin:
  access:
    update: public

admin.access.upload

Upload or delete a template file. Refused for public by rule V34.

Type string or list of string
Required no
admin:
  access:
    upload: public

admin.auth

Which header carries the bearer token.

Type object
Required no
Rules no unknown keys

admin.auth.header

The header a caller presents its token in, as Bearer <token>. Defaults to X-Proxy-Authorization.

Type string
Required no
admin:
  auth:
    header: X-Request-Id

admin.dashboard

Serve the browser dashboard from the admin listener's root.

On by default. Off means /, /static/* and /robots.txt are not routed at all and answer 404 like any other unknown path; the JSON API is untouched either way.

An Option and skipped when absent, for the reason given on groups: adding it must not change the canonical YAML of configurations written before it existed. Read it through [AdminConfig::is_dashboard_enabled].

Type boolean
Required no
admin:
  dashboard: true

admin.enable

Whether to run the admin listener at all.

Defaults to on. Off means the port is never bound and no admin task starts; the proxy and the control socket are untouched, so doppel config reload still works and is then the only way in.

The validation rules do not consult this. A configuration that is only safe because nothing serves it is a trap set for whoever turns the listener on later, and they will not re-read the rules first.

Type boolean
Required no
admin:
  enable: true

admin.groups

Which names access may reference, here and in a proxy's overrides.

["*"], the default, allows any. A concrete list allows exactly those: ["admin", "ci"] permits admin and ci and refuses user.

[] names nobody, which leaves no action anything to reference -- so it means the same thing as public: true, and is treated as it. It used to mean a configuration with no legal value for create, update, delete or upload: V36 refused their admin default and V34 refused public, so the document could not be written at all.

public and admin are never governed by this. public is the absence of a subject rather than a name; admin is the fallback every action already has, and a list that revoked it would produce the same unsatisfiable state as [] once did.

Checked by rule V36, not by this type: it compares one field against another, which is what is left for the rule set once the types have taken everything they can decide alone.

An Option rather than a Vec defaulting to ["*"], and skipped when absent, so that adding this field did not change the canonical YAML of every configuration written before it existed. The revision is derived from that YAML, so a materialised default would have made every stored configuration fail its own revision check on the first load after the upgrade. Read it through [AdminConfig::allowed_groups].

Type array
Required no
admin:
  groups:
    - "*"

admin.host

An IP address, not a hostname: a name would have to be resolved, and which address it resolves to is not the configuration's to decide. utoipa has no schema for IpAddr, so it is described here as the string it is written as.

Type string
Required yes
admin:
  host: 127.0.0.1

admin.port

The TCP port the admin API listens on. Must differ from server.port.

Type integer
Required yes
Rules 1 to 65535
admin:
  port: 8080

admin.public

Serve the whole admin API unauthenticated.

false by default. true makes every action public and leaves no name to reference, so groups is effectively empty and access effectively all-public -- whatever either of them says. Anything they did say is reported as a startup advisory rather than refused, so a configuration being made temporarily public does not have to be gutted first and rebuilt afterwards.

This overrides rule V34, which otherwise refuses a public write action. V34 exists so that an unauthenticated writable proxy set cannot happen by omission; a field named public set to true is not an omission. Think of it as the flag V34 was holding the line for.

An Option and skipped when absent, for the reason given on groups: adding it must not change the canonical YAML of configurations written before it existed. Read it through [AdminConfig::is_public].

Type boolean
Required no
admin:
  public: true

admin.title

The heading the dashboard shows, and the browser tab's title.

Defaults to Doppel. Useful when several Doppels are open at once and the tabs are otherwise identical.

An Option and skipped when absent, for the same reason as dashboard. Read it through [AdminConfig::title].

Type string
Required no
Rules 1 to 64 characters
admin:
  title: billing-api (staging)

admin.tokens

The tokens that may call the admin API. Names and token values are each unique. DOPPEL_ADMIN_TOKENS can supply these instead.

Type list of object
Required no
admin:
  tokens:
    - name: billing-api
      group: billing-api
      token: replace-me-00000000000000000000000

admin.tokens[].group

The group it belongs to. admin and user are predefined; any other name must be carried by at least one token.

Type string
Required yes
Rules matches ^[A-Za-z0-9_-]{2,64}$; 2 to 64 characters
admin:
  tokens:
    - group: billing-api

admin.tokens[].name

What to call this token in access lists and in logs. Never the secret itself.

Type string
Required yes
Rules matches ^[A-Za-z0-9_-]{2,64}$; 2 to 64 characters
admin:
  tokens:
    - name: billing-api

admin.tokens[].token

The secret the caller sends. A version 4 UUID is the recommended shape.

Type string
Required yes
Rules 32 to 255 characters
admin:
  tokens:
    - token: replace-me-00000000000000000000000

admin.upload

Bounds an uploaded template file.

Type object
Required yes
Rules no unknown keys
admin:
  upload:
    limit: 1048576

admin.upload.limit

Largest template file the admin API accepts. A larger upload is refused with 413.

Type integer or string
Required yes
Rules 1 to 1073741824
admin:
  upload:
    limit: 1048576

control

The Unix socket doppel config reload talks to.

Type object
Required no
Rules no unknown keys

control.socket

Path to the control socket, created with mode 0600 and removed on shutdown. Its parent directory must already exist. A filesystem path. utoipa has no schema for PathBuf, so it is described as the string it is written as.

Type string
Required no
control:
  socket: /tmp/doppel.sock

logging

Log level and output format.

Type object
Required no
Rules no unknown keys

logging.format

json for machines, text for a terminal.

Type string
Required no
Rules one of json, text
logging:
  format: json

logging.level

The lowest level that is logged. RUST_LOG overrides it when set and non-empty.

Type string
Required no
Rules one of trace, debug, info, warn, error
logging:
  level: trace

proxies

The proxies this instance serves, in the order they are tried.

May be empty or left out: Doppel then starts and serves the admin API, and a request is answered 503 NO_PROXIES_CONFIGURED until a proxy is added by reload or over that API.

Type list of object
Required no
proxies:
  - name: billing-api
    type: http
    url: https://example.com/api/

proxies[].access

Overrides the admin access rules for this proxy alone.

Type object
Required no
Rules no unknown keys

proxies[].access.delete

Who may remove this proxy. Absent leaves the global rule.

Type string or list of string
Required no
proxies:
  - access:
      delete: public

proxies[].access.read

Who may read this proxy's document. Absent leaves the global rule.

Type string or list of string
Required no
proxies:
  - access:
      read: public

proxies[].access.update

Who may replace this proxy. Absent leaves the global rule.

Type string or list of string
Required no
proxies:
  - access:
      update: public

proxies[].access.upload

Who may upload templates for this proxy. Absent leaves the global rule.

Type string or list of string
Required no
proxies:
  - access:
      upload: public

proxies[].body_limit

Bounds the request body a matched mock is allowed to buffer in order to extract from it; phase 1 streams bodies deliberately, and reading .content.items needs the whole thing in hand. See rule V33.

Type integer or string
Required no
Rules 1 to 1073741824
proxies:
  - body_limit: 1048576

proxies[].headers

Headers injected into every outbound request, overriding whatever the client sent by the same name.

Type map of string to string
Required no
proxies:
  - headers:
      X-Request-Id: staging

proxies[].latency

Makes a share of requests take a chosen time. Applies to mocked responses too; a mock may override the figure.

Type object
Required no
Rules no unknown keys
proxies:
  - latency:
      percentage: 0.25
      min: 0.25
      max: 0.25

proxies[].latency.max

Upper bound of the delay, in seconds. Must be at least min.

Type number
Required yes
Rules 0 to 300
proxies:
  - latency:
      max: 0.25

proxies[].latency.min

Lower bound of the delay, in seconds. The delay is a target for the whole response: time the upstream already spent is subtracted.

Type number
Required yes
Rules 0 to 300
proxies:
  - latency:
      min: 0.25

proxies[].latency.percentage

The share of requests to delay, as a fraction.

Type number
Required yes
Rules 0 to 1
proxies:
  - latency:
      percentage: 0.25

proxies[].loss

Drops a share of requests rather than forwarding them. Not applied to a request a mock answered.

Type object
Required no
Rules no unknown keys
proxies:
  - loss:
      percentage: 0.25
      status: 200

proxies[].loss.percentage

The share of requests to drop, as a fraction. 0.1 is one in ten.

Type number
Required yes
Rules 0 to 1
proxies:
  - loss:
      percentage: 0.25

proxies[].loss.status

The status a dropped request is answered with, rather than being left to hang.

Type integer
Required yes
Rules 100 to 599
proxies:
  - loss:
      status: 200

proxies[].mocks

Mocks in the order they are tried. First match wins, and patterns are unanchored, so a general one placed first shadows the rest.

Type list of object
Required no
proxies:
  - mocks:
      - name: billing-api
        request:
          method: GET
          url: "/api/(?P<id>\\d+)/"
        response:
          status: 200

proxies[].mocks[].name

Names this mock in the mock log field and the hit counter. Unique within the proxy.

Type string
Required yes
Rules matches ^[A-Za-z0-9_-]{2,64}$; 2 to 64 characters
proxies:
  - mocks:
      - name: billing-api

proxies[].mocks[].proxy

Per-mock overrides of the proxy's replace, loss and latency.

Type object
Required no
Rules no unknown keys

proxies[].mocks[].proxy.latency

Replaces the proxy's latency for this mock's responses, rather than adding to it.

Type object
Required no
Rules no unknown keys
proxies:
  - mocks:
      - proxy:
          latency:
            percentage: 0.25
            min: 0.25
            max: 0.25

proxies[].mocks[].proxy.latency.max

Upper bound of the delay, in seconds. Must be at least min.

Type number
Required yes
Rules 0 to 300
proxies:
  - mocks:
      - proxy:
          latency:
            max: 0.25

proxies[].mocks[].proxy.latency.min

Lower bound of the delay, in seconds. The delay is a target for the whole response: time the upstream already spent is subtracted.

Type number
Required yes
Rules 0 to 300
proxies:
  - mocks:
      - proxy:
          latency:
            min: 0.25

proxies[].mocks[].proxy.latency.percentage

The share of requests to delay, as a fraction.

Type number
Required yes
Rules 0 to 1
proxies:
  - mocks:
      - proxy:
          latency:
            percentage: 0.25

proxies[].mocks[].proxy.loss

Drops a share of the requests this mock would have answered. Not inherited from the proxy: a mock without it is never dropped.

Type object
Required no
Rules no unknown keys
proxies:
  - mocks:
      - proxy:
          loss:
            percentage: 0.25
            status: 200

proxies[].mocks[].proxy.loss.percentage

The share of requests to drop, as a fraction. 0.1 is one in ten.

Type number
Required yes
Rules 0 to 1
proxies:
  - mocks:
      - proxy:
          loss:
            percentage: 0.25

proxies[].mocks[].proxy.loss.status

The status a dropped request is answered with, rather than being left to hang.

Type integer
Required yes
Rules 100 to 599
proxies:
  - mocks:
      - proxy:
          loss:
            status: 200

proxies[].mocks[].proxy.replace

Overrides the proxy's replace for requests this mock matches.

Type number
Required no
Rules 0 to 1
proxies:
  - mocks:
      - proxy:
          replace: 0.25

proxies[].mocks[].request

What this mock matches, and what it takes out of the request.

Type object
Required yes
Rules no unknown keys
proxies:
  - mocks:
      - request:
          method: GET
          url: "/api/(?P<id>\\d+)/"

proxies[].mocks[].request.body

Variable name -> selector such as .content.items.

Type map of string to string
Required no
proxies:
  - mocks:
      - request:
          body:
            key: .content.items

proxies[].mocks[].request.headers

Variable name -> request header name.

Type map of string to string
Required no
proxies:
  - mocks:
      - request:
          headers:
            key: X-Request-Id

proxies[].mocks[].request.method

The method this mock answers, matched exactly and upper case.

Type string
Required yes
Rules one of GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, CONNECT, QUERY, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK
proxies:
  - mocks:
      - request:
          method: GET

proxies[].mocks[].request.query

Variable name -> selector such as .filter.

Type map of string to string
Required no
proxies:
  - mocks:
      - request:
          query:
            key: .content.items

proxies[].mocks[].request.url

A regex matched against the request path, unanchored. Named capture groups become template variables.

Type string
Required yes
proxies:
  - mocks:
      - request:
          url: "/api/(?P<id>\\d+)/"

proxies[].mocks[].response

What it answers with.

Type object
Required yes
Rules no unknown keys
proxies:
  - mocks:
      - response:
          status: 200

proxies[].mocks[].response.body

A template rendered and sent as text/plain. Exclusive with json and template.

Type string
Required no
proxies:
  - mocks:
      - response:
          body: widget {{ id }} is ready

proxies[].mocks[].response.headers

Header name -> template producing the value.

The value is a template, not a header value: what it renders to is only a header value once a request has been served, which is checked there. The name is a name now.

Type map of string to string
Required no

proxies[].mocks[].response.json

A template whose rendered output must be valid JSON, sent as application/json. Exclusive with body and template.

Type string
Required no
proxies:
  - mocks:
      - response:
          json: "{\"id\": \"{{ id }}\"}"

proxies[].mocks[].response.status

The status to answer with.

Type integer
Required yes
Rules 100 to 599
proxies:
  - mocks:
      - response:
          status: 200

proxies[].mocks[].response.template

A template file under this proxy's template directory. Read per request, so it may be uploaded after the configuration was loaded. Exclusive with body and json.

Type string
Required no
Rules at most 200 characters
proxies:
  - mocks:
      - response:
          template: put.json.j2

proxies[].name

Names this proxy in X-Proxy-Name, in metrics labels, in log lines and as its template subdirectory. Unique within the document.

Type string
Required yes
Rules matches ^[A-Za-z0-9_-]{2,32}$; 2 to 32 characters
proxies:
  - name: billing-api

proxies[].replace

What share of requests a matching mock actually answers; the rest go upstream. Defaults to 1.0, so a matching mock answers.

Type number
Required no
Rules 0 to 1
proxies:
  - replace: 0.25

proxies[].resolve

How a request is matched to this proxy: by header, or as the default.

Type object
Required no
Rules no unknown keys

proxies[].resolve.header

The header carrying the proxy name. Required when type: header, and meaningless otherwise.

Type string
Required no
proxies:
  - resolve:
      header: X-Request-Id

proxies[].resolve.type

default takes anything unclaimed; header takes requests naming this proxy in header.

Type string
Required no
Rules one of default, header
proxies:
  - resolve:
      type: default

proxies[].rewrite_redirects

Whether a redirect whose Location points back into the space this proxy forwards is rewritten to point at Doppel instead. Absent means enabled.

On by default because the alternative is a silent failure: Host is replaced with the upstream's authority, so the upstream's Location names the upstream, and a client following it leaves Doppel -- along with every fault and every mock -- with nothing reported. Turn it off to have the response relayed byte for byte, which is what a client being tested against redirect handling itself needs.

Type boolean
Required no
proxies:
  - rewrite_redirects: true

proxies[].rewrite_urls

Whether the upstream's own address is rewritten to Doppel's inside the bodies this proxy relays. Absent means enabled.

The same problem rewrite_redirects solves, one layer down: a page, a script or a JSON document that names https://api.example.com/v2/ sends the client straight there on the next request, past every fault and every mock. Rewriting the body keeps it inside Doppel.

Only the exact host is replaced. https://cdn.api.example.com/ is a different host and is left alone, because Doppel does not proxy it and pointing it at itself would break a page rather than keep it working.

Only text bodies, and only ones the upstream did not compress. Turn it off for a client being tested against the bytes the upstream actually sent.

Type boolean
Required no
proxies:
  - rewrite_urls: true

proxies[].timeout

Bounds the whole upstream exchange, in seconds. Exceeding it is 504 UPSTREAM_TIMEOUT. Defaults to 30.

Type integer
Required no
Rules 1 to 3600
proxies:
  - timeout: 30

proxies[].type

What it forwards. http is the only value.

Type string
Required yes
Rules one of http
proxies:
  - type: http

proxies[].url

The upstream base. A request path is grafted underneath it, and the result can never escape it -- so a base with a path confines the proxy to that subtree.

Type string
Required yes
proxies:
  - url: https://example.com/api/

sentry

Optional error reporting. Absent, or an empty DSN, disables it.

Type object
Required no
Rules no unknown keys
sentry:
  dsn: https://key@o0.ingest.sentry.io/0

sentry.dsn

The Sentry DSN to report to. Empty disables reporting, so a deployment can blank it without removing the section.

DOPPEL_SENTRY_DSN overrides it. A DSN carries the key that authorises sending events, so a deployment that keeps credentials in the environment can leave this field out entirely; an empty variable counts as unset and leaves this value in force.

Type string
Required yes
sentry:
  dsn: https://key@o0.ingest.sentry.io/0

server

Where the proxy listens for the traffic being forwarded or mocked.

Type object
Required yes
Rules no unknown keys
server:
  host: 127.0.0.1
  port: 8080

server.external_url

Where clients reach this Doppel, when that is not host:port.

Behind a container port mapping, a load balancer or an ingress, the address Doppel bound is not the address a client used -- and a rewritten Location has to name the second. Doppel cannot infer it: Host is a claim by the caller, and building a redirect out of it hands the caller the redirect.

Absent, host and port are used, with a wildcard bind (0.0.0.0, ::) read as loopback -- which is right for a laptop or a pod reached at its own address, and wrong behind a port mapping or an ingress, where the client used neither. Doppel logs the address it settled on at startup.

DOPPEL_EXTERNAL_URL overrides it. Part of server, so a change takes a restart like the rest of that section.

Type string
Required no
server:
  external_url: https://doppel.example.com/

server.host

An IP address, not a hostname: a name would have to be resolved, and which address it resolves to is not the configuration's to decide. utoipa has no schema for IpAddr, so it is described here as the string it is written as.

Type string
Required yes
server:
  host: 127.0.0.1

server.port

The TCP port proxied traffic arrives on. Must differ from admin.port.

Type integer
Required yes
Rules 1 to 65535
server:
  port: 8080

templates

Where mock template files are read from and uploaded to.

Type object
Required no
Rules no unknown keys

templates.dir

Directory holding mock templates, one subdirectory per proxy. Created at startup if absent. A filesystem path. utoipa has no schema for PathBuf, so it is described as the string it is written as.

Type string
Required no
templates:
  dir: ./templates