This page describes how to deploy Rabbithole to production. The canonical live deployment is isarabbithole.com, hosted on Fly.io. All methods described here have been used with the real codebase at github.com/ajbt200128/rabbithole.
Fly.io is the recommended and tested deployment target for Rabbithole. It supports persistent volumes (needed for SQLite), Docker-based deploys, and easy secret management for API keys.
Install the Fly.io CLI (flyctl) using the official installer:
# macOS / Linux curl -L https://fly.io/install.sh | sh # Windows (PowerShell) iwr https://fly.io/install.ps1 -useb | iex
Then authenticate:
fly auth login
From the root of your cloned Rabbithole repository, run:
fly launch
When prompted:
my-rabbithole)iad for US East)This creates a fly.toml in the project root and registers your app on Fly.io.
After configuring fly.toml, volumes, and secrets (see sections below), deploy with:
fly deploy
Fly.io will build the Docker image (using the included Dockerfile), push it, and start the VM. The app will be available at https://<appname>.fly.dev.
Below is a representative fly.toml for a Rabbithole deployment. Adjust the app name, region, and paths as needed.
app = "my-rabbithole" primary_region = "iad" [build] dockerfile = "Dockerfile" [env] PORT = "8080" DATABASE_PATH = "/data/rabbithole.db" [http_service] internal_port = 8080 force_https = true auto_stop_machines = true auto_start_machines = true min_machines_running = 0 [[vm]] cpu_kind = "shared" cpus = 1 memory_mb = 512 [[mounts]] source = "rabbithole_data" destination = "/data"
8080 by default. Make sure internal_port in [http_service] matches the port configured in your environment.
Rabbithole uses SQLite to cache generated pages permanently. Without a persistent volume, the database is wiped on every deploy, forcing every page to be regenerated from scratch — wasting LLM API calls and increasing latency for all visitors.
fly deploy. If you deploy without one, your app will start but all cached pages will be lost on restart or redeploy.
# Create a 1 GB persistent volume in your app's region fly volumes create rabbithole_data --size 1 --region iad
The [[mounts]] section in fly.toml (shown above) tells Fly.io to mount this volume at /data inside the container. Rabbithole writes its SQLite database to /data/rabbithole.db (set via DATABASE_PATH env var).
fly ssh console -C "du -sh /data"fly ssh sftp get /data/rabbithole.db ./rabbithole-backup.dbRabbithole requires an LLM API key at runtime. Never hard-code secrets into fly.toml or the Docker image. Use Fly.io secrets instead:
# Set your LLM provider API key as a Fly.io secret fly secrets set ANTHROPIC_API_KEY=sk-ant-... # Or for OpenAI: fly secrets set OPENAI_API_KEY=sk-... # Verify secrets are set (values are never shown) fly secrets list
Secrets are injected as environment variables into the running container automatically.
| Variable | Description | Example |
|---|---|---|
ANTHROPIC_API_KEY |
API key for Anthropic (Claude models) | sk-ant-api03-... |
OPENAI_API_KEY |
API key for OpenAI (GPT models) | sk-proj-... |
DATABASE_PATH |
Path to the SQLite database file | /data/rabbithole.db |
PORT |
HTTP port the server listens on | 8080 |
SEED_PROMPT |
The homepage prompt for this instance | A website about ... |
See the Configuration page for a full reference of all configuration options.
A Dockerfile is included in the Rabbithole repository. It performs a multi-stage Rust build to produce a minimal final image.
git clone https://github.com/ajbt200128/rabbithole cd rabbithole docker build -t rabbithole .
docker run -p 8080:8080 \ -e ANTHROPIC_API_KEY=sk-ant-... \ -e SEED_PROMPT="A website about competitive programming" \ -v $(pwd)/data:/data \ -e DATABASE_PATH=/data/rabbithole.db \ rabbithole
This mounts a local ./data directory as the persistent volume so the SQLite cache survives container restarts.
version: "3.9"
services:
rabbithole:
build: .
ports:
- "8080:8080"
environment:
- PORT=8080
- DATABASE_PATH=/data/rabbithole.db
- SEED_PROMPT=A website about competitive programming
env_file:
- .env # place ANTHROPIC_API_KEY= here
volumes:
- rabbithole_data:/data
volumes:
rabbithole_data:
.env file (not committed to git) and reference it via env_file to keep secrets out of your compose file.
If you prefer running on a plain Linux VPS (e.g. a DigitalOcean Droplet, Hetzner server, or Linode), you can run the compiled Rabbithole binary directly and manage it with systemd.
cargo build --release # Binary is at ./target/release/rabbithole sudo cp target/release/rabbithole /usr/local/bin/rabbithole
Create /etc/systemd/system/rabbithole.service:
[Unit] Description=Rabbithole LLM Web Server After=network.target [Service] Type=simple User=www-data WorkingDirectory=/var/lib/rabbithole ExecStart=/usr/local/bin/rabbithole Restart=on-failure RestartSec=5 # Secrets — use a credentials file or EnvironmentFile EnvironmentFile=/etc/rabbithole/env [Install] WantedBy=multi-user.target
Create /etc/rabbithole/env (mode 600, owned by root):
ANTHROPIC_API_KEY=sk-ant-... DATABASE_PATH=/var/lib/rabbithole/rabbithole.db PORT=8080 SEED_PROMPT=A website about something interesting
sudo mkdir -p /var/lib/rabbithole /etc/rabbithole sudo chmod 600 /etc/rabbithole/env sudo systemctl daemon-reload sudo systemctl enable --now rabbithole sudo systemctl status rabbithole
Place nginx in front to handle HTTPS via Let's Encrypt:
server {
server_name mysite.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 120s; # LLM generation can take time
}
}
# Then: certbot --nginx -d mysite.example.com
proxy_read_timeout to at least 60–120 seconds. On a cache miss, Rabbithole must call the LLM API, which can take several seconds for a full page generation.
Each Rabbithole instance serves a completely independent website driven by its own seed prompt. You can run multiple instances with different seed prompts and point different (sub)domains at each. For example:
| Domain | Seed Prompt | Notes |
|---|---|---|
| isarabbithole.com | Rabbithole documentation homepage | Primary instance |
| acapa.isarabbithole.com | A cappella music group website | Subdomain → separate instance |
| cgpa.isarabbithole.com | A GPA calculator and college planning resource | Subdomain → separate instance |
# Add the domain to your Fly app fly certs add mysite.example.com # Fly will show you the DNS records to add fly certs show mysite.example.com
Point a CNAME (for subdomains) or A/AAAA (for apex domains) record from your DNS provider to Fly's targets as shown by the command above. Fly.io provisions TLS automatically via Let's Encrypt.
Each separate website requires its own Fly.io app, its own volume, and its own seed prompt secret. Use fly launch in a fresh directory for each instance, or use the --app flag:
# Deploy a second instance fly launch --name acapa-rabbithole fly secrets set SEED_PROMPT="An a cappella music group website..." \ --app acapa-rabbithole fly volumes create rabbithole_data --size 1 --region iad \ --app acapa-rabbithole fly deploy --app acapa-rabbithole
When a visitor requests a URL that has not been generated yet, Rabbithole must:
Step 3 dominates. Typical latency for full page generation with Claude Sonnet is 3–15 seconds depending on page complexity, network conditions, and API load. This is unavoidable on cache miss — it is the core behavior of Rabbithole.
On subsequent visits, the page is served directly from SQLite. This is essentially a disk read followed by a network write — response times are typically under 10 milliseconds. The persistent volume means the cache survives server restarts and redeployments.
With auto_stop_machines = true and min_machines_running = 0 in fly.toml, Fly.io will shut down your VM when there is no traffic and cold-start it on the next request. This adds an additional 1–3 seconds cold-start penalty on top of LLM latency. To avoid this, set:
[http_service] auto_stop_machines = false min_machines_running = 1
This keeps one VM always running, which costs a small amount of Fly.io compute credits continuously but eliminates cold-start delays.
| Model | Speed | Quality | Cost |
|---|---|---|---|
| claude-3-5-haiku | Fast (~2–5s) | Good | Low |
| claude-3-5-sonnet | Medium (~5–10s) | Very good | Medium |
| claude-3-7-sonnet | Slower (~8–15s) | Excellent | Higher |
| gpt-4o-mini | Fast (~2–4s) | Good | Low |
| gpt-4o | Medium (~5–10s) | Very good | Medium |
For public-facing deployments, a balance of speed and quality is usually preferable. Since each generated page is cached forever, the per-generation cost is paid only once per unique URL.
You can pre-generate the most important pages by simply visiting or curl-ing them after deploy:
for path in / /about.html /getting-started.html /examples.html; do curl -s "https://my-rabbithole.fly.dev$path" -o /dev/null echo "Warmed: $path" done
See also: Configuration Reference | Architecture | Getting Started | Live Examples