Перейти до вмісту

Install with Docker

Цей вміст ще не доступний вашою мовою.

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.

For a new installation, copy the template only if .env does not already exist, then restrict access to it:

Terminal window
(umask 077; test -e .env || cp .env.example .env)
chmod 600 .env

Open .env in your editor. The template already contains blank assignments such as SECRET= and ACCOUNT_ID_SECRET=; fill those lines rather than appending duplicate keys. Add VALKEY_PASSWORD if it is absent.

Generate a separate value for each missing or blank secret with:

Terminal window
openssl rand -hex 32

For the simple stack, set SECRET and VALKEY_PASSWORD. For the full stack, also set RABBITMQ_USER (for example, ots), RABBITMQ_PASS, AUTH_SECRET, ACCOUNT_ID_SECRET and ARGON2_SECRET. Use a fresh generated value for each secret. Set ARGON2_SECRET before creating passwords and keep it: changing it prevents existing passwords from verifying. See Secrets and keys.

Before starting, check that each required key has exactly one assignment and a non-empty value. Keep .env out of version control and back it up securely.

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.

For a new installation, create the root key once. The subshell sets private permissions and refuses to overwrite an existing file:

Terminal window
(umask 077; set -C; openssl rand -hex 32 > .ots_secret)

If the file already exists, keep it. Back it up securely; if it is empty or lost after the instance has been used, restore the original key before starting.

Use the same file every time you create the container. This startup block refuses a missing or empty key file:

Terminal window
if [ -s .ots_secret ]; then
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
else
printf '%s\n' 'Missing or empty .ots_secret; restore the original key before starting.' >&2
false
fi

--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.