LLM Observability
    All-in-One Docker

    Deploy Helicone on a VPS

    Self-host Helicone LLM observability on a RamNode VPS — all-in-one container with dashboard, Jawn proxy, MinIO, Postgres, and ClickHouse behind Caddy TLS.

    Helicone is an open source LLM observability platform. It logs, monitors, and analyzes LLM requests by sitting in front of providers such as OpenAI and Anthropic, then gives you dashboards for cost, latency, and usage. This guide covers a self hosted production deployment on a RamNode KVM VPS running Ubuntu 24.04 LTS, using the official all in one container, persistent volumes, host hardening, and a reverse proxy with automatic TLS.

    Architecture

    The self hosted Helicone all in one image bundles every component into a single container:

    • Web dashboard on port 3000
    • Jawn (the API and LLM proxy) on port 8585
    • MinIO S3 compatible object storage on port 9080
    • PostgreSQL on 5432 and ClickHouse on 8123, both internal only

    Browsers need to reach the dashboard (3000), the Jawn API (8585), and MinIO (9080). The database ports stay private. In this guide each browser facing service gets its own subdomain behind Caddy with TLS, and the raw ports are closed at the firewall.

    Recommended RamNode sizing

    ClickHouse is the memory hungry component. Size accordingly:

    WorkloadvCPURAMDisk
    Light, evaluation24 GB40 GB
    Steady production48 GB80 GB+

    Log volume drives disk growth over time. Monitor it and resize the VPS if your request volume climbs.

    Prerequisites

    • A RamNode KVM VPS with Ubuntu 24.04 LTS.
    • Three DNS records pointing at the VPS public IP, for example:
      • helicone.example.com for the dashboard
      • jawn.example.com for the API and proxy
      • s3.example.com for MinIO
    • SSH access as root or a sudo user.

    Step 1: Host hardening

    Create a non-root sudo user:

    shell
    adduser deploy
    usermod -aG sudo deploy
    rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

    Confirm key based login as deploy, then harden SSH in /etc/ssh/sshd_config:

    shell
    PermitRootLogin no
    PasswordAuthentication no
    shell
    sudo systemctl reload ssh

    Set up the firewall. Only SSH, HTTP, and HTTPS are public; everything else is reached through the reverse proxy.

    shell
    sudo apt update && sudo apt -y install ufw fail2ban
    sudo ufw default deny incoming
    sudo ufw default allow outgoing
    sudo ufw allow OpenSSH
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw enable

    Enable automatic security updates:

    shell
    sudo apt -y install unattended-upgrades
    sudo dpkg-reconfigure --priority=low unattended-upgrades

    Step 2: Install Docker

    shell
    sudo apt -y install ca-certificates curl
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
      sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    sudo apt update
    sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    sudo usermod -aG docker deploy

    Log out and back in for the group change.

    Step 3: Deploy Helicone with persistent storage

    A bare docker run of the all in one image wipes all data on restart. The fix is named volumes for the three stateful stores. Use a small Compose file so the configuration is version controlled and reproducible.

    Create the project directory:

    shell
    sudo mkdir -p /var/lib/helicone
    sudo chown -R deploy:deploy /var/lib/helicone
    cd /var/lib/helicone

    Create /var/lib/helicone/.env. Generate a strong auth secret first:

    shell
    echo "BETTER_AUTH_SECRET=$(openssl rand -base64 32)" > /var/lib/helicone/.env

    Then append your public URLs and storage settings to the same file:

    shell
    SITE_URL=https://helicone.example.com
    BETTER_AUTH_URL=https://helicone.example.com
    NEXT_PUBLIC_APP_URL=https://helicone.example.com
    NEXT_PUBLIC_HELICONE_JAWN_SERVICE=https://jawn.example.com
    S3_ENDPOINT=https://s3.example.com
    NEXT_PUBLIC_IS_ON_PREM=true
    S3_ACCESS_KEY=helicone-minio
    S3_SECRET_KEY=replace-with-a-strong-secret
    S3_BUCKET_NAME=request-response-storage

    Create /var/lib/helicone/docker-compose.yml:

    shell
    services:
      helicone:
        container_name: helicone
        image: helicone/helicone-all-in-one:latest
        restart: unless-stopped
        env_file: .env
        ports:
          - "127.0.0.1:3000:3000"   # web dashboard
          - "127.0.0.1:8585:8585"   # jawn API and LLM proxy
          - "127.0.0.1:9080:9080"   # MinIO S3
        volumes:
          - helicone-postgres:/var/lib/postgresql/data
          - helicone-clickhouse:/var/lib/clickhouse
          - helicone-minio:/data
    
    volumes:
      helicone-postgres:
      helicone-clickhouse:
      helicone-minio:

    All three published ports are bound to 127.0.0.1 so they are reachable only by the reverse proxy on the same host. The internet sees only the TLS terminated subdomains.

    Start it:

    shell
    docker compose up -d
    docker compose logs -f

    The first start takes a few minutes while it runs database migrations.

    Step 4: Reverse proxy with automatic TLS

    Install Caddy:

    shell
    sudo apt -y install debian-keyring debian-archive-keyring apt-transport-https curl
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
    sudo apt update
    sudo apt -y install caddy

    Replace /etc/caddy/Caddyfile with three site blocks:

    shell
    helicone.example.com {
        reverse_proxy 127.0.0.1:3000
    }
    
    jawn.example.com {
        reverse_proxy 127.0.0.1:8585
    }
    
    s3.example.com {
        reverse_proxy 127.0.0.1:9080
    }
    shell
    sudo systemctl reload caddy

    All three URLs must share a consistent scheme and origin. Mixing localhost with a public hostname, or HTTP with HTTPS, causes "Invalid origin" errors on sign in.

    Step 5: Create your account

    Helicone's self hosted build does not include email. RamNode also blocks outbound SMTP (port 25) by default on VPS plans, so email verification would not work even if it were wired up. Verify your user manually in the database instead.

    Sign up at https://helicone.example.com/signup, then mark the account verified:

    shell
    docker exec -u postgres helicone psql -d helicone_test -c \
      "UPDATE \"user\" SET \"emailVerified\" = true WHERE email = 'you@example.com';"

    If you see a "No organization ID found" error, create an org and attach your user:

    shell
    # Get your user ID
    docker exec -u postgres helicone psql -d helicone_test -c \
      "SELECT id, email FROM \"user\" WHERE email = 'you@example.com';"
    
    # Create the organization and note the returned ID
    docker exec -u postgres helicone psql -d helicone_test -c \
      "INSERT INTO organization (name, is_personal) VALUES ('My Org', true) RETURNING id;"
    
    # Link the user to the organization as admin (substitute the IDs)
    docker exec -u postgres helicone psql -d helicone_test -c \
      "INSERT INTO organization_member (\"user\", organization, org_role) \
       VALUES ('USER_ID', 'ORG_ID', 'admin');"

    You can now sign in at the dashboard.

    Step 6: Route LLM traffic through Helicone

    Point your application's base URL at the Jawn proxy and pass your provider key plus your Helicone key. For OpenAI:

    shell
    curl --location 'https://jawn.example.com/v1/gateway/oai/v1/chat/completions' \
      --header "Content-Type: application/json" \
      --header "Authorization: Bearer $OPENAI_API_KEY" \
      --header "Helicone-Auth: Bearer $HELICONE_API_KEY" \
      --data '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Hello"}]}'

    For Anthropic, the proxy path is https://jawn.example.com/v1/gateway/anthropic/v1/messages. Vertex AI, AWS Bedrock, and Azure OpenAI are not supported in the self hosted build.

    Step 7: Lock down the proxy

    The Jawn proxy on 8585 does not authenticate the proxying path itself, so anyone who can reach it can route LLM requests through your endpoint and burn your provider credits. Two layers protect you:

    1. The raw port is bound to localhost and closed at the firewall, so it is only reachable through Caddy.
    2. Restrict who can reach jawn.example.com. If only your own backend calls it, add an IP allowlist in Caddy:
    shell
    jawn.example.com {
        @blocked not remote_ip 203.0.113.10 198.51.100.0/24
        respond @blocked "Forbidden" 403
        reverse_proxy 127.0.0.1:8585
    }

    Replace the addresses with the IPs of the servers that legitimately send traffic.

    Step 8: Backups

    The state lives in three Docker volumes. Dump Postgres and ClickHouse logically, and snapshot MinIO. Create /usr/local/bin/helicone-backup.sh:

    shell
    #!/usr/bin/env bash
    set -euo pipefail
    STAMP=$(date +%Y%m%d-%H%M%S)
    DEST="/var/backups/helicone"
    mkdir -p "$DEST"
    
    # PostgreSQL logical dump
    docker exec -u postgres helicone pg_dump helicone_test | gzip > "$DEST/pg-$STAMP.sql.gz"
    
    # ClickHouse: back up the data volume via a throwaway container
    docker run --rm -v helicone-clickhouse:/data -v "$DEST":/backup alpine \
      tar czf "/backup/clickhouse-$STAMP.tar.gz" -C /data .
    
    # MinIO object data
    docker run --rm -v helicone-minio:/data -v "$DEST":/backup alpine \
      tar czf "/backup/minio-$STAMP.tar.gz" -C /data .
    
    # Retain the last 14 of each
    for p in pg clickhouse minio; do
      ls -1t "$DEST/$p-"* | tail -n +15 | xargs -r rm
    done
    shell
    sudo chmod +x /usr/local/bin/helicone-backup.sh
    echo "30 3 * * * deploy /usr/local/bin/helicone-backup.sh" | sudo tee /etc/cron.d/helicone-backup

    Push the archives off the box (rsync over SSH or object storage). For best consistency on a busy instance, run the ClickHouse and MinIO tar steps during a brief maintenance window, or stop the container first.

    Step 9: Updating

    shell
    cd /var/lib/helicone
    docker compose pull
    docker compose up -d

    Because the data is in named volumes, the pull and recreate keeps your history. Always take a backup before upgrading. To pin a version, replace latest with a specific image tag from Docker Hub.

    Troubleshooting

    • API calls fail with connection refused: the dashboard is trying to reach localhost:8585 instead of your public Jawn URL. Confirm NEXT_PUBLIC_HELICONE_JAWN_SERVICE is set to https://jawn.example.com and recreate the container. Verify with curl https://helicone.example.com/__ENV.js | grep JAWN.
    • Infinite redirect loop on the dashboard: NEXT_PUBLIC_IS_ON_PREM=true is missing from the env file.
    • "Invalid origin" on sign in: your URL variables mix origins. They must all use the same HTTPS hostnames.
    • All data gone after a restart: the container was run without the named volumes. Confirm the volumes block is present in the Compose file.
    • ClickHouse out of memory: the VPS is too small for your log volume. Move to a larger RamNode plan, or for very high volume look at the Helm chart with an external ClickHouse cluster.