Deployment

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.

Contents

1. Deploying to Fly.io

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.

1.1 Install flyctl

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

1.2 Launch a new app

From the root of your cloned Rabbithole repository, run:

fly launch

When prompted:

This creates a fly.toml in the project root and registers your app on Fly.io.

1.3 Deploy

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.

2. Configuring fly.toml

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"
Port: Rabbithole listens on port 8080 by default. Make sure internal_port in [http_service] matches the port configured in your environment.

3. Persistent Volumes for SQLite

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.

Important: Always mount a persistent volume before your first fly deploy. If you deploy without one, your app will start but all cached pages will be lost on restart or redeploy.

Creating the volume

# 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).

Volume considerations

4. Environment Variables & API Keys

Rabbithole 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.

Full list of relevant environment variables

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.

5. Docker Usage

A Dockerfile is included in the Rabbithole repository. It performs a multi-stage Rust build to produce a minimal final image.

Building locally

git clone https://github.com/ajbt200128/rabbithole
cd rabbithole
docker build -t rabbithole .

Running locally with Docker

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.

Docker Compose (optional)

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:
Store your API key in a .env file (not committed to git) and reference it via env_file to keep secrets out of your compose file.

6. VPS with systemd

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.

6.1 Build the binary

cargo build --release
# Binary is at ./target/release/rabbithole
sudo cp target/release/rabbithole /usr/local/bin/rabbithole

6.2 Create a systemd service

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

6.3 Reverse proxy with nginx

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
Tip: Set 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.

7. Custom Domains & Subdomains

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

Adding a custom domain on Fly.io

# 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.

Running multiple instances on Fly.io

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

8. Performance Considerations

LLM latency on first visit

When a visitor requests a URL that has not been generated yet, Rabbithole must:

  1. Look up the URL in SQLite (fast, <1ms)
  2. Find or generate the prompt for that URL
  3. Call the LLM API to generate the full HTML page
  4. Store the result in SQLite
  5. Return the response to the visitor

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.

Cache hit speed

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.

Auto-stop / auto-start on Fly.io

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.

Choosing an LLM model

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.

Pre-warming the cache

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