Deployment¶
This page explains how to make your experiment accessible over the internet. It assumes you have installed uproot and created a project.
Choose the option that suits you:
- Use Cloudflare. If you have a computer with a stable, high-bandwidth internet connection, run uproot there and let Cloudflare connect participants to it. The same approach also works on a VPS.
- Self-host on your own VPS. A virtual private server (VPS) is a rented computer that you manage remotely. Use nginx and Let’s Encrypt for full control over your web server and HTTPS setup—no third-party service touches participant traffic.
- Use a hosting service. Deploy to Fly.io, Railway, Render, or Heroku using the instructions below.
Option 1: Use Cloudflare¶
Cloudflare Tunnel connects your computer to Cloudflare, which forwards participants’ requests to uproot. Your experiment and database stay on your computer. Cloudflare provides the public HTTPS address, which encrypts the browser connection, without requiring you to configure nginx, certificates, or port forwarding on your router.
Try it for free¶
TryCloudflare gives you a temporary public address for free, without a Cloudflare account or your own domain name.
First, install cloudflared for your operating system and set an admin password. In your project directory, start uproot:
In a second terminal on the same computer, run:
The command prints a public address such as https://random-words.trycloudflare.com. Leave the tunnel running. In your project’s .env file, add or update this line, replacing the example with the address you received:
This tells uproot which public address to use for links. uproot reads .env at startup, so stop it with Ctrl+C in its terminal, then run uv run uproot run again to pick up the change. Open the public address followed by /admin/, sign in, and create a session or room. Share the participant links from the admin interface.
Keep both terminals running and prevent the computer from sleeping while anyone is using the experiment. Its internet connection needs sufficient upload bandwidth, since your computer sends the experiment’s pages and files to participants. If you restart the Quick Tunnel, update UPROOT_ORIGIN with its new address and restart uproot before sharing new links.
For testing
Cloudflare’s Quick Tunnels are intended for testing and development. They have no uptime guarantee and a limit of 200 simultaneous requests. For collecting research data, use a permanent tunnel as described below.
Use a permanent address for production¶
For a stable address such as https://study.example.com, follow Cloudflare’s tunnel setup guide. You need a Cloudflare account and a domain name added to it. Give the tunnel a name, choose your public hostname, and point it to http://localhost:8000. Update UPROOT_ORIGIN to the public HTTPS address and restart uproot.
Permanent tunnels and custom hostnames are available on the free plan. For production use, we recommend a paid Cloudflare plan for additional security features. See current plans and pricing before choosing. Payment is not required simply to name a tunnel or use your own domain.
Cloudflare publishes GDPR safeguards and a data processing addendum. Because participant traffic passes through Cloudflare, your institution should assess whether the setup meets your study’s data-protection requirements.
Use the same setup on a VPS
You can also install uproot and cloudflared on a VPS and point a permanent tunnel to http://localhost:8000 there. Cloudflare handles public HTTPS, saving you the nginx and Let’s Encrypt setup, and your experiment and database live on the VPS so your personal computer can be switched off.
- Follow the VPS setup guide to prepare the server.
- Start uproot on boot with a systemd service.
- Use Cloudflare’s tunnel setup in place of the nginx and certificate steps.
- Run
cloudflaredas a service so it starts automatically.
Option 2: Self-host on your own VPS¶
For full control over your deployment, run uproot behind nginx, a web server that forwards requests to uproot as a reverse proxy. You manage the server, certificates, and software yourself. This is the most sovereign option: participants connect directly to your VPS, and you choose who provides the infrastructure. We strongly recommend that you use a VPS with Debian 13+. (While uproot itself works flawlessly on Ubuntu, using Ubuntu is in general considered bad practice. uproot also works on OpenBSD.)
New to VPS deployment?
If you are setting up a server from scratch, follow the complete VPS setup guide. It covers everything from getting a VPS to a working HTTPS setup, step by step.
HTTPS required
uproot requires HTTPS in production. Many browser features (like the secure cookies needed for accessing the admin area) only work over HTTPS. Use Let’s Encrypt with Certbot to get free TLS certificates—the credentials your web server uses to provide HTTPS.
Running uproot¶
The simplest approach is a tmux session that persists after you disconnect:
Detach with Ctrl+B then D. Reattach later with tmux attach -t uproot.
For a more robust setup that survives reboots automatically, the VPS setup guide shows how to create a systemd user service instead.
uproot listens on port 8000 by default. Use --port to change it if needed.
nginx configuration¶
Configure nginx as a reverse proxy. WebSockets keep a connection open between the browser and server; the upgrade headers below are required for uproot’s real-time features. The following example config (to be added within an existing http block) is battle-tested and has been proven to work reliably:
map $http_upgrade $connection_upgrade {
default upgrade;
"" close;
}
server {
server_name example.com; # Adjust this
listen 443 ssl;
listen 443 quic;
listen [::]:443 ssl;
listen [::]:443 quic;
http2 on;
add_header Alt-Svc 'h3=":443"; ma=86400';
ssl_certificate PATH_TO_fullchain.pem; # Adjust this
ssl_certificate_key PATH_TO_privkey.pem; # Adjust this
location / {
proxy_pass http://127.0.0.1:8000; # Maybe adjust this
proxy_read_timeout 600s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Front-End-Https on;
proxy_set_header X-Forwarded-Protocol https;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Url-Scheme https;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
Hosting in a subdirectory¶
If you want to serve uproot at a path like https://example.com/my-study/ instead of the root, set the UPROOT_SUBDIRECTORY environment variable to your chosen path:
Leading and trailing slashes are stripped automatically, so my-study, /my-study, and /my-study/ are all equivalent.
When set, uproot prefixes all routes with the subdirectory. Hence, participant pages, the admin interface, static files, and WebSocket connections all work without any further changes to your code or templates.
Then adjust your nginx config to match. Here is a complete example; the only difference from a root deployment is the location path:
location /my-study/ {
proxy_pass http://127.0.0.1:8000;
proxy_read_timeout 600s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Front-End-Https on;
proxy_set_header X-Forwarded-Protocol https;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Url-Scheme https;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
The admin interface will be at https://example.com/my-study/admin/.
Persistent configuration
If you run uproot via a systemd service, set the variable in an environment file or in the service unit’s Environment= directive so it persists across restarts.
Option 3: Use a hosting service¶
These services run uproot on infrastructure they manage. Choose Fly.io, Railway, or Render to keep the default SQLite database, or Heroku with PostgreSQL. Follow the storage instructions for your chosen service so that saved experiment data survives restarts.
Fly.io with SQLite¶
Fly.io supports long-running WebSocket connections and persistent volumes, so you can run uproot with its default SQLite database. Fly charges for the Machines, volumes, and network traffic that you use; check its current pricing before you deploy.
Why Fly.io?
- Full support for WebSocket connections and real-time features
- Works with uproot’s default SQLite database on a persistent volume
- Regional deployment for lower latency
- Native support for persistent volumes
Prerequisites¶
- Create a Fly.io account
- Install the Fly CLI:
- Log in to Fly:
Deploy your experiment¶
Navigate to your uproot project directory and run:
The --no-deploy option is important: it lets you attach persistent storage before uproot starts for the first time. Fly will detect your Python application and guide you through the setup. When prompted:
- Choose an app name or let Fly generate one
- Choose a region close to your participants
- Do not add PostgreSQL or Redis if Fly offers them
This creates a fly.toml configuration file. Edit it to ensure the correct settings:
app = "your-app-name"
primary_region = "iad" # or your chosen region
[build]
builder = "paketobuildpacks/builder-jammy-base"
[env]
PORT = "8080"
UPROOT_SQLITE3 = "/data/uproot.sqlite3"
UPROOT_ORIGIN = "https://your-app-name.fly.dev"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "off" # Keep WebSocket connections alive
auto_start_machines = true
min_machines_running = 1
[[vm]]
memory = "512mb"
cpu_kind = "shared"
cpus = 1
Replace your-app-name and iad with the app name and region selected by fly launch.
If you later add a custom domain, update UPROOT_ORIGIN to that domain.
Adding persistent storage for SQLite¶
Create a persistent volume to ensure your SQLite database survives app restarts:
Update your fly.toml to mount the volume:
The UPROOT_SQLITE3 setting above points uproot at the mounted volume.
Use one Machine
A Fly volume is attached to one Machine and is not automatically replicated. Keep this SQLite deployment at one Machine; do not clone the Machine or scale it horizontally.
Set the admin password¶
New uproot projects use upd.auto_login() in main.py. Set the production password as a Fly secret so that it is not committed to Git:
If your project does not already contain this line, add it to main.py:
Deploy¶
Deploy your application:
After deployment completes, Fly will show your app’s URL. Open it in your browser:
Monitoring and logs¶
View your app’s logs:
Check app status:
Access the admin interface at https://your-app-name.fly.dev/admin/.
Railway with SQLite¶
Railway can deploy an uproot project directly from GitHub. Its WebSocket connections are exempt from inactivity timeouts, but its ordinary container disk is temporary. You must attach a volume for the SQLite database.
Quick start¶
- Sign up at railway.com and create a project from your GitHub repository.
- Open the uproot service’s Settings tab. Under Deploy, set the start command to
uproot run -h 0.0.0.0 -p $PORT. This is the same command as the generatedProcfile, but setting it explicitly avoids relying on deprecated Procfile detection. - Add a Railway volume to the service and set its mount path to
/data. - Under Networking, select Generate Domain.
-
Under Variables, add the following values, replacing the example domain and password:
-
Deploy the staged changes, then open
https://your-service.up.railway.app/admin/.
The password variable assumes that main.py contains upd.ADMINS["admin"] = upd.auto_login(), as current uproot projects do. Add that line if you have an older project.
Railway services with a volume cannot use replicas and have a short period of downtime during a redeploy. Do not redeploy while an experiment is running.
Render with SQLite¶
Render supports WebSockets without a fixed connection timeout and can attach a persistent disk to a paid web service. Do not use a free web service for a real experiment: free services cannot attach a disk, and their local SQLite files are lost when the service restarts, redeploys, or spins down.
Quick start¶
- Sign up at render.com and select New > Web Service.
- Connect the Git repository that contains your uproot project and choose the Python 3 runtime.
- Set the build command to
pip install -r requirements.txt. - Set the start command to
uproot run -h 0.0.0.0 -p $PORT. - Choose a paid instance type. Under Advanced, add a persistent disk with the mount path
/var/data. -
Add the following environment variables, replacing the example service name and password:
-
Create the web service. When the first deploy finishes, open
https://your-service-name.onrender.com/admin/.
The password variable assumes that main.py contains upd.ADMINS["admin"] = upd.auto_login(), as current uproot projects do. Add that line if you have an older project. Render’s Python runtime uses the project’s .python-version file, so the file generated by uproot selects a compatible Python version. If you add a custom domain, update UPROOT_ORIGIN to its full https:// URL.
Persistent-disk limitations
A Render persistent disk is available to only one service instance and disables zero-downtime deploys. This matches uproot’s single-process live state, but it means each deploy briefly stops the experiment. Keep one instance and do not deploy while participants are active. Render can also replace an instance during platform maintenance, which closes its WebSocket connections.
Heroku¶
Heroku can run uproot with PostgreSQL. A Heroku dyno’s local disk is temporary, so the default SQLite database is not suitable for production there.
WebSocket timeout
Heroku supports WebSockets and applies a rolling 55-second idle timeout. uproot sends a heartbeat every 9 seconds, so a participant reading or waiting does not leave the connection idle long enough to hit that limit.
Dyno restarts
Heroku restarts dynos at least daily, as well as after deploys and configuration changes. PostgreSQL keeps the saved data, but a restart interrupts active connections and in-process background tasks. Restart or deploy shortly before a scheduled experiment, and do not change the app while participants are active.
Prerequisites¶
- Create a Heroku account
- Install the Heroku CLI
- Log in to Heroku:
Deploy your experiment¶
First, add PostgreSQL support to the project’s main dependencies:
This updates pyproject.toml and uv.lock. Current uproot projects also contain a requirements.txt, but Heroku’s Python buildpack requires exactly one package-manager file. Remove requirements.txt from Git so that Heroku uses the lock file:
The generated Procfile already binds uproot to Heroku’s $PORT. Make sure main.py contains upd.ADMINS["admin"] = upd.auto_login(). Then create the app, enable runtime metadata so uproot can discover its public URL, add PostgreSQL, and set the admin password:
# Create a new Heroku app
heroku create my-experiment-name
# Enable runtime metadata
heroku labs:enable runtime-dyno-metadata
# Provision PostgreSQL
heroku addons:create heroku-postgresql:essential-0
heroku pg:wait
# Store the admin password outside the source code
heroku config:set UPROOT_ADMIN_PASSWORD='choose-a-long-random-password'
# Deploy
git push heroku main
# Keep uproot on exactly one web dyno
heroku ps:scale web=1:basic
# Open the admin interface
heroku open /admin/
If your local branch is not named main, deploy it with git push heroku HEAD:main instead. The PostgreSQL add-on sets DATABASE_URL; uproot detects it automatically. If you add a custom domain, set UPROOT_ORIGIN to its full https:// URL with heroku config:set.
Do not scale horizontally
uproot’s live connection queues and background tasks belong to one server process. Keep exactly one web dyno; adding more web dynos would split participants across processes that do not share that live state.