For the complete documentation index, see llms.txt. This page is also available as Markdown.

Deploy the Tempest Server with Docker Compose

A complete, tested Docker Compose deployment of the open-source Tempest server image — Postgres, Redis, CouchDB, S3 storage, and the sign-in flow a real Tempest client uses.

Tempest Server ships as a Docker image. The open-source core is published on Docker Hub as tempestterm/tempest-web-services-oss, and this page is the deployment that goes with it: one docker-compose.yml, one script that generates every secret, and a checklist that proves the install works before you point a client at it.

Everything below was run end to end on a plain Ubuntu 24.04 box with Docker 29. Nothing here is a sketch.

This is the open-source build. For what it does and does not include — shared vaults, two-factor authentication, relays, the AI assistant, the full administration console — see OSS vs Enterprise Edition.

What the stack is made of

Service
Image
Why it is there

web

tempestterm/tempest-web-services-oss

The Rails app: sign-in, Console, OAuth/OIDC, the gateway API

worker

same image

Sidekiq — scheduled snippet runs, mail, push relay

postgres

postgres:17-alpine

Accounts, teams, devices, OAuth grants

redis

redis:7-alpine

Sidekiq queues, Action Cable, cache

couchdb

couchdb:3.5.2

The encrypted vaults themselves — clients replicate against it directly

rustfs

rustfs/rustfs

S3-compatible object storage for Active Storage (avatars, uploads)

One thing about the shape of it is worth knowing before you start.

CouchDB is not behind the app. Clients replicate straight against it, so its URL has to be reachable from every device, not just from the web container. Plan for two published endpoints, not one.

Where configuration comes from

Tempest reads its configuration from three places. Most values accept more than one, so you can put a secret wherever it belongs in your setup rather than wherever the app happens to insist.

1. Mounted encrypted credentials

config/credentials/production.yml.enc, unlocked at boot by RAILS_MASTER_KEY. Mount the file, pass the key, and Rails decrypts it in memory — the plaintext never touches disk.

Credential
What it is

jwt_privkey

Required. Generated at install time in step 2, and paired with CouchDB there. No environment equivalent — it must come from credentials.

oidc.signing_key

RSA key for ID Tokens. Also settable as OIDC_SIGNING_KEY.

sign_in_with_apple_pem

Apple sign-in private key (Enterprise Edition)

google_oauth.client_id / .client_secret

Google sign-in (Enterprise Edition)

github_oauth.client_id / .client_secret

GitHub sign-in (Enterprise Edition)

push.apns.*, push.fcm.*

APNs / FCM credentials (Enterprise Edition)

Back up RAILS_MASTER_KEY and the encrypted file together. Without the key the file is unreadable, and without jwt_privkey the instance loses access to every vault database it has.

2. Environment variables

Some values have to arrive this way, because they are what the app needs in order to reach a database at all:

Variable
Notes

DATABASE_URL

Postgres connection URL

REDIS_URL

Sidekiq, Action Cable and the cache

SECRET_KEY_BASE

Rails message signing

RAILS_MASTER_KEY

Unlocks the credentials file above

TEMPEST_EXTERNAL_URL

The canonical browser-facing origin. Environment only.

TEMPEST_FORCE_HTTPS

HTTP → HTTPS redirect, off by default

OIDC_SIGNING_KEY, OIDC_ISSUER

Wins over credentials.oidc.signing_key

AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_BUCKET, AWS_ENDPOINT, AWS_FORCE_PATH_STYLE

Active Storage

PORT, WEB_CONCURRENCY, RAILS_MAX_THREADS, DB_POOL, SIDEKIQ_CONCURRENCY, RAILS_LOG_LEVEL

Process sizing and logging

AVO_LICENSE_KEY

Administration console (Enterprise Edition)

And every backend setting from the next section is also an environment variable, spelled by uppercasing the key and turning non-alphanumerics into _: couchdb_host → COUCHDB_HOST, smtp_address → SMTP_ADDRESS, skip_registration_confirmation → SKIP_REGISTRATION_CONFIRMATION.

3. The database

Everything in the backend_settings table is a key / value_text row, and can be set there instead of in the environment. The point of this channel is that it takes effect without a redeploy — the value is resolved per request, so you can flip a setting on a running instance.

Setting
ENV spelling
What it does

couchdb_host

COUCHDB_HOST

Required. The CouchDB URL handed to clients

admin_emails

ADMIN_EMAILS

Bootstrap instance admins, comma- or space-separated

mail_from

MAIL_FROM

Sender for app and Devise mail; default Tempest <account@gotempest.app>

smtp_address

SMTP_ADDRESS

SMTP server hostname; required when sending mail in production

smtp_port

SMTP_PORT

Default 587, or 465 when SMTP_TLS=true

smtp_username, smtp_password

SMTP_USERNAME, SMTP_PASSWORD

SMTP credentials, if required by your provider

smtp_authentication

