Production setup
Before your team uses oneloop, set up three things:
- HTTPS, with a reverse proxy such as Caddy or nginx in front of oneloop.
- A service that starts oneloop automatically and restarts it after a crash.
- Nightly backups, as described in Backups and upgrades.
Give oneloop at least 512 MB of memory. It uses about 50 MiB when idle, and up to about 200 MiB more while it decodes an image for a thumbnail or an avatar, one image at a time. Knowledge search keeps up to 64 MiB of indexes, and needs up to about 100 MiB more while it builds one. HTML previews prepare two pages at once, which takes about 50 MiB more for two 10 MiB pages.
A prepared HTML preview waits on disk while it is sent, in a temporary file in
the data folder that has no name, so it goes away when the response ends,
however it ends. It takes about the size of the file, and up to 4 times as
much for a page full of < characters, and under 1 MiB of memory. Each person
can have three attachment pages and three Knowledge pages being sent at once.
So HTML previews need free disk space, as uploads do: a preview that would
leave less than ONELOOP_DISK_MIN_FREE free
shows that the server is low on disk space instead.
HTTPS with a reverse proxy
oneloop serves plain HTTP and leaves certificates to the proxy. It accepts an
http:// address only for the server itself (see
ONELOOP_PUBLIC_URL), because passwords and
sign-in cookies must never cross a network unencrypted.
-
Point a DNS name, such as
tasks.example.com, to your server. -
Set these variables for oneloop, then restart it:
Variable Value ONELOOP_PUBLIC_URLhttps://tasks.example.com, the exact address people openONELOOP_TRUSTED_PROXIESThe address of the proxy; see Trusted proxy With Docker, change them under
environment:incompose.yaml, then rundocker compose up -d. -
Set up the proxy, as shown below for Caddy and nginx.
-
Open the new address and sign in again.
Keep oneloop itself reachable only at 127.0.0.1:8080, as the default settings
and the Docker example do. Then the proxy is the only way in.
Caddy
Caddy gets and renews certificates for you:
tasks.example.com {
reverse_proxy 127.0.0.1:8080
}
No public domain? On a local network, use a name such as tasks.home.arpa in
your local DNS, and add tls internal inside the block. Then install
Caddy’s local certificate authority
on each device.
nginx
This example is also in the repository as
deploy/nginx/oneloop.conf.example:
# oneloop behind nginx. Replace the domain and the certificate paths, include
# this file in the http context of nginx.conf, and run nginx -t before you
# reload. Guide: https://code19m.github.io/oneloop/production.html#nginx
#
# Also set "worker_shutdown_timeout 30s;" in the main context of nginx.conf, so
# that old workers don't keep live-update and MCP streams open for long after a
# reload. Browsers reconnect by themselves.
upstream oneloop_app {
# Keep the IPv4 address: a name such as localhost can resolve to an address
# that oneloop doesn't listen on.
server 127.0.0.1:8080;
keepalive 16;
# Let go of idle connections before oneloop does, even if
# ONELOOP_TRUSTED_PROXIES doesn't name nginx: then oneloop closes them
# after 15 seconds.
keepalive_timeout 10s;
}
server {
listen 80;
listen [::]:80;
server_name tasks.example.com;
return 301 https://$host$request_uri;
}
server {
# On nginx 1.25.1 or newer, use "listen 443 ssl;" and "http2 on;".
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name tasks.example.com;
ssl_certificate /etc/ssl/certs/tasks.example.com/fullchain.pem;
ssl_certificate_key /etc/ssl/private/tasks.example.com/privkey.pem;
# oneloop sends its own Content-Security-Policy and Strict-Transport-Security
# headers. Don't add, replace or hide them here.
# Attachments are up to 25 MiB.
client_max_body_size 27m;
proxy_http_version 1.1;
# Keep the external port, which oneloop checks against ONELOOP_PUBLIC_URL.
proxy_set_header Host $http_host;
# Replace any value the client sent. oneloop trusts it only from the
# addresses in ONELOOP_TRUSTED_PROXIES.
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Connection "";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# oneloop compresses the web app's files itself. Don't compress API
# responses; see the guide.
location / {
proxy_pass http://oneloop_app;
proxy_request_buffering off;
}
# Live updates: one long-lived stream for each open tab. Buffering delays them.
location = /api/events {
proxy_pass http://oneloop_app;
proxy_buffering off;
proxy_cache off;
}
# Optional: allow /healthz, which shows the version, only from your
# monitoring network. Signed-in users still see the version in the app.
# location = /healthz {
# allow 127.0.0.1;
# allow ::1;
# allow 192.0.2.0/24;
# deny all;
# proxy_pass http://oneloop_app;
# }
}
Trusted proxy
oneloop uses each visitor’s IP address to slow down repeated failed sign-ins,
and to show where people are signed in. Behind a proxy, all requests seem to
come from the proxy. ONELOOP_TRUSTED_PROXIES names your proxy, so oneloop can
read the visitor’s real address from the X-Forwarded-For header. Without it,
all visitors share one sign-in limit, and oneloop logs a warning at startup.
| oneloop runs | The proxy runs | Set ONELOOP_TRUSTED_PROXIES to |
|---|---|---|
| From source | On the same machine | 127.0.0.1 |
In Docker, with the example compose.yaml | On the host | The gateway of the Docker network, from the command below |
| Anywhere | On another machine | The IP address of that machine |
docker network inspect oneloop_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'
Separate several addresses or CIDR ranges with commas. List only real proxies,
never a network that visitors use. The proxy must replace X-Forwarded-For,
not add to it. oneloop ignores Forwarded, X-Real-IP and
X-Forwarded-Proto.
Naming the proxy also keeps its idle connections to oneloop open
longer than Caddy, nginx and Tailscale keep them. So the
proxy always closes an idle connection first. Otherwise oneloop could close a
connection just as the proxy sends a request on it, and that request would fail
with 502. Caddy needs no keepalive setting.
Rules for any proxy
- Serve oneloop at the root of its own host name. A path such as
/oneloop/doesn’t work. - Pass the
Hostheader unchanged, with the port if there is one. - Don’t add or change
Content-Security-Policyor other security headers. oneloop sets its own, including HSTS. - Use HTTP/2 to browsers. With HTTP/1.1, a browser opens only six connections to a site, and every open tab uses one for live updates.
- Leave compression to oneloop. It gzips the web app’s files itself. Don’t compress API responses: compressed sizes can give away secrets in a response, as the BREACH attack shows.
- Reach oneloop itself over HTTP/1.1. It doesn’t accept HTTP/2 without TLS (h2c).
- Don’t buffer or time out
/api/events. This live-update stream stays open as long as the tab. - Allow request bodies of at least 27 MB, for file uploads.
To test the proxy, open oneloop in two tabs and change a task in one. The change should appear in the other tab without reloading.
Start automatically
Docker
The example compose.yaml is already set up:
restart: unless-stoppedstarts oneloop again after a crash or a reboot, if Docker itself starts at boot.stop_grace_period: 35sgives oneloop time to finish open requests when it stops. Docker’s default of 10 seconds can cut off uploads.
With docker run, use --restart unless-stopped --stop-timeout 35.
systemd (Linux)
-
Create a system account and install the binary:
sudo useradd --system --home-dir /var/lib/oneloop --shell /usr/sbin/nologin oneloop sudo install -d -m 0700 -o oneloop -g oneloop /var/lib/oneloop sudo install ~/.cargo/bin/oneloop /usr/local/bin/oneloop -
Save the unit file as
/etc/systemd/system/oneloop.service. Set yourONELOOP_PUBLIC_URLand any other settings in it. The unit keeps data in/var/lib/oneloop/dataand uses systemd’s sandbox options. -
For a new instance, create the database and the first admin as the service account:
sudo -u oneloop env ONELOOP_DATA_DIR=/var/lib/oneloop/data /usr/local/bin/oneloop db migrate sudo -u oneloop env ONELOOP_DATA_DIR=/var/lib/oneloop/data /usr/local/bin/oneloop user add alice --admin -
Start oneloop now and at every boot:
sudo systemctl daemon-reload sudo systemctl enable --now oneloop
Logs go to the journal; read them with journalctl -u oneloop -f. If oneloop
fails, systemd restarts it after five seconds, and stops trying after 10 starts
in 10 minutes, for example when the database is missing or needs db migrate.
Invalid settings (exit code 2) stop the service at once, because a restart
can’t fix them. Fix the cause, then start oneloop again:
sudo systemctl daemon-reload
sudo systemctl reset-failed oneloop
sudo systemctl restart oneloop
launchd (macOS)
This LaunchAgent runs while you are signed in to macOS. It starts at login and stops at logout. To run before login, use a LaunchDaemon in the system domain.
Complete the source install, including the database
and first admin, then stop the manually started server. The template uses
~/.cargo/bin/oneloop, your home as its working directory, and ~/oneloop-data.
If you chose other locations, use absolute paths to your installed binary,
an existing working directory and the initialized data directory.
Create the job and log folders:
mkdir -p ~/Library/LaunchAgents ~/Library/Logs
The deploy/launchd folder has three files to download:
com.oneloop.example.plistis the job. Replace everyREPLACE_ME, set your environment, and save it as~/Library/LaunchAgents/com.oneloop.plist.oneloop-launchd.shstarts oneloop. It runsoneloop serve --checkfirst. If the binary is missing, a setting is invalid or the data folder has no database, it stops the job and writes the reason to the log, because a restart can’t help. After other failures, launchd starts oneloop again, at most once a minute.oneloop.newsyslog.conf.examplerotates the log. ReplaceREPLACE_MEand save it as~/oneloop.newsyslog.conf.
From the folder where you saved the launcher, install it and load the job:
sudo install -d /usr/local/libexec
sudo install -m 0755 oneloop-launchd.sh /usr/local/libexec/oneloop-launchd.sh
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.oneloop.plist
After you change the plist, or after the job stopped because of a mistake,
unload the job with
launchctl bootout "gui/$(id -u)" ~/Library/LaunchAgents/com.oneloop.plist and
load it again.
The log is ~/Library/Logs/oneloop.log. launchd doesn’t rotate it. Rotate it
only while the job is unloaded, because a running job keeps writing to the old
file:
launchctl bootout "gui/$(id -u)" ~/Library/LaunchAgents/com.oneloop.plist
newsyslog -r -f ~/oneloop.newsyslog.conf
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.oneloop.plist
-r lets newsyslog run without root. It rotates the log when it is larger than
10 MiB, and it keeps seven old logs.
Stopping and restarting
When oneloop gets SIGTERM or Ctrl+C, it stops taking new connections and
gives open requests up to 30 seconds to finish. When it is back, browsers
reconnect by themselves. Nobody has to sign in again.
Knowledge base connections
To sync knowledge bases, oneloop runs git
2.31 or later, and ssh for SSH URLs. The Docker image includes both. On other
servers, install them, for example with apt install git openssh-client.
- Network. The server needs outgoing HTTPS, or SSH on port 22 or the port in
the URL, to your Git hosts. oneloop passes
HTTPS_PROXY,HTTP_PROXY,ALL_PROXYandNO_PROXYto Git. - Private certificate authorities. Set
GIT_SSL_CAINFO(a file) orGIT_SSL_CAPATH(a folder) for the oneloop service. oneloop also passesSSL_CERT_FILEandSSL_CERT_DIR. - Isolation. Git runs with an empty configuration of its own: it ignores the server’s Git settings, credential helpers and hooks, never asks for a password, and speaks only HTTPS and SSH.
- Credentials. Access tokens and deploy keys are encrypted with
keys/knowledge.keyin the data directory, which backups include. - SSH host keys. oneloop trusts a host’s key on the first connection and
keeps it in
keys/knowledge_known_hosts.
The example systemd unit already allows this. With your own sandbox, allow the
service to run git and ssh and to open outgoing connections.
AI assistant connections
By default, assistants connect as Other clients describes. Three settings, all off by default, let more clients connect. Turn on only what your clients need.
- App callbacks. Some desktop apps receive the sign-in through a link of
their own, such as
cursor://…. List those schemes inONELOOP_MCP_REDIRECT_SCHEMES, for examplecursor. Any app on a computer can claim a scheme, but a code it catches is useless without the secret that only the app that started the sign-in holds (PKCE). The Connect page shows the whole callback address. List only the schemes of the AI apps you use: anyone can register an app, so with a listed scheme they can send a person who chooses Connect to any address in that scheme, and some schemes, such asms-msdt, start programs on the computer. - Browser clients. Tools that run in a web page, such as MCP Inspector, call
oneloop from another site. List their exact origins in
ONELOOP_MCP_ALLOWED_ORIGINS, for examplehttp://localhost:6274. Only these pages may register an app and call/mcpfrom a browser. Any site may read oneloop’s OAuth metadata and use its token and revocation endpoints, because they use no cookies, and a request needs a code or token that the site doesn’t have. - Client metadata documents. With
ONELOOP_MCP_CLIENT_METADATA_DOCUMENTS=true, an assistant may use an HTTPS address as its client ID. oneloop then fetches the document at that address for the app’s name and callbacks, and the Connect page shows the document’s whole address, because one host can serve documents for many authors. The server needs outgoing HTTPS for this. oneloop connects directly, without a proxy, and trusts the system’s certificate authorities, which the Docker image includes. It refuses to start with this setting if it finds none. Turning the setting off stops new connections this way; apps that are already connected keep working until someone disconnects them.
Fetching client metadata documents
Anyone who can sign in can make oneloop fetch an address of their choice, so oneloop limits what it fetches:
- It fetches only for someone who is signed in, and only
httpsaddresses with a path, on the default port. - Every address the host name resolves to must be public, never a private, loopback, link-local or other special address. oneloop connects to an address it checked, and follows no redirects.
- It reads at most 5 KiB within 5 seconds, and fetches at most 30 documents a
minute, 10 of them for one person. It keeps a document as long as its
Cache-Controlheader says, from 1 to 60 minutes (10 minutes if it doesn’t say), and at most 1,000 apps described this way that haven’t connected yet, besides those with a sign-in under way.
Two risks remain:
- On a network with its own NAT64 prefix, an address that looks public can lead to an internal server. oneloop may then connect to it and start TLS; the certificate check stops it there.
- A server on a public address that trusts your oneloop server’s address, such as a site that admits only some IP addresses, can get one GET request without credentials from it. oneloop never shows the answer.
Security checklist
- Serve oneloop only over HTTPS, and keep the redirect from HTTP to HTTPS. Browsers learn oneloop’s HSTS rule only on their first HTTPS visit.
- Let only the proxy reach port 8080. A direct connection skips HTTPS.
- Set
ONELOOP_TRUSTED_PROXIESto your proxy’s address, and nothing else. - Keep oneloop’s security headers. They protect sign-in and isolate uploaded files.
- Run oneloop under its own system account, and run maintenance commands as
that account. The data directory holds password hashes, private files,
and
keys/knowledge.key, which encrypts Knowledge credentials. oneloop makes it readable only by its owner, and it warns at startup if others can read it. - Keep backups and logs private. Backups contain everything, and logs contain usernames and IP addresses.
- Install new releases, because security fixes come in new versions.
- Turn on the AI assistant connection settings only for clients that need them.
/healthzis public and shows the version. If you want, allow it only from your monitoring network; the nginx example shows how.
oneloop also protects you by itself:
- Its sign-in cookie works only over HTTPS and only for your exact host name.
- It answers only requests for its own host name, and accepts changes only from its own pages.
- It slows down repeated failed sign-ins, for each account and each address.
- It never runs uploaded or synced files on the server. Downloads are always saved as files, HTML previews run in a sandbox, and Markdown is cleaned before display.
Report security problems privately, as described in SECURITY.md.