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
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.
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:
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.
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:
The environment variable, if it is set and not empty
The
backend_settingsrowThe 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 theSecurecookie flag. AddTEMPEST_FORCE_HTTPS=trueonly when it ishttps://and your proxy sends an accurateX-Forwarded-Proto.COUCHDB_HOST— the URL clients will use, so an internalhttp://couchdb:5984will 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/weband/cnb/process/workercome from theProcfile. Acommand:override would be passed to the web process instead of replacing it.Use named volumes, not
./databind 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
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
Self-Hosted Tempest Server & Web Mode — what the server does, and why you would run one
End-to-End Encryption — unchanged whether you self-host or not
Last updated