← Back to site Loading…

rescriptum

Configuration

Guide

Generated on August 30, 2026

Configuration

Configuration

Environment variables — and, optionally, a file to read them from, in either of two shapes. There is no command line to get wrong, and the variables are the whole configuration: both file formats set exactly the same things under exactly the same rules, so nothing you can write in a file means anything the environment could not.

The variables

VariableDefaultMeaning
RESCRIPTUM_CONFIGunsetRead defaults from this TOML file — see below
RESCRIPTUM_ENV_FILEunsetRead defaults from this KEY=value file — see below
RESCRIPTUM_STOREfilesfiles (a directory) or sqlite (a database)
RESCRIPTUM_ANSWERS_DIR/srv/answersDirectory of answer documents
RESCRIPTUM_DB_PATH/srv/answers.dbDatabase path, when RESCRIPTUM_STORE=sqlite
RESCRIPTUM_LISTEN_ADDR0.0.0.0:8000Listen address. :0 picks a free port, and the bound one is printed
RESCRIPTUM_WORKERSCPU countAsync runtime threads. Not a concurrency limit
RESCRIPTUM_MAX_CONNECTIONS2048In-flight connections before shedding with 503
RESCRIPTUM_TIMEOUT_SECS10Header-read timeout and whole-connection deadline
RESCRIPTUM_ANSWER_TOKENunsetBearer token the answer endpoint requires. Unset means open
RESCRIPTUM_ADMIN_ADDRunsetAdmin API listener. Unset means the admin API is off
RESCRIPTUM_ADMIN_TOKENunsetAdmin bearer token, 16+ characters. Required with RESCRIPTUM_ADMIN_ADDR
RESCRIPTUM_CAPTURE_DIRunsetRecord request bodies here. Unset means no capture
RESCRIPTUM_LOGallall, problems or off — see below
RESCRIPTUM_LOG_FILEunsetA file to append to, or stdout / stderr. Unset means stderr
RESCRIPTUM_MEDIA_DIRunsetInstaller images. Unset means no media and no media listener
RESCRIPTUM_MEDIA_ADDR0.0.0.0:8001The media listener, when there is a media directory
RESCRIPTUM_MEDIA_TIMEOUT_SECS600Whole-transfer deadline. Deliberately not the answer listener’s 10
RESCRIPTUM_MEDIA_MAX_CONNECTIONS16Concurrent transfers. Low on purpose: each holds its permit for minutes
RESCRIPTUM_PUBLIC_HOSTderivedThe host generated URLs name. A host, never a URL
RESCRIPTUM_BOOT_ALLOWunsetClient CIDRs allowed to fetch boot media. Unset means anyone who can reach the port
RESCRIPTUM_BOOT_DIRunsetLoaders and menus, handed out over TFTP. Unset means no TFTP at all
RESCRIPTUM_TFTP_ADDR0.0.0.0:69The TFTP listener, or off for none. Port 69 is privileged; see RESCRIPTUM_USER
RESCRIPTUM_TFTP_PORT_RANGEunsetThe ports transfers answer from, as first-last. A TFTP transfer leaves port 69 immediately — the server replies from a fresh port and the client acknowledges to that — so a firewall allowing only 69 drops the acknowledgement and the machine looks like it lost interest. Pin the range so it can be opened. Unset, the kernel picks
RESCRIPTUM_TFTP_BLKSIZE1468The largest TFTP block to agree to. 1468 fills a 1500-byte path exactly — 1468 payload, 4 TFTP, 8 UDP, 20 IP — so a VLAN tag or a tunnel makes the frame too big and a PXE ROM usually just stops. Lower it (1400, or 512) when a boot stalls at the first block
RESCRIPTUM_BOOT_TIMEOUT_SECS15Seconds before the menu falls through to local boot
RESCRIPTUM_BOOT_UNCLAIMEDmenuWhat a machine no answer claims gets. local hands it back to its firmware instead, which inverts what an answer file means: present is install this one rather than leave this one alone
RESCRIPTUM_INSTALLED_TOKENunsetProxmox’s [post-installation-webhook] token. Set it and POST /installed exists, dropping a machine’s install claim when it reports success. Unset, there is no endpoint
RESCRIPTUM_BOOT_LOGObuilt-inA PNG to show behind the menu
RESCRIPTUM_BOOT_TITLEbuilt-inThe menu’s title bar
RESCRIPTUM_USER / _GROUPunsetDrop to these after binding. The other order fails on deployment

