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.
Command-line flags¶
The flag surface describes the binary, not the configuration: there is
no -addr or -fs-root, because those belong to the three layers above.
The full contract — including exit codes — is
specs/cli.md.
| Flag | Effect |
|---|---|
-config <path> |
Overlay the YAML file at path (layer 2 above). |
-version |
Print build identity and exit. |
-check-config |
Load + validate the configuration and exit. |
-print-config |
Print the effective configuration as YAML and exit. |
The last three are one-shot: they report and exit without binding a listener. Everything else is configuration.
$ iiirisd -version
iiirisd v0.9.1 (linux/amd64, go1.25.0, libvips 8.15.1)
$ iiirisd -check-config -config /etc/iiiris/iiiris.yaml
config OK: /etc/iiiris/iiiris.yaml
$ iiirisd -check-config -config /etc/iiiris/broken.yaml
config: image.supersample: 3 unsupported (want 1, 2, or 4)
$ echo $?
2
-check-config as a deploy preflight. It runs exactly the load and
validation startup runs — same defaults, same file, same IIIRIS_*
overrides, same Validate — so anything it rejects would have stopped a
restart with the same message and the same exit code.
It is a config check, not a full dress rehearsal. It does not build
subsystems, so the boot-time failures listed under
Validation below — unknown cache backend, a filesystem
backend with no path, auth.rules naming a profile that doesn't exist,
an unreachable Redis — are not caught by the preflight; they surface
at startup, also with exit 2. Nor does it check that the filesystem root
exists or that an origin answers; those are runtime conditions, reported
by /ready once the server is up. See
deployment.md for the systemd
ExecStartPre form.
-print-config for "what is this box actually running?" It prints the
merged result of all three layers, which is otherwise a matter of reading
three sources and applying the precedence rules by hand. Secrets are
masked — sentry.dsn, metrics.password,
content_state.store.write_token, hook.webhook.auth_header, every
redis.password, and the inline auth.profiles.*.users hashes each print
as ***. An empty secret stays empty, so the dump never implies a
credential is set when it isn't.
Because secrets are masked, the output is a diagnostic, not a config
file: feeding it back to -config would install *** as a literal
password. Read it, diff it between hosts, paste it into a bug report.
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
color: "" # srgb | respect-icc; "" (unset) = derive from metadata (strip->srgb, preserve->respect-icc)
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)
sharpen: 0.0 # unsharp mask on downscaled renders; 0 = off (default), 0.0–1.0 (0.5 = libvips' own default strength)
sharpen_sigma: 0.0 # unsharp radius; 0 = libvips default (1.5). Advanced
sharpen_x1: 0.0 # unsharp flat/jagged threshold; 0 = libvips default (2.0). Advanced
supersample: 1 # minimum linear decode headroom above the output before the residual resize; 1 = off (default) | 2 | 4
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 | 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)
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
sweep_interval: 10m # how often expired sessions/tokens are pruned
# (0 = default 10m; negative disables)
# NOTE: a cadence, not a lifetime.
# `ttl:` is the deprecated old name.
token_ttl: 5m # global Bearer-token lifetime
session_ttl: 24h # global server-side session LIFETIME
# (not to be confused with
# session.sweep_interval above)
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.
Render quality: sharpening + supersampling¶
Contracts:
specs/render-quality.md(sharpening) andspecs/image-decode.md(supersampling).
Four image: fields tune the perceived sharpness of derivatives. All
are off by default — the defaults reproduce iiiris's existing output
and performance byte-for-byte, so an upgrade changes nothing unless you
opt in.
| Field | Env var | Default | Range |
|---|---|---|---|
image.sharpen |
IIIRIS_IMAGE_SHARPEN |
0.0 (off) |
0.0–1.0 |
image.sharpen_sigma |
IIIRIS_IMAGE_SHARPEN_SIGMA |
0.0 (= libvips default, 1.5) |
>= 0.0 |
image.sharpen_x1 |
IIIRIS_IMAGE_SHARPEN_X1 |
0.0 (= libvips default, 2.0) |
>= 0.0 |
image.supersample |
IIIRIS_IMAGE_SUPERSAMPLE |
1 (off) |
0, 1, 2, or 4 |
0 and 1 both mean "off" — 0 is accepted as a synonym for 1, not
rejected. Any other unsupported value is a startup error, not a silent
clamp: an operator who typed supersample: 3 should learn it is not a
supported factor rather than have it quietly rounded.
sharpen — unsharp mask on downscaled renders¶
Downscaling discards high-frequency detail, so a reduced derivative reads
soft even when the resample itself is clean. image.sharpen applies an
unsharp mask that restores apparent acutance.
0.5reproduces libvips' own default unsharp strength. The operator-facing0.0–1.0strength maps linearly onto libvips' jagged-area slope asm2 = sharpen × 6.0, and libvips' defaultm2is3.0. That mapping is frozen: changing it would silently alter the output of every deployment that has setimage.sharpen. Start at0.5and adjust from there.sharpen_sigma(radius) andsharpen_x1(the flat/jagged threshold) are advanced knobs;0on either means "use libvips' default". Most operators only needsharpen.
When sharpening applies. Only when neither axis was enlarged and at least one axis shrank — i.e. detail was genuinely discarded and none was interpolated up:
| Request | Sharpened? |
|---|---|
Plain downscale (pct:25, !400,400, w, smaller than source) |
yes |
max / full, or a 1:1 crop (nothing shrank) |
no |
| Any upscale | no |
A distorting forced size that enlarges one axis while shrinking the other (e.g. ^2000,100 on an 800×600 region) |
no |
Sharpening an enlarged (interpolated) axis amplifies resample ringing, and sharpening a full-resolution tile amplifies sensor noise and JPEG artifacts on exactly the pixels a deep-zoom user is inspecting most closely — so those cases are left alone.
Two things are never sharpened, regardless of the setting:
- Overlays. The unsharp stage runs before overlay compositing, for both
target=sourceandtarget=outputoverlays, so a watermark stays crisp instead of acquiring halos. Seehooks.md. - CMYK pixels. libvips' unsharp mask works on the L channel of LAB, so sharpening an image that is still CMYK when the stage runs would force a lossy CMYK→LAB→CMYK round-trip. iiiris leaves it alone rather than degrade colour to add acutance.
This is a rule about CMYK pixels, not CMYK sources — which decode
path a request takes decides which it gets. An arbitrary-region or
full-decode render keeps the source's colour space, so a CMYK master really
does arrive at the stage in CMYK and is left unsharpened. A full-region
downscale decodes through libvips' shrink-on-load thumbnail operator,
which converts to sRGB on the way, so the same CMYK master arrives as sRGB
and is sharpened — safely, on sRGB pixels, with no round-trip. Either way
colour is never degraded for the sake of acutance; that is the guarantee.
supersample — force a real resample to run¶
This is the setting that fixes "why is my exact power-of-two request soft?"
iiiris decodes resolution-aware: it picks the deepest reduction the source codec supports (JPEG DCT shrink-on-load, JP2 / pyramidal-TIFF level) that still covers the output, then does a residual Lanczos resize down to the exact target. When the request happens to land on a power of two — an 8192 px region rendered at 1024 px — the decode lands exactly on the target, the residual resize becomes a no-op, and the bytes you serve are raw libjpeg-DCT / pyramid-level output that no Lanczos resampling ever touched. That is visibly soft.
supersample: 2 (or 4) sets the minimum linear headroom the decode
must leave above the output: the reduction backs off a level (or two), so a
real resample always runs on the way down.
- It changes pixels, never dimensions. Output
W×His identical atsupersample: 1,2, and4, soinfo.jsonnever misreports what the server produces. Seespecs/image-decode.md. - Cost. More decode work, not an N² blow-up: the reduction chosen for the decode never backs off below R=1, so supersampling can never cost more than a full-resolution decode of the region — that bound holds for both the arbitrary-region (tile) and full-region (thumbnail) paths. The full-region path adds one more clamp on top: its inflated decode target is additionally capped at the source dimensions, so the intermediate never exceeds full source resolution and a forced size can never trigger an enlarge-then-reduce round-trip. Measured on an Apple M1 Max against the bundled 800×600 sample (approximate, machine-dependent — measure on your own corpus):
| Path | ss=1 |
ss=2 |
ss=4 |
|---|---|---|---|
Full-region thumbnail (pct:12.5) |
1.59 ms | 3.18 ms | 4.73 ms |
Arbitrary-region tile (512×512 → w=128) |
1.03 ms | 1.71 ms | 2.59 ms |
Roughly 2.5–3× the render time from ss=1 to ss=4 on that corpus.
Large masters with deep pyramids will differ; the render cache absorbs
the cost for repeat requests.
supersample pairs naturally with sharpen: supersampling puts a real
Lanczos reduce back in the path, and sharpening restores the acutance that
any clean reduce discards.
Operational notes¶
- Both settings are restart-only. They are not part of the admin config
editor's live-reload subset (see
admin.md); the render-cache salt below is computed once at wiring. - Changing either setting rekeys the render cache rather than serving
stale renders. The render-quality settings are reduced to a short digest
that is appended to the render cache key (
|q=<hash>), so a retune (0.5→0.8) lands renders in a fresh key namespace and the old entries age out through normal eviction. No operator flush is needed, and nothing is ever served stale. - A default deployment evicts nothing on upgrade. The
|q=segment is append-only: withsharpen: 0andsupersample: 1the digest is empty and keys keep their existing bare-urlPathshape. Seespecs/scaffold.md"Cache contract".
Colour policy: sRGB vs ICC passthrough¶
Contract:
specs/color-policy.md.
image.color controls the colour space of derivatives,
independently of image.metadata (which only governs
EXIF/XMP/IPTC and orientation). Two explicit values:
| Field | Env var | Default | Values |
|---|---|---|---|
image.color |
IIIRIS_IMAGE_COLOR |
"" (derive from image.metadata) |
srgb, respect-icc |
srgb— normalize everything to sRGB: colour sources (≥ 3 bands) → sRGB with a ~0.5 KB micro ICC profile embedded; grayscale (≤ 2 bands) → sGray, never promoted to RGB; CMYK → sRGB.respect-icc— leave an RGB source's embedded ICC profile (and its pixels) untouched, so a wide-gamut viewer honours the original profile; grayscale stays untouched too. CMYK is still always converted to sRGB — that step is never negotiable, because no mainstream browser renders a CMYK JPEG correctly.
Any other value is a startup error, not a silent fallback.
Derivation when color is unset¶
Leaving image.color unset (the default) derives it from
image.metadata, so an upgrade that never touches image.color
changes nothing except the CMYK fix:
image.metadata |
effective color |
Result |
|---|---|---|
strip (default) |
srgb |
today's behaviour, unchanged |
preserve |
respect-icc |
today's behaviour, except CMYK now converts to sRGB |
This derivation is deliberate, operator-facing magic: reading
metadata: preserve in isolation does not otherwise tell you it
implies a colour policy, so this table is the whole story.
Setting color explicitly overrides the derivation in either
direction. The headline new combination this decoupling enables:
image:
metadata: strip # drop GPS / camera / serial
color: respect-icc # ...but keep the source's ProPhoto/AdobeRGB profile
Before this feature, the only way to keep a wide-gamut profile was
metadata: preserve, which also retained camera model, serial
number, and GPS — a privacy regression the metadata default exists to
prevent. metadata: strip + color: respect-icc expresses "strip
identifying metadata, keep colour fidelity" for the first time.
Colour policy is restart-only and, like sharpen/supersample,
its effective value folds into the render-quality cache salt
(|q=<hash>): srgb contributes nothing (zero churn), respect-icc
makes the salt non-empty. A preserve-mode deployment therefore
rekeys automatically on upgrade to this feature, so any stale
CMYK-passthrough entries a warm cache holds self-heal without an
operator flush. See specs/color-policy.md.
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_IMAGE_COLOR |
image.color |
IIIRIS_VIPS_CONCURRENCY |
image.vips_concurrency |
IIIRIS_VIPS_MAX_CACHE_MEM |
image.vips_max_cache_mem |
IIIRIS_IMAGE_SHARPEN |
image.sharpen |
IIIRIS_IMAGE_SHARPEN_SIGMA |
image.sharpen_sigma |
IIIRIS_IMAGE_SHARPEN_X1 |
image.sharpen_x1 |
IIIRIS_IMAGE_SUPERSAMPLE |
image.supersample |
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
…and on invalid image: render-quality config:
image.sharpenoutside0.0–1.0→image.sharpen: <v> out of range (want 0.0–1.0, 0 = off)image.sharpen_sigmabelow0→image.sharpen_sigma: <v> must be >= 0 (0 = libvips default)image.sharpen_x1below0→image.sharpen_x1: <v> must be >= 0 (0 = libvips default)image.supersampleother than0,1,2, or4(e.g.3or8) →image.supersample: <n> unsupported (want 1, 2, or 4)(0is accepted and treated the same as1, not rejected)image.colorother than"",srgb, orrespect-icc→image.color: "<v>" unsupported (want "srgb" or "respect-icc")