Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Production setup

Before your team uses oneloop, set up three things:

  1. HTTPS, with a reverse proxy such as Caddy or nginx in front of oneloop.
  2. A service that starts oneloop automatically and restarts it after a crash.
  3. 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.

  1. Point a DNS name, such as tasks.example.com, to your server.

  2. Set these variables for oneloop, then restart it:

    VariableValue
    ONELOOP_PUBLIC_URLhttps://tasks.example.com, the exact address people open
    ONELOOP_TRUSTED_PROXIESThe address of the proxy; see Trusted proxy

    With Docker, change them under environment: in compose.yaml, then run docker compose up -d.

  3. Set up the proxy, as shown below for Caddy and nginx.

  4. 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 runsThe proxy runsSet ONELOOP_TRUSTED_PROXIES to
From sourceOn the same machine127.0.0.1
In Docker, with the example compose.yamlOn the hostThe gateway of the Docker network, from the command below
AnywhereOn another machineThe 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 Host header unchanged, with the port if there is one.
  • Don’t add or change Content-Security-Policy or 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-stopped starts oneloop again after a crash or a reboot, if Docker itself starts at boot.
  • stop_grace_period: 35s gives 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)

  1. 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
    
  2. Save the unit file as /etc/systemd/system/oneloop.service. Set your ONELOOP_PUBLIC_URL and any other settings in it. The unit keeps data in /var/lib/oneloop/data and uses systemd’s sandbox options.

  3. 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
    
  4. 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.plist is the job. Replace every REPLACE_ME, set your environment, and save it as ~/Library/LaunchAgents/com.oneloop.plist.
  • oneloop-launchd.sh starts oneloop. It runs oneloop serve --check first. 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.example rotates the log. Replace REPLACE_ME and 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_PROXY and NO_PROXY to Git.
  • Private certificate authorities. Set GIT_SSL_CAINFO (a file) or GIT_SSL_CAPATH (a folder) for the oneloop service. oneloop also passes SSL_CERT_FILE and SSL_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.key in 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 in ONELOOP_MCP_REDIRECT_SCHEMES, for example cursor. 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 as ms-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 example http://localhost:6274. Only these pages may register an app and call /mcp from 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 https addresses 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-Control header 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_PROXIES to 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.
  • /healthz is 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.