/srv is where the filesystem hierarchy standard puts data served by the system, which is what an answers directory is. Both defaults live there so that a bare rescriptum does something plausible on any Linux host. Nothing creates the directory for you, and the server says so at startup if it is missing.

Logging

One line per event, on stderr by default. Two knobs, because the two questions are different.

WhatRESCRIPTUM_LOG:

ValueKeeps
all (default)every request, plus startup, warnings and errors
problemsstartup, warnings, errors, and only the requests that did not succeed
off / nonenothing at all

A successful answer is one line, and at thirteen thousand requests a second that is the only thing here with any volume. problems is what you want once a rollout is routine and the disk is not. Everything else is low-volume and diagnostic, so it survives both.

A request that never reached a status at all — a connection that timed out mid-body — counts as a problem. An unrecognised value falls back to all with a warning: a typo must not be the reason nobody can see why a rollout failed. The level is named in the startup line (log=problems), so an empty log explains itself.

WhereRESCRIPTUM_LOG_FILE:

ValueGoes to
unset, or stderrstderr, which is what a supervisor reads
stdoutstdout
any other valuethat file, appended to; parent directories are created

A file that cannot be opened is a startup error, not a fall back to stderr — that would be a silent surprise discovered much later. A write that fails once the server is running is dropped instead: a provisioning server that died because its log disk filled up would fail every install in flight in order to report that it could not report something.

Rotation is yours. Under systemd there is nothing to do, since the log goes to the journal; with a file, point logrotate at it with copytruncate.

The TOML file

RESCRIPTUM_CONFIG names a file in TOML that sets the same variables in a shape meant to be read. Reach for it when a person edits the file by hand — on a NAS, in File Station or over SMB — which is exactly where RESCRIPTUM_ANSWERS_DIR=… on every line reads poorly and where the word “environment” sends people looking for a shell that is not there.

# /etc/rescriptum.toml   (chmod 600, owned by root)
answers_dir = "/srv/answers"
listen_addr = "0.0.0.0:8000"
log         = "problems"          # all | problems | off

[store]
kind    = "sqlite"
db_path = "/srv/answers.db"

[server]
workers         = 2
max_connections = 2048
timeout_secs    = 10

[admin]
addr  = "127.0.0.1:8001"
token = "…"

[answer]
token       = "…"
capture_dir = "/var/lib/rescriptum/captures"
$ RESCRIPTUM_CONFIG=/etc/rescriptum.toml rescriptum
2026-08-29T12:42:02Z - reading configuration defaults from /etc/rescriptum.toml (8 set)

Every rule the env file has, this one has too: never discovered, only named (there is no ./rescriptum.toml); the real environment wins; and a file that was asked for and cannot be read is a startup error, never a warning.

Put it outside the answers directory. Every servable .toml at the top of that directory is an answer document, and this format shares the extension — a configuration file dropped in there is reported by check as a misplaced answer, and migrate offers to move it. /etc is the obvious home; on a packaged install the package chooses one.

The names

The prefix goes away and tables do the grouping. Nothing else changes: each line below is the variable of the same name, and rescriptum config prints both spellings.

In the fileVariable
answers_dirRESCRIPTUM_ANSWERS_DIR
listen_addrRESCRIPTUM_LISTEN_ADDR
log, log_fileRESCRIPTUM_LOG, RESCRIPTUM_LOG_FILE
public_hostRESCRIPTUM_PUBLIC_HOST
user, groupRESCRIPTUM_USER, RESCRIPTUM_GROUP
store.kind, store.db_pathRESCRIPTUM_STORE, RESCRIPTUM_DB_PATH
server.workers, server.max_connections, server.timeout_secsRESCRIPTUM_WORKERS, RESCRIPTUM_MAX_CONNECTIONS, RESCRIPTUM_TIMEOUT_SECS
admin.addr, admin.tokenRESCRIPTUM_ADMIN_ADDR, RESCRIPTUM_ADMIN_TOKEN
answer.token, answer.capture_dirRESCRIPTUM_ANSWER_TOKEN, RESCRIPTUM_CAPTURE_DIR
media.dir, media.addr, media.timeout_secs, media.max_connectionsthe RESCRIPTUM_MEDIA_* four
boot.dir, boot.allow, boot.unclaimed, boot.timeout_secs, boot.logo, boot.titlethe RESCRIPTUM_BOOT_* six
tftp.addr, tftp.port_range, tftp.blksizethe RESCRIPTUM_TFTP_* three
installed.tokenRESCRIPTUM_INSTALLED_TOKEN

