Skip to main content

Configuration

arcave is configured by .arcave/config.yaml under the project root, written by arcave init.

It is the one file arcave reads through the operating system rather than through its own store — it is what says where that store points, so it cannot live inside it.

A config with every section present
project:
name: myapp
common:
concurrency: 5
docker:
host: unix:///var/run/docker.sock
compose:
wait_timeout: 2m0s
stop_timeout: 10s
progress: log
storage:
type: directory
directory:
path: .arcave/storage
snapshot:
backend: cdc
writable_layers:
backend: dedupcopy
capture:
writable_layers: true
images: true
volumes:
named: true
bind: true
anonymous: true
rollback:
leftover_volumes: overwrite
backends:
cdc:
restore_mode: incremental
overwrite: always
image: restic/restic:0.19.1
server_address: 127.0.0.1:0
server_scheme: http
container_host: host.docker.internal
network_mode: auto
log:
level: info
format: text

An unknown key is an error, not a value quietly ignored. A typo fails the command rather than falling back to a default you did not choose:

resolve config: unmarshal config: invalid config: decoding failed due to the following error(s):

'project' has invalid keys: nam

Every config error is reported after that same resolve config: unmarshal config: invalid config: prefix, which the examples below leave out. The docker and log errors are not config errors and are quoted whole.

arcave never rewrites this file on its own; that would clobber a path you set deliberately. arcave init --override is the one command that replaces it.

Precedence​

Four sources, highest first:

1. A changed command-line flag
2. An ARCAVE_<KEY> environment variable
3. .arcave/config.yaml
4. The flag's default

Only bound flags feed the config. --project-name, --session, --config, --override, --dry-run, init --name and savepoint create --name are unbound on purpose: they steer one run rather than describing the project. init binds none of its flags: it writes a template, and reads neither the environment nor the existing file.

Which keys an environment variable can reach is narrower than it looks — Environment variables owns that.

project​

The storage tenant every store root is scoped by.

KeyTypeDefaultDescription
project.namestringthe project directory's name, normalisedThe tenant segment of the store path.

Validated against an allowlist: it starts with [a-z0-9] and continues with [a-z0-9_-]. The directory name is normalised into that charset — MyApp becomes myapp — and a name that normalises to nothing is reported rather than guessed at.

The compose project is not here. It is named per invocation with -p/--project-name, because one project directory may hold several compose projects and a persisted name would pin arcave to one of them.

common​

Settings every workflow shares.

KeyTypeDefaultDescription
common.concurrencyint5How many volumes, writable layers or images a capture or a restore works on at once.
common.concurrency must be at least 1, got -1

docker​

How arcave reaches the daemon. Honoured by both halves of the Docker client, so the SDK and the compose runner can never silently target different daemons.

KeyTypeDefaultDescription
docker.hoststring""Daemon endpoint, e.g. unix:///var/run/docker.sock. Empty means read the environment.
docker.api_versionstring""Pin the API version.
docker.contextstring""The Docker context to use.
docker.cert_pathstring""A directory holding ca.pem, cert.pem and key.pem — the DOCKER_CERT_PATH convention.
docker.ca_filestring""Override the CA file on its own.
docker.cert_filestring""Override the certificate file on its own.
docker.key_filestring""Override the key file on its own.
docker.tls_verifyboolfalseVerify the daemon's TLS certificate.

The three *_file keys exist for certificates that do not follow the cert_path naming convention.

docker.host and docker.context both name a daemon, so set at most one; a client certificate needs both halves. These are checked when a command connects, not when the config is read:

initialise docker client: docker.host and docker.context are mutually exclusive: "remote" already names a daemon, so remove one of the two
initialise docker client: docker.cert_file is set but docker.key_file is missing: a client certificate needs both, or set docker.cert_path to supply the pair from one directory

compose​

How patient arcave is with the project, and how loudly compose reports its own progress.

KeyFlagDefaultDescription
compose.wait_timeout--wait-timeout2m0sHow long to wait for services to become healthy after a start.
compose.stop_timeout--stop-timeout10sThe least time a container gets to exit on its own before it is killed; a service's stop_grace_period raises it.
compose.progress--progresslogWhere compose's own progress goes: log, tty or quiet.

