Configuration reference
Every environment variable and binding for self-hosted TrayPage, and what each one means.
This is the list of settings for the self-hosted edition. On Docker / VPS you set them in .env (deploy/docker/.env.example); on Cloudflare you set them in vars in wrangler.jsonc and as secrets.
"Required" means TrayPage cannot start, or cannot work correctly, without it.
| Name | Required | Meaning |
|---|
APP_ORIGIN | Required in production | The app URL (for example https://traypage.example.com). Used to build share URLs and sign-in links, and as the allowed origin for authentication. Required for Docker |
VIEWER_ORIGIN | Required in production | The URL of the viewer host that serves page content (for example https://view.traypage.example.com). Use a different host name from APP_ORIGIN. Required for Docker |
VIEWER_HOST | Optional | The host name (including port) of the viewer host. Set it on Cloudflare. On Docker, if unset, the host of VIEWER_ORIGIN is used |
BETTER_AUTH_SECRET | Required | The key that signs sign-in sessions. Generate one with something like openssl rand -hex 32. Changing it signs everyone out |
VIEWER_TICKET_SECRET | Required on Cloudflare | The key for the short-lived (5 minute) signed tickets handed to the viewer host. On Docker, BETTER_AUTH_SECRET is used if unset. On Cloudflare, use a different value from BETTER_AUTH_SECRET |
TRAYPAGE_EDITION | Optional | self_hosted removes usage limits and trials, and disables operator-only features and scheduled jobs. The Docker setup sets it automatically and the Cloudflare template already sets it. If unset or unrecognized, behavior is the same as tray.page (hosted) |
TRAYPAGE_LICENSE_KEY | Optional | The Enterprise license key (starts with tp1.). See License and Enterprise features |
ENVIRONMENT | Optional | production keeps the test-only login disabled at all times and ignores the setting that turns off the sign-in rate limit (AUTH_RATE_LIMIT). The Cloudflare template already sets production |
| Name | Applies to | Meaning |
|---|
DATABASE_URL | Docker | The Postgres connection string (for example postgres://user:password@host:5432/db?sslmode=require). If unset on Docker, the bundled Postgres is used |
DATABASE_POOL_MAX | Docker | Maximum connection pool size. Defaults to 10 |
POSTGRES_PASSWORD | Docker (compose) | Password of the bundled Postgres. Decide it before the first start |
HYPERDRIVE | Cloudflare | The Hyperdrive binding. You pick an existing configuration in the button flow |
The app creates tables automatically on startup (on Cloudflare, on the first access).
| Name | Applies to | Meaning |
|---|
PAGE_BUCKET | Cloudflare | The R2 bucket binding. Created automatically by the button |
S3_ENDPOINT | Docker | Endpoint of the S3-compatible storage. If unset on Docker, the bundled SeaweedFS is used |
S3_BUCKET | Docker | Bucket name. Defaults to traypage-pages |
S3_ACCESS_KEY_ID | Docker | Access key ID |
S3_SECRET_ACCESS_KEY | Docker | Secret access key |
S3_REGION | Docker | Region. The AWS region for AWS S3, or auto for R2 |
S3_ADDRESSING_STYLE | Docker | path (default) or virtual |
On Cloudflare, PAGE_BUCKET is used when present. On Docker, S3-compatible storage is used when the S3_* values are complete.
| Name | Applies to | Meaning |
|---|
EMAIL_FROM | Both | The sender (for example TrayPage <noreply@example.com>) |
RESEND_API_KEY | Both | For compatibility with older versions. Email is sent through Resend only when this is set, and it takes priority over the EMAIL binding and SMTP |
SMTP_HOST | Docker | SMTP server host name. Used when RESEND_API_KEY is not set (the default on Docker) |
SMTP_PORT | Docker | Port. If unset, 465 when SMTP_SECURE is enabled, otherwise 587 |
SMTP_SECURE | Docker | true uses TLS from the start (port 465); otherwise STARTTLS |
SMTP_USER / SMTP_PASSWORD | Docker | SMTP credentials (omit if no authentication is needed) |
EMAIL | Cloudflare | The Cloudflare Email Service binding. Included in the template. When present, email is sent through it (the default on Cloudflare; the sender domain must be registered) |
MAILPIT_URL | Development | When no sender is configured, email is sent to this Mailpit API (default http://localhost:8025). The bundled compose setup sends to Mailpit over SMTP by default |
| Name | Meaning |
|---|
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | The OAuth client for Google sign-in. The redirect URI is <APP_ORIGIN>/api/auth/callback/google |
NEXT_PUBLIC_GOOGLE_AUTH | Set to 1 to show Google sign-in on the sign-in screen |
Authentication actions, such as sign-in (sending and opening the email link, passwords) and connecting AI agents (OAuth), are always limited per client IP address. Over the limit, requests get "Too many requests" (HTTP 429) for a while, usually about a minute. Counts are stored in the database, so they add up across multiple servers. It is on without any configuration.
| Name | Applies to | Meaning |
|---|
TRUSTED_CLIENT_IP_HEADER | Docker | The header to read the client IP address from. The bundled compose setup (behind Caddy) sets x-forwarded-for, and so does the AWS template. For headers that are appended to, such as X-Forwarded-For, the last value is used. If unset, the address connecting to the app is used. Not needed on Cloudflare (the client IP that Cloudflare adds is used) |
- If you put another load balancer or CDN in front of Caddy, everyone is counted as the same IP (the one in front) and hits the limit together. In that case, set
trusted_proxies in the Caddyfile so Caddy keeps the client IP passed on by the proxy in front (see "認証のレート制限" (auth rate limit) in deploy/docker/README.md).
- If you expose the app directly without a proxy, do not set
TRUSTED_CLIENT_IP_HEADER. Clients can write any header they like, which would let them get around the limit.
| Name | Meaning |
|---|
PORT | The port the app container listens on. Defaults to 8080. In compose it is not exposed; only Caddy connects to it |
HOST | The address to listen on. Defaults to 0.0.0.0 |
| Name | Meaning |
|---|
COMPOSE_PROFILES | Which services to start: postgres (bundled Postgres), s3 (bundled SeaweedFS), and mailpit (a local mail catcher), comma-separated |
TRAYPAGE_IMAGE | The image to use. Defaults to ghcr.io/8d-inc/tray-page:latest. In production, pin a version such as :v1.2.3 |
HTTP_PORT / HTTPS_PORT | Ports Caddy listens on. 80 and 443 in production; locally, the same port written in APP_ORIGIN |
MAILPIT_UI_PORT | Port of the Mailpit UI. Defaults to 8025 (exposed only on your own machine) |
| Name | Meaning |
|---|
HYPERDRIVE | Connection to the database (required) |
PAGE_BUCKET | R2 bucket for page content (required) |
EMAIL | Email Service, for sending email |
BROWSER | Browser Run, for faithful PDF export (optional) |
REVIEW_SESSIONS | Durable Objects, for real-time review notifications (optional) |
ASSETS | Static files for the UI (set in the template; no change needed) |
The following are for operating tray.page (hosted), development, or testing. Do not set them on a self-hosted instance.
| Name | Purpose |
|---|
E2E_LOGIN_SECRET | Login for automated tests. Always disabled when ENVIRONMENT=production |
AUTH_RATE_LIMIT | off removes the sign-in rate limit (for development and automated tests). Ignored when ENVIRONMENT=production |
OPERATOR_EMAILS, OPERATOR_SLACK_WEBHOOK_URL, DEFAULT_TRIAL_DAYS | Operator features of the hosted service. Disabled in the self-hosted edition |
SUPPORT_EMAIL, SUPPORT_EMAILS | Contact addresses for the hosted service's support form |
STORAGE_DIR | A local folder used when no storage is configured at all (for tests) |
BETTER_AUTH_URL | On Docker this is set from APP_ORIGIN automatically |
NEXT_PUBLIC_APP_URL | An old setting name. Use APP_ORIGIN instead |