SMTP_AUTHENTICATION

Provider's authentication method, such as plain or login; unset by default

smtp_domain

SMTP_DOMAIN

Optional HELO/EHLO domain

smtp_tls

SMTP_TLS

Implicit TLS; default false

smtp_enable_starttls_auto

SMTP_ENABLE_STARTTLS_AUTO

STARTTLS; default true, automatically disabled when SMTP_TLS=true

smtp_openssl_verify_mode

SMTP_OPENSSL_VERIFY_MODE

Optional TLS certificate verification mode

skip_registration_confirmation

SKIP_REGISTRATION_CONFIRMATION

Let sign-ups in without confirming their address

skip_invitation_confirmation

SKIP_INVITATION_CONFIRMATION

Same, for invitees (EE)

download_bucket_url

DOWNLOAD_BUCKET_URL

Serve /download from your own bucket

min_client_version

MIN_CLIENT_VERSION

Refuse clients older than this

disable_personal_team

DISABLE_PERSONAL_TEAM

Turn off personal vaults instance-wide

default_shared_team_id

DEFAULT_SHARED_TEAM_ID

Auto-join new accounts to a team (EE)

couchdb_conflict_cleanup

COUCHDB_CONFLICT_CLEANUP

Background conflict sweep

argon2_memory_kib, argon2_iterations, argon2_parallelism

ARGON2_MEMORY_KIB, …

Master-password KDF cost

apns_sandbox

APNS_SANDBOX

APNs environment (EE)

turnstile_site_key, turnstile_secret_key

TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY

Bot challenge on the sign-in forms (EE)

openai_api_key, openai_base_url, ai_codegen_model, ai_max_tokens, …

OPENAI_API_KEY, …

AI assistant (EE)

license_status_api

LICENSE_STATUS_API

License check endpoint (EE)

Enterprise Edition exposes these in its administration console. On an open-source build, write them from a console:

Precedence

For anything that BackendSettings resolves, the order is fixed:

  1. The environment variable, if it is set and not empty

  2. The backend_settings row

  3. The built-in default — and where there is none, boot fails loudly with Backend setting missing: <key> (ENV <KEY> or database) rather than running on a guess

An empty environment variable is treated as absent, so SMTP_ADDRESS= falls through to the database rather than blanking it. The flip side is the one that bites: an exported variable outranks the database row and your .env file, so a stale export in your shell will quietly win over the value you just edited.

Outbound mail (SMTP)

Resend API delivery has been replaced by generic SMTP. Remove RESEND_API_KEY from an existing deployment and configure your mail provider's SMTP endpoint and credentials. Use a sender address your provider authorizes. For a typical authenticated STARTTLS connection, add this to .env:

For implicit TLS, set SMTP_TLS=true and SMTP_PORT=465; STARTTLS is then disabled automatically. Match the port and authentication method to your provider. Leave username, password and authentication empty if your SMTP relay does not require them.

The Compose configuration below passes these settings to both web and worker, since background jobs send mail too. After editing .env, recreate both containers with docker compose up -d web worker. Database settings are read per message and can change without a restart, provided no non-empty environment variable overrides them.

SMTP_ADDRESS is checked when a message is built, not at boot: the app can start without it, but production mail fails with Backend setting smtp_address is missing. Configure SMTP before registering accounts so confirmation and password-reset emails can be delivered. SKIP_REGISTRATION_CONFIRMATION=true bypasses sign-up confirmation only; other email still requires SMTP.

1. Lay out the directory

2. Generate the credentials and the CouchDB keypair

Tempest and CouchDB are paired by one EC keypair created at install time: the private half goes into the encrypted credentials, the public half into CouchDB's config in step 3. Generate it inside the image, so the credentials file is written by the same Rails version that will read it.

Save this as inner-gen.sh:

Run it. launcher is the Cloud Native Buildpacks entrypoint — it is how you run any command other than the image's default process:

production.key is your RAILS_MASTER_KEY. Back it up. Lose it and every vault key escrow in credentials.yml.enc is gone for good.

Vault history retention

If you want to retain earlier vault revisions, disable CouchDB automatic database compaction on every node. Tempest's History reads retained revision bodies; automatic or manual compaction can remove them. Add an empty db_channels under [smoosh] to your persisted couchdb/local.ini, restart CouchDB and verify the effective setting. Monitor disk growth and maintain database backups. See Vault History — Self-hosting for the configuration and limits. Tempest Cloud does not guarantee history retention.

3. Generate couchdb/local.ini and .env

Save this as gen-config.sh and set HOST_IP to the address clients will use.

Every value is generated per install. Nothing in this file should ever be copied from someone else's deployment.