wait_timeout must be positive. "Wait forever" is not on offer: it is exactly the failure this bound exists to turn into an error, since a crash-looping container with restart: always would otherwise hold a savepoint open indefinitely.

compose.wait_timeout must be a positive duration, got -5s

stop_timeout matters more here than it does to docker compose stop. arcave captures the volumes the moment the containers are down, so a database killed mid-shutdown is captured mid-shutdown — and the command still reports success, with a warning naming the killed container. It is a floor: every stop waits up to the longest stop_grace_period the project's services declare, when that is longer. Zero means "unset" and the default applies, so an immediate kill is spelled 1s, not 0. A negative value is refused:

compose.stop_timeout must not be negative, got -1s

progress selects a channel, not a format. A log record is rendered by log.format like any other.

ValueEffect
logEach compose event becomes an arcave log record — one channel, one format.
ttyCompose's own redrawing renderer, at the cost of a second format on stderr.
quietNothing but arcave's own log lines.

All three keep compose off stdout, which carries the JSON result and nothing else.

unsupported compose.progress "loud" (supported: log, tty, quiet)

storage​

Where savepoint data is written. A tagged union: type names the arm, that arm must be present, and any other arm is dropped. Each arm is one backend of github.com/greenmaskio/storages.

KeyTypeDefaultDescription
storage.typestringdirectorydirectory, s3, azure or ssh.

storage.directory​

KeyTypeDefaultDescription
storage.directory.pathstring.arcave/storageA local filesystem path. Relative resolves against the project root; absolute is taken verbatim.

A relative path materialises on first use. An absolute path must already exist — that asymmetry keeps a typo from quietly growing a fresh tree somewhere unexpected.

storage.s3​

An S3-compatible bucket: AWS, MinIO, and the like.

KeyTypeDefaultDescription
storage.s3.bucketstring(required)The bucket. It must exist.
storage.s3.prefixstring""A key prefix inside the bucket.
storage.s3.regionstring""The bucket's region.
storage.s3.endpointstring""A non-AWS endpoint, http://minio:9000. Must carry no user info or query string.
storage.s3.force_path_stylebooltruePath-style addressing, which MinIO needs.
storage.s3.access_key_idstring""With secret_access_key, a static credential. Both empty: the AWS default chain (environment, profile, instance role).
storage.s3.secret_access_keystring""Secret.
storage.s3.session_tokenstring""Secret.
storage.s3.role_arn, session_namestring""A role to assume.
storage.s3.storage_classstringSTANDARDThe class every object is written with.
storage.s3.sse, kms_key_arn, bucket_key_enabled""Server-side encryption: AES256, aws:kms or aws:kms:dsse, and the KMS key.
storage.s3.disable_ssl, no_verify_ssl, cert_fileTLS: plain HTTP, no certificate check, a CA bundle.
storage.s3.max_retries, max_part_size, concurrencyint3, 50 MiB, 0Request tuning.

storage.azure​

An Azure Blob Storage container.

KeyTypeDefaultDescription
storage.azure.containerstring(required)The container. It must exist.
storage.azure.storage_accountstring(required)The account.
storage.azure.prefixstring""A blob prefix inside the container.
storage.azure.access_keystring""Secret. The shared key.
storage.azure.sas_tokenstring""Secret. Used when there is no access_key; with neither, DefaultAzureCredential.
storage.azure.endpointstring""The service URL, path-style: http://azurite:10000/devstoreaccount1. Must carry no user info or query string — a SAS token goes in sas_token.
storage.azure.endpoint_suffixstring""A sovereign cloud's endpoint suffix.
storage.azure.environment_namestringAzurePublicCloudA sovereign cloud by name.
storage.azure.max_retriesint3Retries after a failed try; negative for none.

storage.ssh​

A directory on an SFTP server.

KeyTypeDefaultDescription
storage.ssh.hoststring(required)The server.
storage.ssh.portint22
storage.ssh.userstring(required)
storage.ssh.passwordstring""Secret. One of password or private_key_path is required.
storage.ssh.private_key_pathstring""A private key file.
storage.ssh.private_key_passphrasestring""Secret. Decrypts an encrypted key.
storage.ssh.prefixstring""The remote directory; relative resolves against the user's home.
storage.ssh.connect_timeoutduration30sBounds the TCP dial and the SSH handshake.

