Tay has two deployment modes. Elixir applications continue to install the Hex
package and supervise Tay.child_spec/1. Non-Elixir applications may run the
official OCI image and connect a language client via HTTP/JSON or the local
Executor Protocol v1 Unix socket. The standalone runtime is only a small OTP host around the same
Engine, scheduler, storage implementation, and recovery rules as the library.
Python producer/worker -> HTTPS :8080 ─┐
Python local worker -> Unix socket ──┴─> Tay OTP release -> /var/lib/tayConfiguration
The image runs as UID/GID 10001:10001 and accepts:
| Variable | Default | Meaning |
|---|---|---|
TAY_DATA_DIR | /var/lib/tay | Authoritative durable Store root. |
TAY_SOCKET_PATH | /run/tay/tay.sock without HTTP; disabled with HTTP | Executor Protocol v1 UDS path; explicitly set a path to enable both transports, or off to disable UDS. |
TAY_HTTP_PORT | unset | Enables the Bandit HTTP/JSON API for producers and remote workers. |
TAY_HTTP_IP | 127.0.0.1 | Listener IP; non-loopback requires mTLS. |
TAY_HTTP_TLS_{CERTFILE,KEYFILE,CACERTFILE} | unset | Absolute PEM paths for server certificate, private key and client CA. Set all three together. |
TAY_INITIALIZE_IF_MISSING | false | true, TRUE, or 1 permits initialization only when the Store root is genuinely absent. |
TAY_DEFAULT_QUEUE_CONCURRENCY | 10 | Maximum concurrently executing jobs in the standalone default queue (1..65536). Actual execution is also bounded by registered worker capacity. |
TAY_CALLER_TIMEOUT_MS | 5000 | Maximum time an admitted Engine call may wait for its reply. A timeout after submission is reported as unknown_outcome; reconcile with the same job ID. |
TAY_GROUP_COMMIT_INTERVAL_MS | 0 | Durable enqueue group-commit window in milliseconds (0..1000). 0 preserves immediate fsync behavior. Invalid values stop startup. |
TAY_RATE_LIMITS | {} | JSON queue/task execution limits; see operations. Monotonic fixed windows reset on Engine restart. |
TAY_CHECKPOINT_INTERVAL_MS | 60000 | Periodic crash-safe Store-v2 semantic checkpoint interval in milliseconds (0..4294967295). 0 disables it. Checkpoints reconstruct a candidate from sealed on-disk history at a durable frontier while execution continues, then catch up concurrent mutations. They do not copy Engine ETS state. Recovery replays the resulting snapshot plus its tail. |
TAY_MAX_STATE_BYTES | 268435456 | Shared operational memory budget for active jobs, schedules, retained results and admitted payloads, including reserved headroom. It is not a process/container RSS ceiling. Accepts bytes or an integer with KB, MB, or GB, for example 5GB; suffixes use binary multiples. |
TAY_TERMINAL_RETENTION | 24h | Initial retention for terminal jobs. Positive durations use m, h, or d, for example 30m, 1h, 24h, or 7d. Manual compaction may use zero as a one-shot terminal-history purge, after which the persisted retention is 1h. After any successful compaction, its persisted retention takes priority on restart. |
TAY_ENABLE_DASHBOARD | false | Set to true, TRUE, or 1 to serve the bundled dashboard. |
TAY_DASHBOARD_HOST | localhost | Public hostname for dashboard URL and LiveView origin checks. |
TAY_DASHBOARD_PORT | 4000 | Dashboard listener port. |
TAY_DASHBOARD_USERNAME | unset | Optional HTTP Basic Auth username; requires password. |
TAY_DASHBOARD_PASSWORD | unset | Optional HTTP Basic Auth password; requires username. |
TAY_DASHBOARD_SECRET_KEY_BASE | unset | At least 64 random bytes; required when dashboard is enabled. |
TAY_DURABILITY is intentionally not an environment setting. The standalone
runtime pins durability: :sync and checks for a supported filesystem type before writing;
an unnoticed override to :write would weaken the production durability
contract. This type check does not certify the storage device's power-loss
behavior; operators still need to validate their deployment. Embedded Elixir applications can configure their Engines explicitly,
subject to Tay's production validation.
An unset or empty TAY_SOCKET_PATH uses automatic transport selection.
Malformed explicit values fail startup. The socket must be absolute, at most
100 bytes, and outside the data directory. The socket is created with mode
0660 so a worker can use a shared group where the deployment platform
supports it. Standalone uses the same path resolver as the embedded Engine:
outside the container, an absent /run/tay falls back to XDG, TMPDIR, or the
per-UID /tmp directory. The Python SDK discovers /run/tay/tay.sock from the mounted
/run/tay directory without a TAY_SOCKET_PATH setting. Keep the shared socket
volume mounted into both containers (read-only in the worker); network sharing
alone does not make Unix socket files visible across containers.
Concurrent persistent commands are flushed when the global batch reaches
max_events or its interval_ms expires, whichever happens first. They are
written in arrival order and acknowledged together only after their batch fsync
succeeds. Batching does not change recovery, capacity accounting, or failure
semantics; a write/fsync error fails the generation without returning success
for that batch. Embedded Engines use global
journal: [batch: [max_events: n, interval_ms: milliseconds]] configuration.
The legacy :group_commit_interval_ms Engine option remains accepted.
durability: :write remains immediate.
TAY_MAX_STATE_BYTES uses binary multipliers: KB is 1,024 bytes, MB is
1,024 KB, and GB is 1,024 MB. For example, 5GB is 5_368_709_120 bytes.
A value without a suffix is interpreted as bytes for backward compatibility.
Active-job and terminal-history counts are informational and have no configured
maximum. TAY_MAX_JOBS and TAY_MAX_TERMINAL_JOBS are legacy variables and are
ignored. Active jobs, schedules, retained results and admitted payloads share
one TAY_MAX_STATE_BYTES budget, with conservative accounting for their BEAM
representation and reserved headroom. The former TAY_MAX_STATE_NODES variable is ignored.
Terminal history is removed only by time-based compaction.
Python HTTP producers and workers need only the HTTP(S) address. Set
TAY_HTTP_PORT to run without a Unix socket; an explicit TAY_SOCKET_PATH
enables both transports. TAY_SOCKET_PATH=off is also supported. For an HTTP-only Compose deployment use docker compose up tay;
the example's worker service demonstrates the optional local socket mode.
The example still declares an unused socket volume in HTTP-only mode; neither
the Engine nor Python HTTP clients require a socket in that configuration.
The optional HTTP API runs inside the same Engine. Plaintext is permitted only
on loopback; non-loopback binds require the three TLS files and verify client
certificates. Mount certificates read-only, ensure UID 10001 can read them,
and match the server certificate SAN to the client's DNS name or IP. Legacy
TAY_GRPC_* settings fail startup rather than silently disabling the old API.
Tay deliberately does not interpret a pre-existing empty directory as a
missing Store. Docker creates a named-volume mount root before the process
starts, so mount the volume at /var/lib/tay and set
TAY_DATA_DIR=/var/lib/tay/store, as in
compose.yml.
The image creates both directory targets with ownership 10001:10001; an empty
Docker named volume inherits that ownership and does not need a root init
container. Bind mounts and volume drivers that preserve different ownership
must be prepared for UID/GID 10001 by the operator.
The image supports a read-only root filesystem. Its only writable locations
are the durable data volume, the optional ephemeral socket volume, and /tmp. It drops
no privileges at startup and never runs as root.
Run with Compose
From the repository root:
TAY_IMAGE=tay:local docker compose up --build
This starts Tay only. To include the example local socket worker, use
TAY_IMAGE=tay:local docker compose --profile local-worker up --build.
Compose lists settings needed for this deployment example; the table above
lists all supported standalone environment settings.
Set TAY_ENABLE_DASHBOARD=true in the same Compose deployment to enable the
dashboard. The HTTP API is opt-in; to expose it outside the container, set
TAY_HTTP_PORT=8080, TAY_HTTP_IP=0.0.0.0, and
TAY_HTTP_HOST_BIND=0.0.0.0; mount the three TLS files read-only and set their
TAY_HTTP_TLS_* paths. Without mTLS the non-loopback bind is rejected. Start
only the tay Compose service for HTTP-only use. To retain the local socket
worker too, explicitly set TAY_SOCKET_PATH=/run/tay/tay.sock on the Tay
service. Compose environment overrides must be added to the service environment.
The worker example installs the stdlib-only Python client and advertises an
example.echo.v1 task. Production workers should pin a tay-client version
compatible with the image's protocol version.
For a worker on another host, run Tay without a socket and mount a directory
containing the server certificate, its key, and the CA that signs client
certificates. For example, after creating the taydata volume:
docker run -d --name tay \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m,uid=10001,gid=10001,mode=0700 \
--cap-drop ALL --security-opt no-new-privileges \
-p 8080:8080 \
-e TAY_DATA_DIR=/var/lib/tay/store \
-e TAY_INITIALIZE_IF_MISSING=true \
-e TAY_SOCKET_PATH=off \
-e TAY_HTTP_PORT=8080 -e TAY_HTTP_IP=0.0.0.0 \
-e TAY_HTTP_TLS_CERTFILE=/etc/tay/tls/server.pem \
-e TAY_HTTP_TLS_KEYFILE=/etc/tay/tls/server.key \
-e TAY_HTTP_TLS_CACERTFILE=/etc/tay/tls/client-ca.pem \
--mount type=volume,src=taydata,dst=/var/lib/tay \
--mount type=bind,src=/absolute/path/to/tls,dst=/etc/tay/tls,readonly \
ghcr.io/andriisydorenko1904/tay:1.8.1
Create the volume with docker volume create taydata. Replace the TLS source
path, ensure UID 10001 can read the mounted files, and provision client
certificates separately for workers. On later starts, set
TAY_INITIALIZE_IF_MISSING=false. Restrict the published port to the intended
worker network. The Python connection example is in the
Python client guide.
The health check succeeds when the Engine reports ready or is performing a
normal online checkpoint in compacting/preparing or compacting/switching,
and the selected transport is available (HTTP if the socket is disabled,
otherwise the UDS).
A live BEAM process alone is not healthy. The image grants startup a 15-minute
health-check grace period, matching the default bounded recovery deadline;
steady-state failures still use the ordinary 12 attempts at five-second
intervals.
Persistence and operations
/var/lib/tay is persistent state. Back it up using Tay's documented storage
procedure; copying a live writable directory is not a supported backup.
Deleting the data volume deletes durable job history. /run/tay is runtime IPC
state only and may be recreated on every deployment.
Tay remains single-node and single-writer. Never attach one data volume to two Tay containers. It provides at-least-once execution, performs full recovery on restart, and fails closed on invalid, partial, corrupt, or unsupported storage. It does not automatically repair or truncate evidence. Use a local Linux filesystem whose durability properties have been validated for the workload.
The standalone image is not distributed Tay and does not run user job code; external worker processes do that over HTTP/JSON or the local UDS protocol.
Optional dashboard
The same tay OCI image bundles the dashboard from the tay package and a minimal
Phoenix endpoint. Set TAY_ENABLE_DASHBOARD=true to expose both
/run/tay/tay.sock and the web UI at /tay. Application-level authentication
is optional. No second container or image is required.
TAY_IMAGE=tay:local TAY_ENABLE_DASHBOARD=true docker compose up --build
The headless and dashboard-enabled modes use the same data and transport contracts. See the dashboard container guide for its network boundary, optional credentials, HTTP configuration, and Compose example.
The CI container smoke test runs Tay and the Python worker without
TAY_SOCKET_PATH, with a read-only socket mount in the worker. It replaces the
Tay container while keeping the worker running, checks a recovered result, and
executes a new job through the reconnected worker. The HTTP-only smoke test
sets only TAY_HTTP_PORT and verifies that no Unix socket is created.
Read-only readiness and client diagnostics
GET /health returns a bounded { "type": "health", "state": "ready" }
response (or compacting during the accepted online preparation/switch phases).
An unavailable/non-ready Engine returns HTTP 503. The route uses the listener's
existing loopback/mTLS policy and contains no jobs or private Engine state.
Use tay doctor --http-url ORIGIN --json or tay doctor --socket-path PATH
from the Python SDK to diagnose the configured listener without creating jobs.
The included examples/standalone/worker.py selects TayHTTPWorker when
TAY_HTTP_URL is set, and a socket worker otherwise. Both use the same image
entry point and task declarations; the container smoke test exercises HTTP CLI
execution, result waiting, diagnostics and clean SIGTERM shutdown.