Three of those deserve a note:

  • TEMPEST_EXTERNAL_URL — the canonical browser-facing origin. Scheme required, no path. It drives absolute URLs, the OIDC issuer, WebAuthn, Action Cable origins and the Secure cookie flag. Add TEMPEST_FORCE_HTTPS=true only when it is https:// and your proxy sends an accurate X-Forwarded-Proto.

  • COUCHDB_HOST — the URL clients will use, so an internal http://couchdb:5984 will not do.

  • ADMIN_EMAILS — the bootstrap instance admin. A fresh instance has nobody who can grant admin to anyone, so this is how an installation names its own. Comma-separated.

Both COUCHDB_HOST and ADMIN_EMAILS are backend settings, so you can move them into the database later and change them without a restart.

Then run it:

4. docker-compose.yml

Two details that are not cosmetic:

  • entrypoint: selects the process type. The image is built by Cloud Native Buildpacks, so /cnb/process/web and /cnb/process/worker come from the Procfile. A command: override would be passed to the web process instead of replacing it.

  • Use named volumes, not ./data bind mounts. CouchDB and RustFS run as non-root and cannot write into a host directory your user owns; RustFS fails with a bare [FATAL] Server runtime failed: Io error: Permission denied (os error 13).

5. Start it and migrate

Use db:migrate, not db:prepare. On an empty database db:prepare also runs the seed step, which expects Enterprise Edition to be present and aborts on an open-source build. db:migrate does exactly what a fresh install needs and finishes cleanly.

6. Register the OAuth clients

This step is required, and it is the one people miss. A fresh instance has no OAuth clients registered, and every Tempest client has its client_id compiled in. Until you register them, sign-in fails before it begins.

Save as tools/oauth_apps.rb:

It is idempotent, and it prints one line per client:

7. Create the storage bucket

Active Storage will not create its own bucket. Use the app's own S3 client rather than a second CLI — same credentials, same endpoint, nothing else to install. Save as tools/create_bucket.rb:

8. Verify before you point a client at it

Six checks. Each one fails loudly on its own, and together they cover every moving part.

Then create an account at $BASE/users/sign_up, verify the confirmation email arrives and follow its link (unless you enabled SKIP_REGISTRATION_CONFIRMATION), sign in, and confirm /console returns 200. If you set ADMIN_EMAILS to that address, the background-job dashboard at /admin/sidekiq returns 200 too.

Finally, walk the real client flow: OAuth 2.0 authorization code + PKCE, then call /info with the token. A working instance answers:

additional_vaults is always empty in an open-source build — shared vaults are an Enterprise Edition feature.

Re-run check 3 afterwards: _all_dbs now lists a vault database that was not there before. That is the end-to-end proof — the app reached CouchDB, was accepted, and provisioned storage for the account. If it is still empty, the pairing from steps 2 and 3 is wrong; see the troubleshooting table.

9. Put TLS in front of it

Plain HTTP is fine on a trusted LAN and nowhere else: Apple sign-in and WebAuthn passkeys both require HTTPS on a non-localhost origin. Terminate TLS at a reverse proxy — Caddy, nginx, or Nginx Proxy Manager — and give it two names: one for the app on 51415, one for CouchDB on 5984. Both must be publicly resolvable, because clients talk to CouchDB directly.

The proxy has to preserve the original host and protocol:

Keep TEMPEST_EXTERNAL_URL=https://… even though the proxy-to-Rails hop is plain HTTP. Set TEMPEST_FORCE_HTTPS=true only once the header above is genuinely arriving — otherwise Rails redirect-loops.

10. Point a client at it

On the Tempest welcome screen, choose Self-hosted server, enter your TEMPEST_EXTERNAL_URL, and sign in. From there it is the same product: Web Mode in the browser, encrypted vault sync, scheduled snippets, push notifications.

Day-two operations

Revoking an admin means revoking it in both places: an address left in ADMIN_EMAILS stays an admin no matter what the database column says.

Troubleshooting

Symptom
Cause

couchdb exits 1 with no log output at all

local.ini is not owned by uid 5984 / not mode 0644. Its entrypoint's chown/chmod sweep fails on the read-only mount and set -e kills it before logging starts.

{database_does_not_exist,<<"_users">>} crash loop

single_node = true missing from [couchdb].

CouchDB returns missing json key: _couchdb

roles_claim_path is set in [jwt_auth]. Remove it.

CouchDB returns 401 to the app

local.ini does not carry the public key generated in step 2. Re-run gen-config.sh and restart CouchDB.

rustfs restarts with Permission denied (os error 13)

You bind-mounted a host directory instead of using a named volume.

db:prepare aborts on a missing constant

Use db:migrate instead.

Mail fails with Backend setting smtp_address is missing

Set SMTP_ADDRESS and your provider's SMTP settings for both web and worker, or configure the equivalent database settings.

Client sign-in fails immediately

The OAuth applications from step 6 were never created.

Uploads 500 with NoSuchBucket

Step 7 was skipped.

.env edits do not take effect

A variable of the same name is exported in your shell — a real environment variable outranks .env in Compose. unset it.

See also

Last updated