The server's host key is not verified — arcave warns ssh storage does not verify the server's host key — so use ssh on a network you trust.

storage.ssh needs password or private_key_path
storage.ssh.private_key_passphrase needs private_key_path
storage.ssh.connect_timeout must not be negative, got -1s

Layout and reachability​

The path or prefix you configure is not the path arcave writes to. Every store is rooted under a static arcave segment, and the full layout is in Storage layout:

<path or prefix>/arcave/projects/<project>/sessions/<session>/compose/<compose-project>/

That segment is not configurable, and it is what bounds every write and every delete arcave makes: point the path at /etc and arcave operates in /etc/arcave. Every segment below it is checked against an allowlist, because a list of what a path segment must not be is never provably complete.

Every command that opens the store reaches it first — a HEAD on the bucket, a property read on the container, an SFTP session — so a wrong endpoint or a refused credential fails before anything is stopped.

storage.type is "s3" but no storage.s3 section is present
storage.s3.bucket is required
storage.s3.endpoint must not carry user info; set the credential fields instead
storage.azure.endpoint must not carry a query string; put a SAS token in storage.azure.sas_token
unsupported storage.type "gcs" (supported: directory, s3, azure, ssh)

Credentials​

Keep a secret out of the file by naming its environment variable — see Interpolation. arcave init never writes one: it writes a template of the defaults and --name, with the directory storage, so a credential reaches the file only when you put it there.

Interpolation​

Any string value may name an environment variable, expanded when the config is read:

FormExpands to
${VAR}, $VARThe variable's value. Unset is an error, never an empty string.
${VAR:offset}, ${VAR:offset:length}A substring of the value. Unset is an error.
${VAR:-default}The value, or default when unset or empty. ${VAR:-} says "empty" explicitly.
${VAR-default}The value, or default when unset.
${VAR?message}The value; unset fails with message.
$$A literal $.
storage:
type: s3
s3:
bucket: savepoints
access_key_id: AKIAEXAMPLE
secret_access_key: ${AWS_SECRET_ACCESS_KEY}

An unset variable fails the command:

environment variable AWS_SECRET_ACCESS_KEY is not set (write ${AWS_SECRET_ACCESS_KEY:-} for an empty default, or $$ for a literal $)

A value with a $ that is not a reference — a password like pa$word — is written pa$$word. That holds wherever the value comes from: an ARCAVE_* variable or a flag is interpolated the same way as the file, so ARCAVE_STORAGE_SSH_PASSWORD='pa$$word'.

snapshot​

Which backend captures a new savepoint.

KeyTypeDefaultDescription
snapshot.backendstringfullcopyfullcopy or cdc.

A rollback reads the backend out of the record it is applying, so changing this never orphans an existing savepoint: you can switch to cdc today and still roll back to last week's fullcopy savepoints.

fullcopycdc
How it captures a volumeOne data.tar per volume per savepoint, streamed through the daemonContent-defined chunking into a restic repository that outlives any one savepoint
What a second savepoint costsFull volume size, againOnly the chunks that changed
What it needsNothing beyond DockerPulls and runs restic/restic containers, which must reach a loopback port on your host

This key covers volumes only; writable layers are writable_layers. A label can pick another backend for one volume — see Capture labels.

unsupported snapshot.backend "restic" (supported: fullcopy, cdc)

writable_layers​

How a new savepoint stores each container's writable layer.

KeyTypeDefaultDescription
writable_layers.backendstringdedupcopydedupcopy or fullcopy.

dedupcopy splits each committed layer into the blob store every savepoint of the compose project shares, so a base layer every savepoint carries is written once — see Storage layout. fullcopy stores each layer whole, as one image.tar. The service label arcave.writable-layer.backend overrides it for one service. The images services run on are always stored deduplicated, whatever this key says. Like snapshot.backend, it applies to new savepoints only: a rollback reads each layer back the way its record says it was stored.

unsupported writable_layers.backend "zstd" (supported: dedupcopy, fullcopy)

capture​

