Passer au contenu

Vous consultez le site de documentation en préversionCe contenu est destiné aux tests et peut différer de la documentation de production.

Accéder à la documentation de production

Install with Docker

Ce contenu n'est pas encore disponible dans votre langue.

Onetime Secret ships two Docker Compose stacks, and both live in the application repository alongside the code they deploy. The separate onetimesecret/docker-compose repository is archived as of v0.24 — anything you find there is older than what is described here, so do not clone it.

Choose a release from the releases page and set it once in your shell:

Terminal window
export OTS_VERSION=vX.Y.Z # a release tag from the link above
export OTS_IMAGE_TAG=$OTS_VERSION # what the Compose files read

The Compose files read OTS_IMAGE_TAG and already default to a pinned release, so skipping this still gives you a reproducible stack. For which image to run, what the variants are and why the tag you pin matters, see Images and variants.

The stacks are files in that repository, so you need a checkout of it. Use the release you just picked, so the Compose files and the image they start come from the same one:

Terminal window
git clone https://github.com/onetimesecret/onetimesecret.git
cd onetimesecret
git checkout "$OTS_VERSION"

Every command below runs from that directory. The Compose files carry their own pinned default image tag, which is not necessarily the release you just checked out; exporting OTS_IMAGE_TAG=$OTS_VERSION above is what makes the two agree.

The simple stack is the application plus Valkey — two containers, one published port, everything persisted in Valkey. The full stack adds a Caddy TLS proxy in front, RabbitMQ, an email worker and a scheduler.

The root docker-compose.yml is an include wrapper and includes the simple stack. Switch by editing that include, or skip the wrapper and point Compose at a stack file directly. The two routes are not equivalent — the second one moves the Compose project directory, which changes where your settings come from. Both are spelled out below. These are not Compose profiles; there is no --profile flag involved.

What must be set before either stack starts

Section titled “What must be set before either stack starts”

SECRET and VALKEY_PASSWORD have no defaults. Both are interpolated with Compose’s :? form, so docker compose up aborts with an error rather than starting a half-configured instance. SECRET is the root secret and the HKDF input for the derived keys, so back it up. The full stack additionally requires RABBITMQ_USER and RABBITMQ_PASS, also with no defaults, on the application, both workers and the broker.

The full stack needs two more that Compose will not stop you on. AUTH_SECRET and ACCOUNT_ID_SECRET are interpolated with an empty default instead of the fail-fast form, so Compose starts the containers happily and the application then dies at boot, with nothing at the Compose level to explain it. The full stack runs with RACK_ENV set to production, and an unset RACK_ENV falls back to production as well; in production the authentication layer refuses to boot without both, and ACCOUNT_ID_SECRET must be at least 32 bytes. They are independent secrets — neither is derived from SECRET, neither can be regenerated from it, and rotating one does not invalidate the other. Back them up alongside SECRET.

The application repository’s own quick start copies .env.example and appends only SECRET. That sequence does not work — .env.example carries no VALKEY_PASSWORD and both stacks abort on it. Run this instead:

Terminal window
[ -f .env ] || cp .env.example .env
grep -q '^SECRET=' .env || echo "SECRET=$(openssl rand -hex 32)" >> .env
grep -q '^VALKEY_PASSWORD=' .env || echo "VALKEY_PASSWORD=$(openssl rand -hex 32)" >> .env

For the full stack, add the four it needs on top of those:

Terminal window
grep -q '^RABBITMQ_USER=' .env || echo "RABBITMQ_USER=ots" >> .env
grep -q '^RABBITMQ_PASS=' .env || echo "RABBITMQ_PASS=$(openssl rand -hex 16)" >> .env
grep -q '^AUTH_SECRET=' .env || echo "AUTH_SECRET=$(openssl rand -hex 32)" >> .env
grep -q '^ACCOUNT_ID_SECRET=' .env || echo "ACCOUNT_ID_SECRET=$(openssl rand -hex 32)" >> .env

Every line above is guarded, so the block is safe to rerun against an existing installation. Without the guards, a second run appends a new SECRET — and because Compose and dotenv take the last assignment of a duplicated key, the application would come up on a different root secret with every stored secret encrypted under the old one and no longer readable. The same applies to AUTH_SECRET and ACCOUNT_ID_SECRET: rotating them invalidates existing sessions and account-id derivation. If you need to rotate a value deliberately, edit the existing line in .env rather than appending a second one, and treat it as the destructive operation it is.

