Install on Linux
Este contenido aún no está disponible en su idioma.
This page installs Onetime Secret from a source checkout onto a Debian or Ubuntu host. The project’s clean-room install tests run against Debian base images only, so other distribution families are not covered by them, and the package and service names below are Debian’s.
System packages
Section titled “System packages”Install the toolchain and the build headers first, as root:
sudo apt updatesudo apt install -y --no-install-recommends \ build-essential libssl-dev libffi-dev libyaml-dev libsqlite3-dev \ libpq-dev libsodium23 pkg-config git curl ca-certificates \ python3 procps redis-serverThat list is exactly what the project’s clean-room install lane puts into an
empty container before running the install, mirrored from the image build. It
covers the build headers the pg, sqlite3, argon2, bcrypt and puma gems
compile against, and it includes redis-server for the same reason a bare-metal
host needs one: the datastore has to be reachable locally.
Not all of it is build tooling. libsodium23 is a runtime shared library rather
than a build header — rbnacl binds to the shared object, so the application
fails to boot without it even though nothing needs its headers to compile — and
python3 is what the installer’s locale generation step shells out to.
That list also assumes a Ruby is already present, because the clean-room lane runs inside a Ruby base image. Building Ruby with rbenv, as this page does further down, needs ruby-build’s own prerequisites on top of it:
sudo apt install -y --no-install-recommends \ autoconf zlib1g-dev libreadline-dev libgdbm-devInstall these before you run rbenv install. A Ruby compiled without zlib
cannot run RubyGems, so gem install bundler fails on it.
Node and pnpm are separate. Node is checked by major version only, and the major is 22:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -sudo apt install -y nodejssudo npm install -g pnpmpackage.json pins the pnpm version under packageManager. If pnpm --version
disagrees with that pin, install the pinned version instead.
The datastore
Section titled “The datastore”Onetime Secret stores its data in Valkey or Redis. The two are wire-compatible
and the valkey-* and redis-* binaries are interchangeable, so the
redis-server package installed above is a valid datastore; the shipped
systemd units order themselves after both redis-server.service and
valkey.service for that reason.
The repository ships a reference configuration at etc/examples/valkey.conf,
and it turns persistence on:
dbfilename onetime.rdbappendfilename onetime.aof
bind 127.0.0.1port 6379
save 157680000 1appendonly yesappendfsync everysecsave 157680000 1 is not a snapshot schedule — the interval is five years. It
is there so the datastore still writes an RDB file when it receives SHUTDOWN.
The durable copy is the append-only file, fsynced once a second.
Whether to keep that is a decision about your threat model, not a tuning
knob. With persistence on, the ciphertext of every live secret is written to
onetime.rdb and onetime.aof and stays on disk until the record expires and
the files are rewritten. That is what lets an instance survive a restart
without losing every unretrieved secret, and it is also what puts secret
material into everything that reads the disk afterwards — backups, volume
snapshots, a recovered disk image. To keep secrets in memory only, set
save "" and appendonly no instead and accept that a restart loses them all.
Either way, set a password and keep the listener on loopback. The example ships
requirepass commented out with a CHANGEME placeholder; give it a real value.
The URL that hands that password to the application lives in .env, which does
not exist yet — the installer generates it, and setting the password into it is
a step further down this page.
Take the settings from etc/examples/valkey.conf rather than copying the file
over the one the package installed — it is written as a standalone
configuration, including daemonize yes, which is not what a service-managed
datastore wants. Apply the persistence, bind and password settings to
/etc/redis/redis.conf, then start it, still as root:
sudo systemctl enable --now redis-serversudo systemctl restart redis-serverUnder Docker the same knobs are set as valkey-server flags on the datastore
service instead of a configuration file — see
Install with Docker.
Create the service account and fetch the code
Section titled “Create the service account and fetch the code”The systemd units the repository ships assume a dedicated onetime user and
group with the application at /var/lib/onetimesecret. Use that layout unless
you intend to edit the units.
sudo useradd --system --shell /bin/bash --home-dir /var/lib/onetimesecret onetimesudo git clone https://github.com/onetimesecret/onetimesecret.git /var/lib/onetimesecretsudo chown -R onetime:onetime /var/lib/onetimesecretEverything from here runs as that user, in a login shell — the same kind of shell the service will use:
cd /var/lib/onetimesecretsudo -u onetime -H bash -l
# Choose a release from https://github.com/onetimesecret/onetimesecret/releasesexport OTS_VERSION=vX.Y.Z
git checkout "$OTS_VERSION"$OTS_VERSION is a git tag here and the container image tag on the Docker path.
It is the same string in both places.
Ruby must match .ruby-version exactly — 3.4.10 — and not merely “3.4 or
newer”. Bundler enforces the exact version through the Gemfile, and the
installer refuses to proceed on any other patch level with
need exactly <version>. Read the number out of the checkout rather than
typing it, so a version bump cannot catch you out.
The distribution’s own Ruby packages are too old. Use a version manager; the installer’s own error message names rbenv and mise.
git clone https://github.com/rbenv/rbenv.git ~/.rbenvgit clone https://github.com/rbenv/ruby-build.git "$(~/.rbenv/bin/rbenv root)"/plugins/ruby-buildecho 'eval "$(~/.rbenv/bin/rbenv init - bash)"' >> ~/.profilesource ~/.profile
rbenv install "$(cat .ruby-version)"rbenv global "$(cat .ruby-version)"gem install bundlerThat line goes in ~/.profile, not ~/.bashrc, and the difference decides
whether the service can start. The shipped systemd units run the app through
/bin/bash -lc, and a bash login shell reads /etc/profile and then the first
of ~/.bash_profile, ~/.bash_login and ~/.profile — it never reads
~/.bashrc. useradd --system creates no home-directory files at all, so
nothing exists to bridge the two: the ~/.profile above is a new file, and it
is the one the service reads. Put rbenv anywhere else and the unit starts, finds
no bundle on PATH, and dies — see
Run as a service, which is where this decision
takes effect.
Run the installer
Section titled “Run the installer”bin/install is the front door for a bare-metal install. It is a thin wrapper
over bin/setup --init, and it is safe to re-run: when it finds an environment
that has already been initialized it reconciles instead, so it never
regenerates a live SECRET.
bin/installIn one pass it checks Ruby against .ruby-version and Node against
.node-version, installs the gems and the Node packages against the committed
lockfiles, seeds etc/config.yaml, etc/auth.yaml, etc/logging.yaml and
etc/puma.rb from the shipped templates, writes .env and generates the
secrets in it, sets that file to mode 600, and marks the environment as
installed. That last step boots the app with the .env it just generated, so it
succeeds only if the datastore accepts the credentials in that file — which is
the next section.
Two further steps look reasonable and undo work the installer just did. .env
is generated rather than copied, so copying .env.example over it afterwards
discards the generated SECRET and the 600 permissions. And the etc/* files
are already seeded from etc/defaults/ and etc/examples/, so there is
nothing to put in place by hand.
Gem and Node installs are frozen: the installer never rewrites Gemfile.lock or
pnpm-lock.yaml. If you ever run Bundler yourself, use the same form —
BUNDLE_FROZEN=true bundle install, with no --without flag.
If you are running full authentication mode against PostgreSQL, one step is
left to you: run
apps/web/auth/migrations/schemas/postgres/initialize_auth_db.sql as a
PostgreSQL superuser. SQLite, which is the default, needs nothing.
Point the app at the datastore
Section titled “Point the app at the datastore”The .env the installer just wrote starts life as a verbatim copy of
.env.example, whose datastore line carries no password:
REDIS_URL='redis://127.0.0.1:6379/0'. If you set requirepass on the
datastore, that line is now wrong, and nothing in the installer will fix it —
the secrets step rewrites SECRET and the keys derived from it, and leaves
every other line alone. Edit .env and give the URL the password:
REDIS_URL=redis://:your-password@127.0.0.1:6379/0Then run bin/install again. The second pass leaves the generated .env and
its SECRET alone — the env step skips a file that already exists, and the
secrets step re-derives the child keys from the SECRET it finds rather than
minting a new one — and it records the install mark, which the first pass could
not, because the boot it does to set that mark used the password-less URL and
was refused by the datastore.
REDIS_URL and VALKEY_URL are both read. A URL that is empty, or that still
contains the literal CHANGEME, fails the boot with a named error rather than
an obscure connection failure.
Build the frontend assets
Section titled “Build the frontend assets”The installer deliberately does not build the frontend. A production install has to do it:
pnpm run buildThe build writes to public/web/dist/, and the application serves that
directory itself at the /dist URL prefix. There is no separate static file
server to point at it, and a reverse proxy in front of the app should proxy
those requests like any other.
Skipping this step produces no obvious failure. The app boots, answers requests and reports itself healthy while serving a UI with no assets.
Start the app
Section titled “Start the app”The environment is loaded by sourcing .env with auto-export on. Every shipped
invocation — the Procfile, the systemd units, the installer’s own closing
instructions — uses this exact form:
set -a; source .env; set +abundle exec puma -C etc/puma.rbetc/puma.rb was seeded by the installer and is usable unmodified. Puma binds
plain HTTP on 0.0.0.0 at $PORT, 3000 unless you set it, and takes its
production settings from RACK_ENV, which the config treats as production
when it is unset.
Puma never terminates TLS. Put a reverse proxy in front of it before the instance is reachable from anywhere but localhost — see Reverse proxy and TLS.
That command holds the shell, and the instance dies with it. To keep it running across reboots, and for the Procfile form if you would rather not use systemd, use the units the repository ships: Run as a service. To confirm the install actually works rather than merely answering, Verify your install.