Which kinds of unit a savepoint captures when no capture label says otherwise. Everything is on by default.

KeyTypeDefaultDescription
capture.writable_layersbooltrueEvery container's writable layer.
capture.imagesbooltrueThe image each service's image: names.
capture.volumes.namedbooltrueNamed volumes.
capture.volumes.bindbooltrueBind mounts.
capture.volumes.anonymousbooltrueAnonymous volumes.

These are the defaults a label overrides: capture.volumes.bind: false with arcave.mounts.include: /app/data on one service captures that one bind and no other. They steer a running compose project without recreating it, which a label cannot. What a savepoint left out is listed in its not_covered.

rollback​

KeyTypeDefaultDescription
rollback.leftover_volumesstringoverwriteWhat a rollback does when the compose project has no containers but still owns named volumes the savepoint restores into: overwrite them, with a warning naming each, or refuse.

It matters only for a compose project that is not running — never started, or taken down with docker compose down, which leaves named volumes behind. A running project is always overwritten; that is what a rollback is for. refuse fails before anything is created:

compose project is not running but its volumes survive: myapp_pgdata
unsupported rollback.leftover_volumes "keep" (supported: overwrite, refuse)

backends​

One optional subsection per backend. A project that never uses a backend does not configure it, and an absent section is not an error — arcave resolves the defaults either way, because a rollback restores through the backend its record names.

backends.cdc​

KeyTypeDefaultDescription
restore_modestringincrementalincremental writes over the live data, sending only what differs and deleting what the savepoint does not carry. wipe clears the mount and writes it back whole.
overwritestringalwaysGoverns an incremental restore only. always rewrites every file the savepoint carries; if-changed compares content and rewrites only what differs.
imagestringrestic/restic:0.19.1The restic image every one-shot container runs. --delete/--overwrite need restic 0.17 or newer.
server_addressstring127.0.0.1:0What arcave's restic server binds. Must be host:port.
server_schemestringhttpThe scheme the container fetches the repository over: http, or https for a TLS proxy in front of it.
container_hoststringhost.docker.internalThat server's hostname as the container sees it, on the bridge path. Ignored under host networking.
network_modestringautoHow a restic container reaches the server: auto, host or bridge.

With the defaults, a restore writes over the live data and rewrites every file the savepoint carries — overwrite: if-changed is what turns that into a comparison.

Why port 0. The kernel picks the port. A fixed one would collide between two arcave processes — a CLI run and an arcave mcp server, or two projects — and would be a predictable address on your loopback for anything else on the machine.

What network_mode decides. A restic container has to reach a server on your host loopback, and how it does that differs per platform.

ValueBehaviour
autoProbes the daemon while the command wires itself up: a native Linux daemon gets host networking, anything else the bridge and container_host. The default, because one config then runs everywhere.
hostJoins the containers to the host's network namespace, so the loopback bind is reachable as 127.0.0.1. Linux daemons only.
bridgeKeeps the default network and reaches the host by container_host. The way out for a remote or rootless daemon, which auto reads as local.

The server itself speaks plain HTTP. server_scheme: https is for putting a TLS proxy in front of it, not for making arcave serve TLS.

On Docker Desktop for macOS the cdc backend cannot capture a host bind mount — see Limitations.

unsupported backends.cdc.restore_mode "partial" (supported: wipe, incremental)
unsupported backends.cdc.overwrite "never" (supported: if-changed, always)
backends.cdc.server_address "127.0.0.1" is not a host:port address: …
unsupported backends.cdc.network_mode "none" (supported: auto, host, bridge)

log​

KeyFlagDefaultDescription
log.level--log-levelinfodebug, info or warn.
log.format--log-formattexttext or json.

Both are checked when a command starts, and an unknown value fails it rather than silencing the log:

set log level: get logger: log level trace: unknown log level
set log level: get logger: log format yaml: unknown log format

Logs go to stderr; results go to stdout as JSON. --log-format json makes the whole stderr stream machine-readable, which is what a CI job or an agent wants.

Next step​

Environment variables — which of these keys ARCAVE_<KEY> can actually reach.

Tools and expertise for reproducible, production-like environments.
Copyright © 2026 Greenmask, Inc.