Both stacks declare env_file: ../../.env, so a repository-root .env has to exist even when you supply every value through the shell. There is a second trap in the same area. Compose fills ${...} from the shell environment and from the .env sitting in the project directory — never from a service’s env_file, which only feeds the container at runtime. Pointing Compose at -f docker/compose/<stack>.yml moves the project directory to docker/compose/, so the repository-root .env stops feeding interpolation and every interpolated value has to reach Compose through the shell instead. That is SECRET and VALKEY_PASSWORD for the simple stack, and those plus RABBITMQ_USER, RABBITMQ_PASS, AUTH_SECRET and ACCOUNT_ID_SECRET for the full one — the same six the project’s own CI exports before it brings the full stack up.

Neither stack sets HOST or SSL in its environment: block, so unlike the values above, those two reach the application only through .env. What they control, and how getting SSL wrong silently breaks sessions, is in Reverse proxy and TLS.

Simple stack, from the repository root:

Terminal window
docker compose up -d --wait

Full stack through the wrapper: edit the include: in docker-compose.yml to point at docker/compose/docker-compose.full.yml, then run the same command. The project directory stays at the repository root, so the .env you just wrote feeds interpolation as well as the containers.

Full stack without editing the wrapper: put .env into the shell first, so nothing depends on where the project directory landed.

Terminal window
set -a; . ./.env; set +a
docker compose -f docker/compose/docker-compose.full.yml up -d --wait

The simple stack publishes one port, 3000:3000. Valkey is expose-only and is not reachable from the host at all; debug it from inside the network with docker compose exec maindb valkey-cli, which already has REDISCLI_AUTH set.

The full stack publishes only Caddy’s 80 and 443. The application, Valkey and RabbitMQ are all expose-only on the internal onetime-network bridge.

Valkey data — stored secrets and sessions — lives in the onetime_maindb_data named volume in both stacks. The shipped Valkey command turns AOF persistence on with appendfsync everysec, so the ciphertext of live secrets is written to disk. Install on Linux covers that durability choice and how to change it; the Compose stacks set the same knobs as valkey-server flags on the maindb service rather than in a config file.

The full stack keeps its account database as a SQLite file under /app/data, on the onetime_app_data named volume mounted by the application, the email worker and the scheduler. The simple stack has no /app/data mount. Simple or Full covers what full mode adds and why it needs a database of its own.

If you replace the named volume with a host directory, on Linux make it writable by the container user first — the container runs as uid 1001:

Terminal window
mkdir -p data && sudo chown -R 1001:1001 data

The full stack adds two services the simple stack does not have: worker-email, which consumes the email.message.send queue on RabbitMQ, and scheduler. Both read JOBS_ENABLED, which the Compose files default to false, so the full stack works without touching it. Run as a service covers what the three processes do and what changes once jobs are on.

To put the application in front of a datastore you already have, a plain docker run works. The form below is the one the project’s README documents, and CI exercises it against the published image:

Terminal window
openssl rand -hex 32 > .ots_secret && chmod 600 .ots_secret
docker run -p 3000:3000 -d \
--name onetimesecret \
--add-host=host.docker.internal:host-gateway \
-e REDIS_URL=redis://host.docker.internal:6379/0 \
-e SECRET="$(cat .ots_secret)" \
-e HOST=localhost:3000 \
-e SSL=false \
onetimesecret/onetimesecret:$OTS_VERSION

--add-host=host.docker.internal:host-gateway is not optional on Linux. Without it host.docker.internal does not resolve and the container cannot reach a datastore running on the host. Point REDIS_URL at wherever your Valkey or Redis actually listens; if that address is wrong or unreachable the application does not come up.

Writing the secret to a file first, rather than generating it inline, is deliberate — this is the value you have to keep.

docker compose up --wait blocks until the containers report healthy rather than returning as soon as they are running — a readiness signal, not merely “running”.

What it does not tell you is that the UI is serving its built assets, or that a secret can be created and read back exactly once. Verify an install checks both, and it is also where the first account gets created. For the systemd equivalent of all of this, and for the web, worker and scheduler process model, Run as a service; Self-Hosting Overview is the map of everything else an operator needs.