Why run it yourself?
Most teams pass credentials around in chat. A contractor needs the staging database password, someone pastes it into Slack, and there it stays. It's in the history, in the backups and in search, long after the contractor has left. A one-time link fixes that part. The person opens it once and it's gone.
Hosting it yourself covers the rest. The links are on your domain, the encrypted data sits on a machine you control, and your security review is about code you can actually read. It's the same code that runs 1time.io, and it's MIT licensed.
Your own server can't read it either
Self-hosting doesn't mean whoever runs the box gets to see the secrets. Everything is encrypted
in the sender's browser before it's uploaded, with AES-256-GCM and a key derived using
HKDF-SHA256. The key lives in the part of the link after the #, and browsers don't
send that part to the server.
What the server keeps is the ciphertext and a SHA-256 hash of a read token. It never sees the token itself. Stored data is useless without the link: someone poking around in Redis can't open a secret, and neither can a leaked backup or a database dump. The admin does control the code the server sends, so run it on infrastructure you trust, from the published images or source you've reviewed.
What's in it
Text secrets open once by default. You can allow up to 10 views and pick an expiry anywhere from an hour to 30 days. Files can be up to 250 MB. The filename and type are encrypted along with the contents, and the upload goes in 4 MiB pieces, so a flaky connection only costs you the piece that failed.
You can add a passphrase to a link and send it some other way, for things that shouldn't open from the link alone. There's a password generator that turns what it makes straight into a link, and a My Secrets page where the sender can check whether a link has been opened yet.
The self-hosted build is only that. There's no blog, no marketing pages and no analytics.
Deploying it
On any machine with Docker:
curl -O https://raw.githubusercontent.com/shingrus/1time.io/master/docker-compose.yml
curl -O https://raw.githubusercontent.com/shingrus/1time.io/master/.env.example
cp .env.example .env
docker compose up -d
You get three containers: Redis, the Go API, and nginx with the static frontend. There are images
for amd64 and arm64, and the instance answers on http://localhost:8080. You don't
set a hostname anywhere. Links are built from whatever address people use to open it.
HTTPS
Use the reverse proxy you already run. If that's Caddy on the same machine, this is the entire config:
secrets.example.com {
reverse_proxy localhost:8080
}
The container already sends a Content Security Policy, X-Frame-Options and a strict
Referrer-Policy, and the secret and file pages send no referrer at all. HSTS is the
one header it leaves out, on purpose. Set it on your proxy, where you know which subdomains it
should cover.
Rate limits behind your proxy
The API is rate-limited per visitor. Behind a proxy that only works if nginx trusts the visitor address the proxy passes along, and by default it trusts it only from the container's own Docker network. A Caddy or Traefik container in the same compose file counts, and so does a proxy running on the host. What doesn't work is someone on your office network putting a fake IP in a header to get past the limits.
Everything is set in .env. Change a value and restart, no rebuild.
| Variable | Default | What it does |
|---|---|---|
TRUSTED_PROXIES | container network | Your proxy's address or CIDR, if it runs on another machine |
REAL_IP_HEADER | X-Forwarded-For | Use CF-Connecting-IP behind Cloudflare |
RATE_LIMIT | on | off turns all limits off, handy for a small internal instance |
RATE_LIMIT_WRITE | 45r/m | Text secrets created per visitor |
RATE_LIMIT_UPLOAD | 120r/m | File pieces per visitor (a 250 MB file is 63) |
RATE_LIMIT_READ | 60r/m | Secrets and files opened per visitor |
The CLI and the browser extension
The 1time CLI encrypts on your machine and takes
--host, so a script or a CI job can hand out links from your server:
printf '%s' "$DB_PASSWORD" | npx @1time/cli send --host https://secrets.example.com The browser extension has a target server setting. Point it at your instance, and when you select text and right-click, the link it copies is on your domain.
Self-hosted alternatives: OneTimeSecret, Yopass, PrivateBin, Password Pusher
OneTimeSecret, Yopass, PrivateBin and Password Pusher all run in Docker too. Where they differ is whether the server can see your data, and what else comes with it.
| Feature | 1time | OneTimeSecret | Yopass | PrivateBin | Password Pusher |
|---|---|---|---|---|---|
| Zero-knowledge (E2E encrypted) | Yes | No | Yes | Yes | No |
| Self-destructs after first read | Yes | Yes | Yes | Optional | Optional |
| Self-hosted with Docker Compose | Yes | Yes | Yes | Yes | Yes |
| First-party CLI | Yes | Yes | Yes | No | Yes |
| Built-in password generator | Yes | Yes | No | No | Yes |
| Stack | Go + static HTML | Ruby | Go | PHP | Ruby |
| License | MIT | MIT | Apache-2.0 | zlib | Apache-2.0 |
If you want more detail, there are longer write-ups comparing it with OneTimeSecret and Password Pusher.
Questions people ask
Does a self-hosted instance send anything to 1time.io?
No. The pages load nothing from other hosts. No analytics, no CDN fonts or images, no tracking pixel. There are plain links to 1time.io in the footer and on the view page, and to GitHub in the header, but nothing is fetched unless someone clicks one.
What do we need to run it?
Docker with Docker Compose. It runs as three containers: Redis, the Go API, and nginx serving the frontend. Images exist for amd64 and arm64. You'll want your own reverse proxy in front for HTTPS.
Which domain do the links use?
Yours. A link is built from the address people open the instance at, so a secret made on secrets.example.com gets a secrets.example.com link. There's no hostname setting.
Can we turn the rate limits off for an internal instance?
Yes, set RATE_LIMIT=off in .env and restart. If you'd rather just loosen them, change RATE_LIMIT_WRITE, RATE_LIMIT_UPLOAD or RATE_LIMIT_READ instead.
Is it free?
Yes. It's open source under the MIT license, encryption protocol included. There's no paid tier and no license key.
Getting started
The source, the compose file and the full list of settings are on GitHub. If you'd like to see it work first, make a one-time link on 1time.io. A self-hosted instance behaves exactly the same way.