The format

Any TOML scalara number may be written as a number (workers = 2) or as a string; both reach the server as the same setting
A # commentanywhere, including at the end of a line — unlike the env file, which has no escapes and so cannot have inline comments
A value with a #, a quote or a spacefine, quoted the way TOML quotes things, and read back unchanged
""unset — the same rule as an exported-but-empty variable, which is what lets config unset empty a line instead of deleting the paragraph that documents it
The same key twicerefused by TOML itself, so the file does not load
A key this program does not reada warning naming it, so admin.tokenn is caught rather than ignored
A list or a table where a value belongsa startup error: unlike a misspelling it was aimed at a real setting, and serving the default while the file says otherwise would be silent
A file others can reada warning with its mode, because it may hold admin.token

Warnings name keys and paths, never values.

Both files at once

Naming both is a transition rather than a steady state, so nothing is refused and the order is stated at startup: the environment beats the TOML file, which beats the env file. rescriptum config shows which of the three put every value in force, and config set writes to the TOML file — the one the server reads first, so that a write cannot be a change that silently does nothing.

The env file

RESCRIPTUM_ENV_FILE names a file of the same variables. It exists for deployments with nowhere good to put a token — chiefly Synology DSM 7, which has no systemd. Under systemd, EnvironmentFile= already does this and you do not need it.

# /etc/rescriptum.env   (chmod 600, owned by root)
RESCRIPTUM_STORE=sqlite
RESCRIPTUM_DB_PATH=/srv/answers.db
RESCRIPTUM_ADMIN_ADDR=127.0.0.1:8001
RESCRIPTUM_ADMIN_TOKEN=
$ RESCRIPTUM_ENV_FILE=/etc/rescriptum.env rescriptum
2026-08-24T12:42:02Z - reading configuration defaults from /etc/rescriptum.env (4 set)

It is never discovered, only named. There is no ./.env. This binary runs as root: if it picked a file up from whatever directory it happened to be launched in, anyone who could write there would own RESCRIPTUM_ADMIN_TOKEN — and with it the root password of every machine installed afterwards.

The real environment wins. The file supplies defaults, so something exported deliberately at launch is never silently overridden. An exported-but-empty variable counts as unset, so the file still applies.

A file that was asked for and cannot be read is a startup error, not a warning. That is the whole point: the failure it replaces is a server coming up on its defaults — wrong answers directory, no admin token — without a word in the log.

The format

KEY=value, one per lineleading export is accepted, so the same file can also be sourced
# at the start of a linea comment
# anywhere elsepart of the value. There are no inline comments: truncating a token at a # it legitimately contains would be silent, and a comment landing in a value is loud
"quoted" or 'quoted'the quotes are stripped and inner whitespace is kept; unquoted values are trimmed
$HOME, ${x}not expanded. This is not a shell — no substitution, no continuation lines
the same key twicea startup error, rather than a guess about which was meant
a key this program does not reada warning naming the key — so RESCRIPTUM_ADMIN_TOKENN is caught rather than ignored
a file others can reada warning with its mode, because it may hold the admin token

Warnings name keys and paths, never values.

Reading and editing it

rescriptum config prints every variable, its value, and which of the files and the environment put it there — the distinction that matters, because the files supply defaults and the real environment wins. config set edits the file the way you would want it edited, in either format: comments kept, a commented-out setting uncommented in place rather than duplicated (in the env file) or the value replaced where it stands (in TOML), and a change that would leave a server unable to start refused before anything is written. It is documented in the command line reference, and it is what the DSM application drives underneath.

