Configuration¶
iiiris is configured in three layers, each overriding the previous:
- Built-in defaults (
config.Default()) — what you get when no flag, file, or env var is supplied. - YAML file (optional, via
-config path.yaml) — overlays defaults. - Environment variables — overlay everything else;
IIIRIS_*prefix.
Zero-config operation is a project commitment: ./bin/iiirisd works with
nothing set, serving ./testdata/ on :8080.
A running server can edit a curated subset of these fields from the admin
UI (/admin/config) when started with -config: the edit is written back
to the YAML file, and log.level / image.max_pixel_area /
image.max_concurrent apply without a restart. See
admin.md for the editable subset and the
live-vs-restart split.
YAML shape¶
server:
addr: ":8080"
read_timeout: 30s
write_timeout: 60s
shutdown_timeout: 10s
log:
level: info # debug | info | warn | error
format: text # text | json
file: "" # path to rolling log file; empty = stderr only.
# Size-based rotation: 100 MB per file, 5 backups.
# Windows service mode writes here AND to Event Log.
sources:
default: filesystem # which backend serves identifiers by default
filesystem:
root: ./testdata
http:
base_url: "" # if non-empty, registers an HTTP source
timeout: 30s
s3:
bucket: "" # if non-empty, registers an S3 source
region: ""
prefix: ""
endpoint: "" # optional, for MinIO and other S3-compatibles
caches:
render:
backend: heap # none | heap | filesystem | s3 | redis (none = slot disabled)
max_bytes: 268435456 # 256 MiB; 0 = unbounded for heap/filesystem; required 0 for s3 / redis
ttl: 0 # 0 = no TTL; honored by redis backend (per-entry SET EX)
path: "" # filesystem only
s3: # s3 only
bucket: ""
region: ""
prefix: ""
endpoint: ""
redis: # redis only
addr: "" # e.g. redis.internal:6379
password: "" # inline (prefer password_env)
password_env: "" # env var name holding the password
db: 0
tls: false
prefix: "" # namespace for shared-Redis deployments
coordinator: # filesystem only; promotes to distributed mode
redis: # required: Redis as the shared-index coordinator
addr: "" # set to enable distributed mode
password_env: ""
db: 0
tls: false
prefix: ""
info:
backend: heap
max_bytes: 16777216
origin:
backend: heap
max_bytes: 268435456
manifest: # IIIF Presentation documents (manifests + collections)
backend: heap
max_bytes: 16777216
image:
max_pixel_area: 100000000 # source area guard (rejects oversized originals up front)
max_output_area: 100000000 # output area guard (rejects huge upscales; 0 = unbounded)
max_output_width: 0 # output width cap; 0 = unbounded
max_output_height: 0 # output height cap; 0 = unbounded
max_concurrent: 0 # in-flight render cap; explicit 0 = unbounded; omit the field → defaults to host CPU count
max_concurrent_per_ip: 0 # in-flight IIIF requests per client IP; 0 = disabled (opt-in)
tile_size: 512 # info.json tiles edge length; 0 = no tiles array
jpeg_progressive: false # progressive JPEG output; false = baseline (~3x faster encode)
metadata: strip # strip = remove EXIF/XMP/IPTC + normalize colour/orientation; preserve = byte-faithful passthrough
vips_concurrency: 0 # libvips worker threads per op; 0 = default (1)
vips_max_cache_mem: 0 # libvips operation-cache cap in bytes; 0 = default (50 MiB)
presentation: # IIIF Presentation API (manifests + collections)
enabled: true # default-on (stateless, part of the zero-config path); false = image-only
default_viewer: mirador # viewer at /view/{id}: mirador (self-hosted) | uv (Universal Viewer, from CDN); ?viewer= overrides per-request
validate: false # warn-only structural conformance checks on served/derived docs (logs problems; never rejects)
content_state: # IIIF Content State API (shareable "open here" links)
enabled: true # default-on (stateless); mounts /content-state/ mint+resolve and makes /view read iiif-content. Needs presentation.enabled
store: # bookmark store (Form-B hosting) — OFF by default
enabled: false # true mounts POST /content-state + GET /content-state/s/{id}
dir: "" # durable filesystem dir (required when store.enabled); never evicted
write_token: "" # POST requires `Authorization: Bearer <token>`; empty = writes refused
mcp: # Model Context Protocol server (read-only IIIF tools for AI agents)
enabled: false # OFF by default; true mounts the /mcp Streamable HTTP endpoint. See docs/mcp.md
metrics: # Prometheus exposition (opt-in)
enabled: false # master switch; everything below ignored when false
path: /metrics # route on the main listener
addr: "" # non-empty (e.g. ":9090") = dedicated listener instead of the main one
username: "" # both set = HTTP Basic required on the endpoint
password: "" # (Prometheus scrape configs support this via basic_auth)
sentry: # error tracking via the Sentry Go SDK (opt-in)
enabled: false # master switch; an empty dsn also disables the subsystem
dsn: "" # Sentry project ingestion endpoint; empty = disabled
environment: production # tags every event (e.g. production | staging)
release: "" # build attribution; empty derives it from the binary version
sample_rate: 1.0 # fraction of error events sent (0.0–1.0)
send_default_pii: false # false = strip client IP / cookies / auth headers / query strings
capture: # which error sources are reported (all default true)
panics: true # recovered panics (with stack traces) from the recover middleware
server_errors: true # requests ending in a 5xx status
internal_errors: true # source / decode / cache errors captured at the error site
tracing: # distributed tracing via OpenTelemetry (opt-in)
enabled: false # master switch; everything below ignored when false
otlp_endpoint: "" # OTLP/gRPC collector, e.g. "localhost:4317"; empty = sample but don't export
insecure: false # true = plain HTTP/2 (no TLS) to the collector
sample_rate: 0.0 # head-sampling probability for root spans (0.0–1.0); 0 = trace nothing
propagate_upstream: false # true = inject W3C traceparent on outbound HTTP source fetches (not S3)
hook:
type: noop # noop | lookup | js | lua | webhook
lookup: # used when type: lookup
backend: filesystem
template: "{{.Identifier}}"
js: # used when type: js (recommended scripting engine)
path: "" # path to .js file
script: "" # OR inline script (one or the other)
watch_interval: 5s # hot-reload poll cadence (path mode); 0=default, <0=disabled
timeout: 1s # per-call execution budget; 0=default (1s)
lua: # used when type: lua — DEPRECATED, removed in 1.0; use js
path: "" # path to .lua file
script: "" # OR inline script (one or the other)
watch_interval: 5s # hot-reload poll cadence (path mode); 0=default, <0=disabled
webhook: # used when type: webhook
url: ""
timeout: 2s
auth_header: ""
auth: # absent or empty profiles → AllowAll, no auth services
profiles:
reading-room: # profile name (used by rules: and the login route)
pattern: clickthrough # clickthrough | active | kiosk | external
label: # BCP-47 language map (per IIIF JSON-LD)
en: ["Reading Room"]
heading: # optional; shown on the login page and probe denials
en: ["Restricted material"]
description: # optional; longer note on probe denials
en: ["You must agree to view this material."]
confirm_label: "I agree" # CTA on the built-in clickthrough button
logo_url: "" # optional <img src> in the built-in login UI
access_service:
backend: builtin # builtin | header | external | oidc
template: "" # path to override the built-in login HTML (builtin only)
header: X-Forwarded-User # request header to trust (header backend)
url: "" # operator's auth page URL (external backend)
callback_secret_env: "" # env var holding HMAC-SHA256 secret (external)
issuer: "" # OIDC issuer URL (oidc backend)
client_id: "" # OIDC client_id
client_secret_env: "" # env var holding OIDC client_secret
scopes: [] # OIDC scopes; defaults to [openid, profile, email]
user_claim: sub # OIDC claim used as session user; defaults to "sub"
token_ttl: 5m # per-profile override for the global default
session_ttl: 24h # per-profile override for the global default
substitute:
max_size: "" # IIIF size string for degraded view on deny ("!400,400")
users: # inline bcrypt/apr1 map (active pattern + builtin backend)
alice: "$2y$10$..."
htpasswd_file: "" # path to htpasswd; mtime-watched; beats users when both set
kiosk:
allowed_ips: [] # IPs and/or CIDRs (v4 or v6); kiosk pattern only
rules: # first match wins; identifiers matching no rule are public
- match: "special/*" # exact | "prefix/*" | "*"
profile: reading-room
cookie:
name: "" # session cookie name; defaults to "iiiris_session"
path: "" # auto-derived from X-Forwarded-Prefix when empty
cors:
probe_origins: [] # empty = wildcard ("*")
token_origins: [] # REQUIRED list for the postMessage token flow
session:
backend: heap # heap | filesystem | s3 | redis
path: "" # required when backend=filesystem
s3: # required when backend=s3
bucket: ""
region: ""
prefix: ""
endpoint: "" # optional, for MinIO et al.
redis: # required when backend=redis
addr: "" # e.g. redis.internal:6379
password: "" # inline (prefer password_env)
password_env: "" # env var name
db: 0
tls: false
prefix: "" # namespace for shared-Redis deployments
ttl: 10m # sweep interval for expired sessions (heap only)
token_ttl: 5m # global Bearer-token lifetime
session_ttl: 24h # global server-side session lifetime
trusted_proxies: [] # CIDRs whose X-Forwarded-For is honored
See iiif-auth.md for what each field means in the
context of the four patterns and four backends.
Env vars¶
| Var | Overrides |
|---|---|
IIIRIS_ADDR |
server.addr |
IIIRIS_LOG_LEVEL |
log.level |
IIIRIS_LOG_FORMAT |
log.format |
IIIRIS_LOG_FILE |
log.file |
IIIRIS_SOURCE_DEFAULT |
sources.default |
IIIRIS_FS_ROOT |
sources.filesystem.root |
IIIRIS_HTTP_BASE_URL |
sources.http.base_url |
IIIRIS_HTTP_TIMEOUT |
sources.http.timeout |
IIIRIS_S3_BUCKET |
sources.s3.bucket |
IIIRIS_S3_REGION |
sources.s3.region |
IIIRIS_S3_PREFIX |
sources.s3.prefix |
IIIRIS_S3_ENDPOINT |
sources.s3.endpoint |
IIIRIS_MAX_PIXEL_AREA |
image.max_pixel_area |
IIIRIS_IMAGE_METADATA |
image.metadata |
IIIRIS_VIPS_CONCURRENCY |
image.vips_concurrency |
IIIRIS_VIPS_MAX_CACHE_MEM |
image.vips_max_cache_mem |
IIIRIS_METRICS_ENABLED |
metrics.enabled |
IIIRIS_METRICS_PATH |
metrics.path |
IIIRIS_METRICS_ADDR |
metrics.addr |
IIIRIS_METRICS_USER |
metrics.username |
IIIRIS_METRICS_PASS |
metrics.password |
IIIRIS_SENTRY_ENABLED |
sentry.enabled |
IIIRIS_SENTRY_DSN |
sentry.dsn |
IIIRIS_SENTRY_ENVIRONMENT |
sentry.environment |
IIIRIS_SENTRY_RELEASE |
sentry.release |
IIIRIS_SENTRY_SAMPLE_RATE |
sentry.sample_rate |
IIIRIS_SENTRY_SEND_DEFAULT_PII |
sentry.send_default_pii |
IIIRIS_SENTRY_CAPTURE_PANICS |
sentry.capture.panics |
IIIRIS_SENTRY_CAPTURE_SERVER_ERRORS |
sentry.capture.server_errors |
IIIRIS_SENTRY_CAPTURE_INTERNAL_ERRORS |
sentry.capture.internal_errors |
IIIRIS_TRACING_ENABLED |
tracing.enabled |
IIIRIS_TRACING_OTLP_ENDPOINT |
tracing.otlp_endpoint |
IIIRIS_TRACING_INSECURE |
tracing.insecure |
IIIRIS_TRACING_SAMPLE_RATE |
tracing.sample_rate |
IIIRIS_TRACING_PROPAGATE_UPSTREAM |
tracing.propagate_upstream |
IIIRIS_ADMIN_USER |
Admin Basic-auth user (no YAML equivalent) |
IIIRIS_ADMIN_PASS |
Admin Basic-auth password |
Env-var policy. Overrides cover the flat operational fields — bind
address, logging, the default source and its roots, the image
guards/knobs above, the flat metrics.* block, the sentry.* block
(including the sentry.capture.* toggles), and the tracing.* block.
Everything else is YAML-only: the rest of image.*
(max_output_area/width/height, max_concurrent,
max_concurrent_per_ip, tile_size, jpeg_progressive) and the structured
caches.* / hook.* / auth.* blocks, which are too nested for env vars.
For env-only deployments (Helm, 12-factor), set the flat tweaks via env on
top of a mounted config file that carries the structured blocks.
AWS credentials follow the standard AWS SDK chain
(AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, ~/.aws/credentials, IMDS).
Windows-specific defaults¶
When iiirisd is installed via the Windows MSI (see
deployment.md) the installer
seeds a default config.yaml at
%ProgramData%\iiiris\config\config.yaml with these Windows-aware
values:
| Setting | Default in installed config |
|---|---|
server.addr |
:8080 |
log.level |
info |
log.format |
json (recommended for service mode — parses cleanly) |
log.file |
C:\ProgramData\iiiris\logs\iiirisd.log |
sources.filesystem.root |
C:\path\to\your\images (operator-editable placeholder) |
caches.render.path |
C:\ProgramData\iiiris\cache\render |
caches.origin.path |
C:\ProgramData\iiiris\cache\origin |
When iiirisd runs under the Windows Service Control Manager (the
MSI's service-install mode), an additional Windows Event Log
handler writes to the Event Log source iiiris registered by the
installer under HKLM\SYSTEM\CurrentControlSet\Services\EventLog\
Application\iiiris. The handler maps slog levels to Event Log
severity (Info → Info, Warn → Warning, Error → Error) and truncates
records at the Event Log's ~32 KB ceiling. Events appear in Event
Viewer → Applications and Services Logs → iiiris. See
deployment.md for operational
detail.
Validation¶
iiiris fails fast at boot on invalid cache config:
- Unknown
backendvalue →unknown backend "x" (want "heap", "filesystem", "s3", or "redis") backend: filesystemwith emptypath→filesystem backend requires a pathbackend: s3withmax_bytes != 0→s3 backend ignores max_bytes (use S3 lifecycle policies)backend: s3with emptys3.bucket→s3.bucket is requiredbackend: rediswithmax_bytes != 0→redis backend ignores max_bytes (use Redis MAXMEMORY + an eviction policy)backend: rediswith emptyredis.addr→redis backend requires redis.addrbackend: rediswith an unreachableredis.addr→ connection PING error surfaced at bootbackend: rediswith acoordinator.redis.addralso set →backend=redis and coordinator.redis are mutually exclusive(coordinator promotes filesystem backend to distributed mode; pick one)backend: filesystemwithcoordinator.redis.addrset and unreachable → connection PING error surfaced at boot
…and on invalid auth: config:
auth.rulesreferencing a profile name not inauth.profiles→auth: unknown profileaccess_service.backend: externalwithouturl:orcallback_secret_env:→ build error naming the missing fieldaccess_service.backend: externalwithcallback_secret_env:set but the named env var unset/empty → build erroraccess_service.backend: oidcmissingissuer:/client_id:/client_secret_env:→ build erroraccess_service.backend: <other>→unknown access_service.backendauth.session.backendother thanheap/filesystem/s3/redis→unsupported session backendauth.session.backend: filesystemwith emptypath:→session backend=filesystem requires session.path
image.metadata does not fail fast: any value other than the exact
string preserve is treated as strip. The failure mode deliberately
favours privacy — a typo leaves stripping on rather than silently
shipping EXIF/GPS.
- auth.session.backend: filesystem with an unwritable path: → mkdir
error surfaced at build time
- auth.session.backend: s3 with empty s3.bucket: →
session backend=s3 requires session.s3.bucket
- auth.session.backend: redis with empty redis.addr: →
session backend=redis requires session.redis.addr
- auth.session.backend: redis with an unreachable address → PING
error surfaced at boot
- auth.kiosk.allowed_ips or auth.trusted_proxies containing an
unparseable IP/CIDR → build error naming the entry
- auth.profiles.<name>.substitute.max_size not parseable as an IIIF
v3 size string → build error