Configuration
Railhook is configured through environment variables in the .env file next to docker-compose.yml. A variable you leave out keeps the default built into the Compose file. After editing .env, run ./railhook up -d. Compose recreates only the services whose settings changed.
Settings you must decide
Section titled “Settings you must decide”Most defaults are fine. These are development defaults that become wrong once the instance is reachable.
| Variable | Why it matters |
|---|---|
APP_ENV |
production makes the API refuse to start on unsafe configuration instead of running with it |
WEBHOOK_ENCRYPTION_KEY, WEBHOOK_ENCRYPTION_SALT |
Encrypt every endpoint secret in the database. Back them up. Never change the salt once data exists |
JWT_SECRET |
Signs sign-in sessions |
POSTGRES_PASSWORD, DB_PASSWORD |
The first creates the database user and the second is what the API and worker connect with. They must be equal |
REDIS_PASSWORD |
Protects the cache |
APP_BASE_URL |
The URL people type. Verification, invite and reset links are built from it |
CORS_ALLOWED_ORIGINS |
Must include the origin the dashboard is served from, or sign-in fails with “Invalid CORS request” |
TUNNEL_INGRESS_BASE_URL |
The public base of source ingress URLs and CLI tunnel URLs. Unset, it follows APP_BASE_URL |
EMAIL_ENABLED, EMAIL_FROM, SMTP_* |
With email off, accounts are created already verified and invites cannot be sent |
RAILHOOK_PORT, RAILHOOK_BIND |
Where the one published port listens |
WEBHOOK_TRUSTED_PROXIES |
The proxies whose X-Forwarded-For is believed. Empty trusts nothing |
WEBHOOK_ALLOW_PRIVATE_IPS, WEBHOOK_ALLOWED_HOSTS |
Keep private addresses blocked. List a named internal host to allow just that one |
DB_HOST and COMPOSE_PROFILES |
To use a managed PostgreSQL, set the DB_* variables and leave embedded-db out of the profiles |
PLATFORM_ADMIN_TOKEN |
Unlocks the operator endpoints. Empty keeps them unreachable |
API_REPLICAS |
2 keeps the API answering while ./railhook upgrade restarts it, at the cost of roughly another 600 MB. With 1, expect about 30 s of 502 per upgrade |
Onboarding email
Section titled “Onboarding email”ONBOARDING_EMAILS_ENABLED sends two mails to new accounts: a welcome once the address is verified, and one nudge two days later if the account’s organization has still sent and received no events. Each goes at most once per account, replies go to EMAIL_SUPPORT_ADDRESS, and nothing is sent unless EMAIL_ENABLED is on as well. Accounts that existed before the upgrade that added the setting never receive either.
Leave it off on your own instance. Both mails are written in the first person by Railhook’s author for Railhook Cloud, and your users did not sign up with him.
The blog
Section titled “The blog”BLOG_ENABLED serves /blog, its posts and its feed. The image carries them only because railhook.io runs the same image as your instance: they are Railhook’s own articles, not your site’s.
It is off by default, and only railhook.io turns it on. Off, /blog and everything under it answer 404, the header and footer carry no Blog link, and /sitemap.xml lists no posts. The UI container reads it at startup, so changing it takes docker compose up -d ui, not a rebuild. On Kubernetes the setting is ui.publicBlog.
All variables
Section titled “All variables”Database location
| Variable | Default | Description |
|---|---|---|
DB_MODE | embedded | embedded | external. Read by `make backup-db`, `restore-db` and `shell-db` only; Compose starts the PostgreSQL container when COMPOSE_PROFILES includes embedded-db. |
COMPOSE_PROJECT_NAME | railhook | Compose project name. Pinned because the monitoring stack joins this project's network by name, and Compose would otherwise derive it from the clone's directory name. |
DOCKER_REGISTRY | unsetexample: ghcr.io/yourorg/railhook | Registry prefix for the api, worker and ui images; defaults to ghcr.io/vadymkykalo/railhook. |
Application environment
| Variable | Default | Description |
|---|---|---|
APP_ENV | development | development | staging | production. Production refuses to start with placeholder secrets. |
APP_BASE_URL | http://localhost:8080 | Public origin of the dashboard, used in emails, shared links, canonical tags and the sitemap. Include the port when it is not 80; this one matches RAILHOOK_PORT below. |
Database - embedded mode (COMPOSE_PROFILES=embedded-db)
| Variable | Default | Description |
|---|---|---|
POSTGRES_IMAGE | postgres:16-alpine | Image and credentials of the PostgreSQL container. |
POSTGRES_DB | webhook_platform | Image and credentials of the PostgreSQL container. |
POSTGRES_USER | webhook_user | Image and credentials of the PostgreSQL container. |
POSTGRES_PASSWORD | webhook_dev_pass_12345 | Image and credentials of the PostgreSQL container. |
Database - external mode (no embedded-db profile)
| Variable | Default | Description |
|---|---|---|
DB_HOST | postgres | Database connection; DB_HOST stays `postgres` (the Compose service) in embedded mode. |
DB_PORT | 5432 | Database connection; DB_HOST stays `postgres` (the Compose service) in embedded mode. |
DB_NAME | webhook_platform | Database connection; DB_HOST stays `postgres` (the Compose service) in embedded mode. |
DB_USER | webhook_user | Database connection; DB_HOST stays `postgres` (the Compose service) in embedded mode. |
DB_PASSWORD | webhook_dev_pass_12345 | Database connection; DB_HOST stays `postgres` (the Compose service) in embedded mode. |
DB_POOL_MAX_SIZE | 20 | API connection pool (connections; timeout in ms). Named apart from the worker's on purpose: a shared name once gave the worker the API's smaller pool. |
DB_POOL_MIN_IDLE | 10 | API connection pool (connections; timeout in ms). Named apart from the worker's on purpose: a shared name once gave the worker the API's smaller pool. |
DB_POOL_CONNECTION_TIMEOUT | 30000 | API connection pool (connections; timeout in ms). Named apart from the worker's on purpose: a shared name once gave the worker the API's smaller pool. |
WORKER_DB_POOL_MAX_SIZE | 40 | Worker connection pool. Smaller than the worker's thread count because no transaction spans the outbound HTTP call; check hikari_connections_pending under load before changing it. |
WORKER_DB_POOL_MIN_IDLE | 20 | Worker connection pool. Smaller than the worker's thread count because no transaction spans the outbound HTTP call; check hikari_connections_pending under load before changing it. |
Redis configuration (for distributed rate limiting & concurrency)
| Variable | Default | Description |
|---|---|---|
REDIS_IMAGE | redis:7-alpine | Redis image, address, password and memory cap. |
REDIS_HOST | redis | Redis image, address, password and memory cap. |
REDIS_PORT | 6379 | Redis image, address, password and memory cap. |
REDIS_PASSWORD | webhook_redis_pass | Redis image, address, password and memory cap. |
REDIS_MAXMEMORY | 256mb | Redis image, address, password and memory cap. |
Security (required - must change for production)
| Variable | Default | Description |
|---|---|---|
WEBHOOK_ENCRYPTION_KEY | dev_encryption_key_32_chars_min! | Key that encrypts stored secrets, 32+ characters. |
WEBHOOK_ENCRYPTION_SALT | dev_encryption_salt_16_chars | PBKDF2 salt for the AES-256 key, 16+ characters, unique per deployment. |
WEBHOOK_ENCRYPTION_KEYS | empty | Keys for zero-downtime rotation as "version:key,version:key"; empty uses WEBHOOK_ENCRYPTION_KEY as version 1. |
WEBHOOK_ENCRYPTION_KEY_ACTIVE_VERSION | 0 | Key version new encryptions use; 0 picks the highest. |
JWT_SECRET | dev_jwt_secret_key_32_chars_minimum | JWT signing secret, 32+ characters. |
PLATFORM_ADMIN_TOKEN | empty | Token for /api/v1/admin/** (X-Platform-Admin-Token header), including key rotation across all organizations; an organization OWNER does not get this. Empty keeps those endpoints closed. |
PLATFORM_ADMIN_EMAILS | empty | Comma-separated emails allowed into the platform admin panel (sign-in under 12 hours old). Empty means nobody. |
API service
| Variable | Default | Description |
|---|---|---|
API_IMAGE_TAG | latest | API image tag. |
Worker service
| Variable | Default | Description |
|---|---|---|
WORKER_IMAGE_TAG | latest | Worker image tag. |
Scheduling (api + worker)
| Variable | Default | Description |
|---|---|---|
SCHEDULING_POOL_SIZE | 8 | Threads shared by all scheduled jobs. Keep at 8 or more: with Spring's default of 1, one slow job stalls every other one. |
UI service
| Variable | Default | Description |
|---|---|---|
RAILHOOK_PORT | 8080 | Host port and bind address of the dashboard's nginx, the only published port; it also proxies the API. Use RAILHOOK_BIND=127.0.0.1 behind a reverse proxy. |
RAILHOOK_BIND | 0.0.0.0 | Host port and bind address of the dashboard's nginx, the only published port; it also proxies the API. Use RAILHOOK_BIND=127.0.0.1 behind a reverse proxy. |
RAILHOOK_DOMAIN | empty | Domain and ACME contact for the built-in Caddy TLS terminator (COMPOSE_PROFILES=tls). |
ACME_EMAIL | empty | Domain and ACME contact for the built-in Caddy TLS terminator (COMPOSE_PROFILES=tls). |
UI_IMAGE_TAG | latest | UI image tag. |
VITE_API_URL | empty | Build-time only: API origin baked into the UI bundle. Leave empty; the UI's nginx proxies the API on the same origin. If set, add it to CORS_ALLOWED_ORIGINS too. |
VITE_CSP_EXTRA_CONNECT | empty | Build-time only: extra CSP connect-src origins, space-separated. |
RAILHOOK_CONTACT_DOMAIN | empty | Domain for the sales@ and support@ addresses in the contact form and footer, e.g. example.com. Empty offers no addresses. Read by the ui container at start. |
Data retention policies
| Variable | Default | Description |
|---|---|---|
DATA_RETENTION_ATTEMPTS_DAYS | 90 | Days to keep failed and successful (2xx) Attempts, and received incoming events. |
DATA_RETENTION_SUCCESSFUL_ATTEMPTS_DAYS | 14 | Days to keep failed and successful (2xx) Attempts, and received incoming events. |
DATA_RETENTION_INCOMING_EVENTS_DAYS | 30 | Days to keep failed and successful (2xx) Attempts, and received incoming events. |
DATA_RETENTION_EVENTS_DAYS | 90 | Days to keep Events and, by cascade, their Deliveries; -1 keeps them forever. The only bound on those tables when billing is off. Matches DATA_RETENTION_ATTEMPTS_DAYS on purpose. |
DATA_RETENTION_DLQ_DAYS | 14 | Days a Delivery or Forward stays in the DLQ (Failed Messages) before it is deleted; -1 keeps it until its Event expires. |
DATA_RETENTION_MAX_ATTEMPTS_PER_DELIVERY | 10 | Most recent Attempts kept per Delivery. |
DATA_RETENTION_BATCH_SIZE | 1000 | Rows per cleanup batch. |
DATA_RETENTION_CRON | 0 0 2 * * * | Cleanup schedule (cron, daily at 2 AM). |
DATA_RETENTION_LIMIT_CRON | 0 */30 * * * * | Limit enforcement schedule (cron, every 30 minutes). |
DATA_RETENTION_BURST_CLEANUP_CRON | 0 0 */4 * * * | Extra cleanup for daytime error spikes (cron, every 4 hours). |
DATA_RETENTION_TABLE_METRICS_INTERVAL_MS | 900000 | Table size gauge refresh interval (ms). |
Partition maintenance
| Variable | Default | Description |
|---|---|---|
PARTITION_MAINTENANCE_ENABLED | true | Creates delivery_attempts and tunnel_request_log partitions ahead of need and drops those past retention; lookahead in months and weeks, schedule as cron. |
PARTITION_MAINTENANCE_DELIVERY_ATTEMPTS_LOOKAHEAD_MONTHS | 3 | Creates delivery_attempts and tunnel_request_log partitions ahead of need and drops those past retention; lookahead in months and weeks, schedule as cron. |
PARTITION_MAINTENANCE_TUNNEL_LOG_LOOKAHEAD_WEEKS | 3 | Creates delivery_attempts and tunnel_request_log partitions ahead of need and drops those past retention; lookahead in months and weeks, schedule as cron. |
PARTITION_MAINTENANCE_CRON | 0 45 1 * * * | Creates delivery_attempts and tunnel_request_log partitions ahead of need and drops those past retention; lookahead in months and weeks, schedule as cron. |
Rate limiting
| Variable | Default | Description |
|---|---|---|
GLOBAL_RATE_LIMIT_PER_SECOND | 5000 | Platform-wide request limit per instance (requests/s). |
ORG_RATE_LIMIT_ENABLED | false | Per-organization request limit (requests/s), for shared installs where one tenant must not spend everyone's budget. Costs a Redis round trip per request, so leave it off when self-hosting. |
ORG_RATE_LIMIT_PER_SECOND | 200 | Per-organization request limit (requests/s), for shared installs where one tenant must not spend everyone's budget. Costs a Redis round trip per request, so leave it off when self-hosting. |
GLOBAL_RATE_LIMIT_ENABLED | true | Per-organization request limit (requests/s), for shared installs where one tenant must not spend everyone's budget. Costs a Redis round trip per request, so leave it off when self-hosting. |
EVENT_INGESTION_RATE_LIMIT_PER_SECOND | 100 | Events per second per project (token bucket). |
AUTH_RATE_LIMIT_LOGIN_PER_MINUTE | 10 | Sign-in and registration attempts per minute, per IP and per email. |
AUTH_RATE_LIMIT_REGISTER_PER_MINUTE | 5 | Sign-in and registration attempts per minute, per IP and per email. |
AUTH_RATE_LIMIT_REFRESH_PER_TOKEN_PER_MINUTE | 30 | Session refreshes per minute, per refresh token and per IP. Separate from sign-in, whose budget page loads would otherwise exhaust. |
AUTH_RATE_LIMIT_REFRESH_PER_IP_PER_MINUTE | 600 | Session refreshes per minute, per refresh token and per IP. Separate from sign-in, whose budget page loads would otherwise exhaust. |
Password authentication
| Variable | Default | Description |
|---|---|---|
AUTH_BCRYPT_STRENGTH | 12 | BCrypt cost, 4-31; each step doubles the work. Changing it never invalidates stored hashes. |
AUTH_LOCKOUT_ENABLED | true | Lock an account after consecutive failed sign-ins. Held in Postgres, not Redis, so it does not weaken when Redis is down. Locks expire on their own and a password reset clears them. |
AUTH_LOCKOUT_THRESHOLD | 5 | Consecutive failures before the account locks. |
AUTH_LOCKOUT_INITIAL_SECONDS | 60 | First lockout, in seconds. Doubles with each further failure. |
AUTH_LOCKOUT_MAX_SECONDS | 900 | Cap on the doubling, in seconds; longer only helps someone locking a stranger out. |
AUTH_LOCKOUT_FAILURE_WINDOW_MINUTES | 60 | Minutes after which a failure no longer counts toward the lockout. |
AUTH_SESSION_CLEANUP_INTERVAL_MS | 3600000 | Interval for purging expired sessions (ms). |
Webhook delivery
| Variable | Default | Description |
|---|---|---|
WEBHOOK_MAX_PAYLOAD_SIZE_BYTES | 262144 | Largest event payload accepted (bytes). |
WEBHOOK_MAX_FANOUT_PER_EVENT | 100 | Most Deliveries one Event may fan out to. |
WEBHOOK_PAYLOAD_COMPRESSION_THRESHOLD_BYTES | 1024 | Payloads larger than this are stored gzip-compressed (bytes). |
ORGANIZATION_SUSPENSION_CACHE_TTL_SECONDS | 60 | Seconds an organization's suspension state is cached on each node; other nodes see a change within this window. |
WEBHOOK_PROJECT_RATE_LIMIT_PER_SECOND | 50 | Deliveries per second one project may take from the shared worker pool. Not the plan's ingest limit: size it from WEBHOOK_OUTGOING_POOL_SIZE. |
WEBHOOK_ENDPOINT_VERIFICATION_REQUIRED | false | Require new endpoints to prove ownership before they receive deliveries (true | false). |
WEBHOOK_INGRESS_REPLAY_WINDOW_MINUTES | 5 | Minutes a received signature is remembered to reject replays. For a source signed with a plain body HMAC this is the only replay bound; raise it for such sources. |
WEBHOOK_ALLOW_PRIVATE_IPS | false | Allow delivery to private IPs (SSRF risk, e.g. cloud metadata). Development only; refused in production. |
WEBHOOK_MAX_CONCURRENT_PER_ENDPOINT | 5 | Concurrent deliveries per endpoint. |
WEBHOOK_MAX_CONCURRENT_PER_TENANT | 20 | Concurrent in-flight attempts per organization across all its endpoints, so one tenant's slow receivers cannot occupy the whole WEBHOOK_OUTGOING_POOL_SIZE. |
WEBHOOK_CONCURRENCY_PERMIT_LEASE_SECONDS | 90 | Seconds a concurrency permit lives before Redis reclaims it; must exceed the 60s maximum delivery timeout plus setup time. |
WEBHOOK_OUTGOING_POOL_SIZE | 50 | Threads for outgoing deliveries. |
WEBHOOK_INCOMING_POOL_SIZE | 20 | Threads for incoming forwards. |
WEBHOOK_CLAIM_ENABLED | true | Worker claims due Deliveries and Forwards from Postgres (true | false). |
WEBHOOK_CLAIM_IDLE_POLL_MS | 200 | How long a poll that found nothing sleeps (ms); a full batch polls again at once. |
WEBHOOK_CLAIM_TIMEOUT_SECONDS | 300 | Seconds after which a claimed row whose worker has not finished is claimed again; keep above 60s plus transformation time. |
WEBHOOK_CLAIM_MAX_TARGETS | 500 | Endpoints or destinations one poll visits. |
WEBHOOK_ASYNC_SHUTDOWN_TIMEOUT_SECONDS | 60 | Seconds to wait for in-flight deliveries on shutdown. |
CLIENT_ERROR_REPORTING_ENABLED | true | Dashboard error reporting. Dashboard errors are posted back and written to this installation's own logs, never to a third party; reports per user per minute. |
CLIENT_ERROR_REPORTS_PER_USER_PER_MINUTE | 20 | Dashboard error reporting. Dashboard errors are posted back and written to this installation's own logs, never to a third party; reports per user per minute. |
WEBHOOK_MAX_RESPONSE_BODY_BYTES | 1048576 | Receiver response body buffered per attempt (bytes); past it the status code alone decides. Only the first 10 KiB is stored. |
TRANSFORM_SCRIPT_TIMEOUT_MS | 2000 | JavaScript transformations. Wall-clock limit per script run (ms); a runaway script holds one delivery slot this long. Exceeding any of these limits fails the Delivery, which is retried. |
TRANSFORM_SCRIPT_MAX_MEMORY_MB | 128 | Allocation limit per run (MB), counted as total allocation rather than live heap. |
TRANSFORM_SCRIPT_MAX_OUTPUT_BYTES | 1048576 | Largest body a script may return (bytes). |
TRANSFORM_SCRIPT_MAX_CONSOLE_LINES | 100 | console.* lines kept per run for the Transform Studio. |
TRANSFORM_SCRIPT_MAX_CHARS | 65536 | Largest script the engine compiles (characters). |
WEBHOOK_POOL_MAX_CONNECTIONS | 200 | Outgoing HTTP connection pool shared by deliveries and forwards (connections, seconds). |
WEBHOOK_POOL_PENDING_ACQUIRE_TIMEOUT_SECONDS | 10 | Outgoing HTTP connection pool shared by deliveries and forwards (connections, seconds). |
WEBHOOK_POOL_MAX_IDLE_TIME_SECONDS | 60 | Outgoing HTTP connection pool shared by deliveries and forwards (connections, seconds). |
WEBHOOK_ALLOWED_HOSTS | empty | Comma-separated host names exempt from the private-IP check, matched as written in the endpoint URL. Empty exempts nothing; other rules still apply. |
WEBHOOK_TRUSTED_PROXIES | empty | Comma-separated proxy IPs/CIDRs whose X-Forwarded-For sets the client IP (rate limits, audit, source IP checks); behind a CDN include its ranges. Empty trusts nothing. |
Incoming webhooks
| Variable | Default | Description |
|---|---|---|
WEBHOOK_INGRESS_BASE_URL | http://localhost:8080 | Base URL for ingress URLs shown in the dashboard. |
WEBHOOK_INCOMING_MAX_PAYLOAD_SIZE_BYTES | 524288 | Largest incoming payload accepted (bytes). |
WEBHOOK_INCOMING_RATE_LIMIT_PER_SECOND | 100 | Default ingress limit for a source that sets none (requests/s); 0 disables the default. |
DLQ_MONITORING_INTERVAL_MS | 60000 | DLQ monitoring interval (ms). |
Ordering (FIFO delivery per endpoint)
| Variable | Default | Description |
|---|---|---|
ORDERING_GAP_TIMEOUT_SECONDS | 60 | Seconds an ordered Delivery waits on a gap before going out of order, and only once nothing in the gap is being or about to be attempted. |
ORDERING_BUFFER_RESCHEDULE_DELAY_SECONDS | 5 | Fallback poll for a Delivery parked behind a missing sequence (seconds). |
ORDERING_DELIVERED_SEQ_TTL_HOURS | 24 | TTL of delivered-sequence tracking in Redis (hours). |
ORDERING_BUFFER_TTL_MINUTES | 10 | TTL of the ordering buffer in Redis (minutes). |
ORDERING_BUFFER_GAUGE_RESYNC_MS | 30000 | How often the ordering buffer size gauge resyncs from Redis (ms). |
ORDERING_STRANDED_SEQUENCE_AFTER_SECONDS | 120 | Backfills ordered Deliveries left without a sequence number by a process that died after commit; only rows older than this many seconds, per batch. Below a few seconds it races ingest. |
ORDERING_STRANDED_SEQUENCE_BATCH_SIZE | 500 | Backfills ordered Deliveries left without a sequence number by a process that died after commit; only rows older than this many seconds, per batch. Below a few seconds it races ingest. |
ORDERING_SEQUENCE_RECONCILIATION_LOOKBACK_HOURS | 48 | How far back sequence reconciliation looks for active ordered endpoints (hours). |
ORDERING_SEQUENCE_RECONCILIATION_INTERVAL_MS | 900000 | Sequence reconciliation interval (ms). |
Event time machine (replay)
| Variable | Default | Description |
|---|---|---|
REPLAY_BATCH_SIZE | 200 | Events read per replay batch. |
REPLAY_BATCH_DELAY_MS | 50 | Pause between replay batches (ms). |
REPLAY_MAX_EVENTS_PER_SESSION | 500000 | Most events one replay session may cover. |
WEBHOOK_RETRY_AFTER_MAX_SECONDS | 21600 | Longest a receiver's Retry-After (on 429 and 503) may delay the next attempt (seconds); it only ever delays. 0 ignores the header. Kept within DELIVERY_ESCALATION_HARD_CAP_HOURS. |
Auto-disable a persistently failing target
| Variable | Default | Description |
|---|---|---|
ENDPOINT_AUTO_DISABLE_ENABLED | true | Turn off an endpoint or destination that has only failed for a whole window, and notify its owners. Set it the same on api and worker. |
ENDPOINT_AUTO_DISABLE_AFTER_HOURS | 72 | Hours of failures without a success before disabling, at least 1. Shorter than DELIVERY_ESCALATION_HARD_CAP_HOURS on purpose. |
ENDPOINT_AUTO_DISABLE_MIN_FAILURES | 10 | Failed attempts that window must also contain, so an idle endpoint is not disabled. |
ENDPOINT_AUTO_DISABLE_INTERVAL_MS | 300000 | Sweep interval (ms) and targets checked per sweep. |
ENDPOINT_AUTO_DISABLE_BATCH_SIZE | 200 | Sweep interval (ms) and targets checked per sweep. |
Circuit breaker (Redis-backed, shared across worker pods)
| Variable | Default | Description |
|---|---|---|
CIRCUIT_BREAKER_FAILURE_RATE_THRESHOLD | 50 | Failure rate that opens the circuit (percent, 0-100). |
CIRCUIT_BREAKER_MINIMUM_CALLS | 5 | Calls needed before the failure rate is evaluated. |
CIRCUIT_BREAKER_WAIT_DURATION_SECONDS | 30 | Seconds the circuit stays open before probing. |
CIRCUIT_BREAKER_WINDOW_TTL_SECONDS | 120 | Sliding window of the failure counters (seconds). |
CIRCUIT_BREAKER_SLOW_CALL_THRESHOLD_MS | 10000 | A call slower than this counts as slow, even with a 2xx (ms). |
CIRCUIT_BREAKER_SLOW_CALL_RATE_THRESHOLD | 80 | Slow call rate that opens the circuit (percent, 0-100). |
Stale delivery escalation (hard-cap auto-DLQ)
| Variable | Default | Description |
|---|---|---|
DELIVERY_ESCALATION_HARD_CAP_HOURS | 96 | Hours after which a PENDING Delivery goes to the DLQ. Must cover the outgoing retry ladder's worst case (~83h), or startup fails. |
DELIVERY_ESCALATION_BATCH_SIZE | 100 | Deliveries per escalation batch. |
Stale forward escalation (hard-cap auto-DLQ, Incoming)
| Variable | Default | Description |
|---|---|---|
FORWARD_ESCALATION_HARD_CAP_HOURS | 24 | Hours from receipt after which an outstanding Forward goes to the DLQ; batch size and interval (ms). Must cover the incoming retry ladder's worst case (~11h), or startup fails. |
FORWARD_ESCALATION_BATCH_SIZE | 100 | Hours from receipt after which an outstanding Forward goes to the DLQ; batch size and interval (ms). Must cover the incoming retry ladder's worst case (~11h), or startup fails. |
FORWARD_ESCALATION_INTERVAL_MS | 300000 | Hours from receipt after which an outstanding Forward goes to the DLQ; batch size and interval (ms). Must cover the incoming retry ladder's worst case (~11h), or startup fails. |
DELIVERY_ESCALATION_INTERVAL_MS | 300000 | Stale delivery check interval (ms). |
Queue depth metrics
| Variable | Default | Description |
|---|---|---|
QUEUE_DEPTH_METRICS_INTERVAL_MS | 900000 | Queue depth gauge refresh interval (ms). |
QUEUE_DEPTH_METRICS_RETENTION_DAYS | 30 | Only rows created within this many days are counted. |
Test endpoints (Request Bin)
| Variable | Default | Description |
|---|---|---|
TEST_ENDPOINT_BASE_URL | unset | Public origin of test endpoint URLs (…/hook/<slug>); defaults to APP_BASE_URL. Never an internal name. |
TEST_ENDPOINT_MAX_PER_PROJECT | 10 | Test endpoints per project. |
TEST_ENDPOINT_MAX_TTL_HOURS | 72 | Longest test endpoint lifetime (hours). |
TEST_ENDPOINT_CLEANUP_INTERVAL_MS | 3600000 | Expired test endpoint cleanup interval (ms). |
Rules engine
| Variable | Default | Description |
|---|---|---|
RULES_CACHE_REFRESH_MS | 30000 | Compiled rules cache refresh (ms). |
RULES_EXECUTION_LOG_RETENTION_DAYS | 7 | Rule execution log retention (days). |
Transformations
| Variable | Default | Description |
|---|---|---|
TRANSFORMATION_HISTORY_LIMIT | 50 | Published versions kept per transformation (up to ~64 KB each); the one in use is never dropped. |
Workflow engine
| Variable | Default | Description |
|---|---|---|
WORKFLOW_POOL_CORE_SIZE | 4 | Workflow executor threads and queue capacity. |
WORKFLOW_POOL_MAX_SIZE | 8 | Workflow executor threads and queue capacity. |
WORKFLOW_POOL_QUEUE_CAPACITY | 50 | Workflow executor threads and queue capacity. |
WORKFLOW_MAX_DURATION_SECONDS | 600 | Longest a workflow run may take (seconds). |
WORKFLOW_MAX_RECURSION_DEPTH | 3 | Deepest workflow → CreateEvent → workflow chain. |
WORKFLOW_STUCK_THRESHOLD_MINUTES | 15 | Stuck execution recovery: threshold (minutes) and check interval (ms). |
WORKFLOW_RECOVERY_INTERVAL_MS | 120000 | Stuck execution recovery: threshold (minutes) and check interval (ms). |
WORKFLOW_RESUME_INTERVAL_MS | 5000 | How often suspended delay nodes are checked for being due (ms); the resolution of a delay. |
WORKFLOW_RESUME_BATCH_SIZE | 50 | Due executions resumed per tick; the rest wait for the next tick. |
WORKFLOW_NODE_TIMEOUT_DEFAULT_SECONDS | 30 | Per-node timeouts (seconds) and the timeout thread pool size. |
WORKFLOW_NODE_TIMEOUT_HTTP_SECONDS | 60 | Per-node timeouts (seconds) and the timeout thread pool size. |
WORKFLOW_NODE_TIMEOUT_SLACK_SECONDS | 60 | Per-node timeouts (seconds) and the timeout thread pool size. |
WORKFLOW_NODE_TIMEOUT_CREATE_EVENT_SECONDS | 30 | Per-node timeouts (seconds) and the timeout thread pool size. |
WORKFLOW_NODE_TIMEOUT_POOL_SIZE | 16 | Per-node timeouts (seconds) and the timeout thread pool size. |
WORKFLOW_SHUTDOWN_AWAIT_SECONDS | 30 | Seconds to wait for in-flight workflows on shutdown. |
WORKFLOW_TRIGGER_OUTBOX_POLL_INTERVAL_MS | 2000 | Workflow trigger outbox: poll interval (ms), batch size, attempts per trigger. |
WORKFLOW_TRIGGER_OUTBOX_BATCH_SIZE | 50 | Workflow trigger outbox: poll interval (ms), batch size, attempts per trigger. |
WORKFLOW_TRIGGER_OUTBOX_MAX_ATTEMPTS | 3 | Workflow trigger outbox: poll interval (ms), batch size, attempts per trigger. |
WORKFLOW_TRIGGER_OUTBOX_MAX_PER_PROJECT | 5 | Triggers claimed per project per batch. |
WORKFLOW_TRIGGER_OUTBOX_MAX_CONCURRENT_PER_PROJECT | 3 | Concurrent workflow executions per project. |
WORKFLOW_TRIGGER_OUTBOX_STALLED_AFTER_MINUTES | 15 | Minutes after which a trigger abandoned in PROCESSING is requeued, and the sweep interval (ms). Keep it above your longest workflow, or a live one runs twice. |
WORKFLOW_TRIGGER_OUTBOX_STALLED_SWEEP_MS | 300000 | Minutes after which a trigger abandoned in PROCESSING is requeued, and the sweep interval (ms). Keep it above your longest workflow, or a live one runs twice. |
Device authorization
| Variable | Default | Description |
|---|---|---|
DEVICE_AUTH_CLEANUP_INTERVAL_MS | 300000 | Expired device code cleanup interval (ms). |
Email verification
| Variable | Default | Description |
|---|---|---|
EMAIL_ENABLED | false | Send real email (false logs links to the console). Also enforces email verification: while off, new accounts are marked verified. Turn on wherever registration is open to strangers. |
EMAIL_FROM | noreply@example.com | Sender address; use a domain you own, or mail fails SPF/DKIM and lands in spam. |
EMAIL_SUPPORT_ADDRESS | empty | Contact address in the notice about an unexpected email change; empty says to contact the administrator. |
ONBOARDING_EMAILS_ENABLED | false | Welcome and follow-up mail to new accounts, written for Railhook Cloud; keep off when self-hosting. |
CONTACT_DAILY_LIMIT | 30 | Contact form messages sent per UTC day, so the form cannot spend the mail quota verification needs. |
SMTP_HOST | localhost | SMTP relay, used when EMAIL_ENABLED=true. |
SMTP_PORT | 1025 | SMTP relay, used when EMAIL_ENABLED=true. |
SMTP_USERNAME | empty | SMTP relay, used when EMAIL_ENABLED=true. |
SMTP_PASSWORD | empty | SMTP relay, used when EMAIL_ENABLED=true. |
SMTP_AUTH | false | SMTP relay, used when EMAIL_ENABLED=true. |
SMTP_STARTTLS | false | SMTP relay, used when EMAIL_ENABLED=true. |
Alert notifications
| Variable | Default | Description |
|---|---|---|
ALERTS_NOTIFICATIONS_ENABLED | false | Send alert notifications to Slack, webhooks and email (false logs them instead). |
ALERTS_EVALUATION_CRON | 0 * * * * * | Alert rule evaluation schedule (cron); how late an alert can be, not its window. |
Entitlements (limits when billing is off)
| Variable | Default | Description |
|---|---|---|
ENTITLEMENT_DEFAULT_RATE_LIMIT | 100 | Rate limit (events/s) and fan-out applied when BILLING_ENABLED=false, instead of no limit. |
ENTITLEMENT_DEFAULT_MAX_FANOUT | 50 | Rate limit (events/s) and fan-out applied when BILLING_ENABLED=false, instead of no limit. |
Captcha (registration)
| Variable | Default | Description |
|---|---|---|
CAPTCHA_SECRET_KEY | unset | Registration CAPTCHA secret and verify URL (Turnstile by default; hCaptcha works too). Empty means no challenge; required with APP_ENV=production and BILLING_ENABLED=true. Fails closed. |
CAPTCHA_VERIFY_URL | unsetexample: https://challenges.cloudflare.com/turnstile/v0/siteverify | Registration CAPTCHA secret and verify URL (Turnstile by default; hCaptcha works too). Empty means no challenge; required with APP_ENV=production and BILLING_ENABLED=true. Fails closed. |
CAPTCHA_SITE_KEY | unset | Site key and script for the registration page, read by the ui container at start. Set together with CAPTCHA_SECRET_KEY, or every registration is refused. |
CAPTCHA_SCRIPT_URL | unsetexample: https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit | Site key and script for the registration page, read by the ui container at start. Set together with CAPTCHA_SECRET_KEY, or every registration is refused. |
Public webhook tester
| Variable | Default | Description |
|---|---|---|
PUBLIC_TESTER_ENABLED | unsetexample: false | The anonymous /tester page; off by default. Live URLs per address and overall when on. |
PUBLIC_TESTER_PER_ADDRESS | unsetexample: 3 | The anonymous /tester page; off by default. Live URLs per address and overall when on. |
PUBLIC_TESTER_MAX_ACTIVE | unsetexample: 5000 | The anonymous /tester page; off by default. Live URLs per address and overall when on. |
Live demo
| Variable | Default | Description |
|---|---|---|
DEMO_ENABLED | unsetexample: false | Anonymous read-only demo organization; off by default, and turning it off deletes it. Session length and data refresh in minutes. Set on both api and ui. |
DEMO_SESSION_TTL_MINUTES | unsetexample: 30 | Anonymous read-only demo organization; off by default, and turning it off deletes it. Session length and data refresh in minutes. Set on both api and ui. |
DEMO_REFRESH_INTERVAL_MINUTES | unsetexample: 60 | Anonymous read-only demo organization; off by default, and turning it off deletes it. Session length and data refresh in minutes. Set on both api and ui. |
DEMO_SCRIPT_RUNS_PER_SESSION_PER_MINUTE | unsetexample: 20 | JavaScript transformation runs per minute for a demo session and per IP. Runs hold request threads, so at a 2000 ms timeout 60 per IP bounds one address to two threads. |
DEMO_SCRIPT_RUNS_PER_IP_PER_MINUTE | unsetexample: 60 | JavaScript transformation runs per minute for a demo session and per IP. Runs hold request threads, so at a 2000 ms timeout 60 per IP bounds one address to two threads. |
Blog
| Variable | Default | Description |
|---|---|---|
BLOG_ENABLED | unsetexample: false | Serve Railhook's own blog at /blog; off by default. Read by the ui container. |
Web analytics
| Variable | Default | Description |
|---|---|---|
WEB_ANALYTICS_TOKEN | unset | Cloudflare Web Analytics token; empty loads no analytics. Read by the ui container at start. |
STATUS_PAGE_URL | unset | Status page URL linked from the footer; empty shows no link. Read by the ui container at start. |
Sign in with google
| Variable | Default | Description |
|---|---|---|
GOOGLE_OAUTH_CLIENT_ID | unset | "Continue with Google", off unless both are set. Redirect URI: <APP_BASE_URL>/api/v1/auth/oauth/google/callback |
GOOGLE_OAUTH_CLIENT_SECRET | unset | "Continue with Google", off unless both are set. Redirect URI: <APP_BASE_URL>/api/v1/auth/oauth/google/callback |
Billing / plans
| Variable | Default | Description |
|---|---|---|
BILLING_ENABLED | false | Enforce plan quotas (false = self-hosted, unlimited). In production it also requires EMAIL_ENABLED=true and CAPTCHA_SECRET_KEY. |
BILLING_DEFAULT_PROVIDER | noop | noop | stripe | wayforpay; noop with billing on runs the free plan only. |
STRIPE_SECRET_KEY | unsetexample: sk_test_... | Stripe credentials; STRIPE_PRICE_MAP is plan_interval=price_id (interval _monthly or _yearly). |
STRIPE_WEBHOOK_SECRET | unsetexample: whsec_... | Stripe credentials; STRIPE_PRICE_MAP is plan_interval=price_id (interval _monthly or _yearly). |
STRIPE_PRICE_MAP | unsetexample: starter_monthly=price_xxx,starter_yearly=price_xxy,pro_monthly=price_yyy,pro_yearly=price_yyz | Stripe credentials; STRIPE_PRICE_MAP is plan_interval=price_id (interval _monthly or _yearly). |
BILLING_RECONCILIATION_CRON | 0 0 */6 * * * | Schedule for syncing billing state from Stripe (cron). |
WAYFORPAY_MERCHANT_ACCOUNT | unsetexample: your_merchant_account | WayForPay credentials; WAYFORPAY_PLAN_PRICES is plan_interval=amount_in_cents. |
WAYFORPAY_MERCHANT_SECRET | unsetexample: your_merchant_secret_key | WayForPay credentials; WAYFORPAY_PLAN_PRICES is plan_interval=amount_in_cents. |
WAYFORPAY_MERCHANT_DOMAIN | unsetexample: example.com | WayForPay credentials; WAYFORPAY_PLAN_PRICES is plan_interval=amount_in_cents. |
WAYFORPAY_SERVICE_URL | unsetexample: https://example.com/api/v1/billing/webhook/wayforpay | WayForPay credentials; WAYFORPAY_PLAN_PRICES is plan_interval=amount_in_cents. |
WAYFORPAY_PLAN_PRICES | unsetexample: starter_monthly=2900,starter_yearly=29000,pro_monthly=9900,pro_yearly=99000 | WayForPay credentials; WAYFORPAY_PLAN_PRICES is plan_interval=amount_in_cents. |
PRERENDER_LOCALE | en | Build-time only: locale the public pages are prerendered in. |
Tunnel (CLI local webhook tunnels)
| Variable | Default | Description |
|---|---|---|
TUNNEL_INGRESS_BASE_URL | http://localhost:8080 | Public base URL of incoming-source and tunnel URLs; defaults to APP_BASE_URL. |
TUNNEL_HEARTBEAT_TIMEOUT_SECONDS | 120 | Seconds without a heartbeat before a tunnel session is stale. |
TUNNEL_CLEANUP_INTERVAL_MS | 60000 | Stale session cleanup interval (ms). |
TUNNEL_REQUEST_LOG_RETENTION_DAYS | 7 | Tunnel request log retention (days), enforced by dropping weekly partitions. |
Mcp server (ai agents)
| Variable | Default | Description |
|---|---|---|
MCP_ENABLED | true | Serve the MCP server at /mcp, authenticated with a project API key. |
MCP_OAUTH_ENABLED | true | OAuth sign-in for MCP connectors (claude.ai, ChatGPT); needs APP_BASE_URL to be the public origin. false accepts API keys only. |
CORS configuration
| Variable | Default | Description |
|---|---|---|
CORS_ALLOWED_ORIGINS | http://localhost:8080,http://localhost:5173 | Comma-separated allowed origins. Must include the RAILHOOK_PORT origin: browsers send Origin on same-origin POSTs too. |
Observability
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL | INFO | TRACE | DEBUG | INFO | WARN | ERROR |
SWAGGER_ENABLED | false | Serve Swagger UI; keep false in production. |
JVM tuning
| Variable | Default | Description |
|---|---|---|
API_JAVA_OPTS | unsetexample: -Xms256m -Xmx384m | Extra JVM flags, appended to the image's container tuning; an explicit -Xmx still wins. |
WORKER_JAVA_OPTS | unsetexample: -Xms384m -Xmx512m | Extra JVM flags, appended to the image's container tuning; an explicit -Xmx still wins. |
METRICS_ENABLED | true | Expose Prometheus metrics. |
Resource limits (Docker deploy.resources)
| Variable | Default | Description |
|---|---|---|
POSTGRES_MEMORY_LIMIT | unsetexample: 1G | Container memory limits. |
REDIS_MEMORY_LIMIT | unsetexample: 512m | Container memory limits. |
API_MEMORY_LIMIT | unsetexample: 768m | Container memory limits. |
WORKER_MEMORY_LIMIT | unsetexample: 768m | Container memory limits. |
API_REPLICAS | 1 | API containers. Two keep the API answering during `railhook upgrade` (about 600 MB more); one costs ~30s of 502 per upgrade while the JVM starts. |
UI_MEMORY_LIMIT | unsetexample: 128m | UI container memory limit. |
Monitoring stack (optional)
| Variable | Default | Description |
|---|---|---|
GRAFANA_ADMIN_PASSWORD | empty | Grafana admin password, 16+ characters; the stack refuses to start without it. Re-applied on every start. |
GRAFANA_ADMIN_USER | admin | Grafana admin login. |
GRAFANA_PORT | 3001 | Loopback port Grafana is published on. |
MONITORING_DOMAIN | empty | Host name to serve Grafana on through the built-in Caddy; empty keeps it loopback-only. Put an identity-aware proxy in front. |
MONITORING_TLS_CERT | empty | Certificate and key Caddy serves for MONITORING_DOMAIN instead of obtaining one, as paths inside the Caddy container. Both or neither. |
MONITORING_TLS_KEY | empty | Certificate and key Caddy serves for MONITORING_DOMAIN instead of obtaining one, as paths inside the Caddy container. Both or neither. |
MONITORING_PROBE_URLS | empty | Comma-separated URLs for the uptime probes; empty probes RAILHOOK_DOMAIN when set. |
RAILHOOK_NETWORK | empty | Platform Docker network to join; empty finds it by its Compose label. |
MONITORING_NODENAME | empty | Server name shown on the Host dashboard; empty uses the hostname. |
MONITORING_BACKUP_DIR | ../backups | Backup directory, relative to monitoring/, read by the BackupStale alert. |
PROMETHEUS_RETENTION | 15d | Prometheus retention time and disk budget. |
PROMETHEUS_RETENTION_SIZE | 4GB | Prometheus retention time and disk budget. |
Alerting (optional)
| Variable | Default | Description |
|---|---|---|
ALERTMANAGER_SLACK_WEBHOOK_URL | empty | Alertmanager receivers. With none set, alerts show only in Grafana. Slack incoming webhook URL and channel. |
ALERTMANAGER_SLACK_CHANNEL | #railhook-alerts | Alertmanager receivers. With none set, alerts show only in Grafana. Slack incoming webhook URL and channel. |
ALERTMANAGER_WEBHOOK_URL | empty | Generic webhook receiver URL (PagerDuty Events v2, Opsgenie, …). |
ALERTMANAGER_EMAIL_TO | empty | Alert email recipient; sender and relay default to EMAIL_FROM and SMTP_*. |
ALERTMANAGER_EMAIL_FROM | empty | Alert email recipient; sender and relay default to EMAIL_FROM and SMTP_*. |
ALERTMANAGER_SMTP_HOST | empty | Alert email recipient; sender and relay default to EMAIL_FROM and SMTP_*. |
ALERTMANAGER_SMTP_PORT | empty | Alert email recipient; sender and relay default to EMAIL_FROM and SMTP_*. |
ALERTMANAGER_SMTP_USERNAME | empty | Alert email recipient; sender and relay default to EMAIL_FROM and SMTP_*. |
ALERTMANAGER_SMTP_PASSWORD | empty | Alert email recipient; sender and relay default to EMAIL_FROM and SMTP_*. |
ALERTMANAGER_SMTP_REQUIRE_TLS | empty | true | false; empty requires TLS except for localhost, mailpit and mailhog. |
ALERTMANAGER_TELEGRAM_BOT_TOKEN | empty | Telegram bot token and chat id (negative for groups); both, or Telegram stays off. |
ALERTMANAGER_TELEGRAM_CHAT_ID | empty | Telegram bot token and chat id (negative for groups); both, or Telegram stays off. |
ALERTMANAGER_HEARTBEAT_URL | empty | Heartbeat URL pinged every minute by the Watchdog alert, so an outside service notices this host going down. |
ALERTMANAGER_EXTERNAL_URL | http://localhost:9093 | Alertmanager's address in Slack and webhook payloads. |
Log aggregation (Loki)
| Variable | Default | Description |
|---|---|---|
LOKI_RETENTION_PERIOD | 168h | How long Loki keeps log lines (Go duration, e.g. 168h). |
Backup configuration
| Variable | Default | Description |
|---|---|---|
BACKUP_DIR | ./backups | Backup directory and retention (days) for `make backup-db` and the db-backup service. |
BACKUP_RETENTION_DAYS | 30 | Backup directory and retention (days) for `make backup-db` and the db-backup service. |
DB_BACKUP_INTERVAL_SECONDS | 86400 | Seconds between scheduled backups by the db-backup service. |
No variable matches the search.