Invalid values

CaseWhat happens
Exported but empty (RESCRIPTUM_LISTEN_ADDR=)treated as unset — an empty value is a mistake, not an instruction
Whitespace-onlysame, and values are trimmed
A zero or unparseable numberfalls back to the default, rather than starting a server that accepts connections and never answers
RESCRIPTUM_STORE set to anything elsea warning, and files is used
RESCRIPTUM_ENV_FILE or RESCRIPTUM_CONFIG naming a missing, unreadable or malformed filea startup error
A TOML setting given a list or a tablea startup error, unlike a misspelled key, which warns
RESCRIPTUM_STORE=sqlite on a binary built without the featurea startup error

Startup errors

These stop the server rather than warning, because starting anyway would be worse:

ConditionWhy it is fatal
RESCRIPTUM_ADMIN_ADDR set with RESCRIPTUM_STORE not sqlitetwo ways to change the same configuration, racing
RESCRIPTUM_ADMIN_ADDR set with no RESCRIPTUM_ADMIN_TOKENan open API that rewrites root credentials
RESCRIPTUM_ADMIN_TOKEN under 16 charactersshort enough to guess
The listen address cannot be boundnothing to do
The store cannot be openednothing to serve
RESCRIPTUM_MEDIA_ADDR set with no RESCRIPTUM_MEDIA_DIRa listener with nothing to serve
RESCRIPTUM_MEDIA_ADDR equal to the answer or admin addressthe second bind loses, and which one depends on start order
RESCRIPTUM_PUBLIC_HOST carrying a scheme, a port or a pathit is written into URLs for two listeners; one port in the value pins every generated script to one of them
RESCRIPTUM_TFTP_ADDR set with no RESCRIPTUM_BOOT_DIRa listener with no loaders to hand out
The boot directory cannot be resolvedevery path check compares against it
RESCRIPTUM_USER names an account that does not existnothing to become

Startup warnings

These are printed and the server carries on:

ConditionLine
Answers directory missingwarning: … does not exist yet — every request will 404 until it does
Answers path exists but is not a directorywarning: … is not a directory — every request will 404 until it is
Answers directory present but unreadablewarning: … cannot be read: … — every request will 404 until that is fixed. The likeliest cause is the server running as a user that is not the directory’s owner
Admin API not on loopbackwarning: the admin API is not bound to loopback — …
RESCRIPTUM_ANSWER_TOKEN under 16 charactersa warning, not an error — refusing to start would leave a fleet unable to install
Any problem in the answer setone warning: line each, the same set check reports
RESCRIPTUM_PUBLIC_HOST unsetThe routing table’s answer, or the sole interface address when there is no default route. Logged either way, as a warning naming the other addresses when there are any. A NAT host still gets it wrong silently
TFTP cannot bindwarning: cannot bind TFTP on … the one listener whose failed bind is not fatal. Port 69 is the only privileged port in the design, so it is the only bind that can fail for something nobody configured; answers are the product, and dying would fail every install in flight to report that a second port could not be opened. boot check exits non-zero and the message names the ways to have the port
Media directory missing or unlistableone warning: media: … line — a fleet must never be unable to install because one image is odd

Compile-time options

FeatureDefaultEffect
sqliteonThe SQLite store and the admin API
bootonThe media catalogue, the ISO reader and the media listener

Measured on ARMv7 (gnueabihf, glibc floor 2.17), all four in one sitting on 2026-08-29. Re-measure rather than quoting these: they moved by about 375 KB when that target changed from musl, and the set they replace here had drifted about 200 KB out of date.

BuildBytes
both (default)2,813,712
sqlite only2,557,592
boot only1,649,048
neither1,392,544

Fixed limits

Not configurable, and deliberately so:

LimitValueWhere
Request body1 MBanswer endpoint — an aberrant Content-Length is refused from the header
Document size256 KBadmin API PUT
Captured requests1000 capturescounted from the directory at startup, so a restart does not start again
Admin failures before a block5 within 60 sblock doubles to a maximum of 900 s
Addresses tracked by the guard4096so the guard cannot be turned into a memory leak
Listing reload backstop1 sforces a re-read even when the directory mtime looks unchanged