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:
| Workload | vCPU | RAM | Disk |
|---|---|---|---|
| Light, evaluation | 2 | 4 GB | 40 GB |
| Steady production | 4 | 8 GB | 80 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.comfor the dashboardjawn.example.comfor the API and proxys3.example.comfor MinIO
- SSH access as root or a sudo user.
Step 1: Host hardening
Create a non-root sudo user:
adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deployConfirm key based login as deploy, then harden SSH in /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication nosudo systemctl reload sshSet up the firewall. Only SSH, HTTP, and HTTPS are public; everything else is reached through the reverse proxy.
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 enableEnable automatic security updates:
sudo apt -y install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgradesStep 2: Install Docker
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 deployLog 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:
sudo mkdir -p /var/lib/helicone
sudo chown -R deploy:deploy /var/lib/helicone
cd /var/lib/heliconeCreate /var/lib/helicone/.env. Generate a strong auth secret first:
echo "BETTER_AUTH_SECRET=$(openssl rand -base64 32)" > /var/lib/helicone/.envThen append your public URLs and storage settings to the same file:
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-storageCreate /var/lib/helicone/docker-compose.yml:
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:
docker compose up -d
docker compose logs -fThe first start takes a few minutes while it runs database migrations.
Step 4: Reverse proxy with automatic TLS
Install Caddy:
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 caddyReplace /etc/caddy/Caddyfile with three site blocks:
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
}sudo systemctl reload caddyAll 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:
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:
# 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:
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:
- The raw port is bound to localhost and closed at the firewall, so it is only reachable through Caddy.
- Restrict who can reach
jawn.example.com. If only your own backend calls it, add an IP allowlist in Caddy:
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:
#!/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
donesudo chmod +x /usr/local/bin/helicone-backup.sh
echo "30 3 * * * deploy /usr/local/bin/helicone-backup.sh" | sudo tee /etc/cron.d/helicone-backupPush 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
cd /var/lib/helicone
docker compose pull
docker compose up -dBecause 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:8585instead of your public Jawn URL. ConfirmNEXT_PUBLIC_HELICONE_JAWN_SERVICEis set tohttps://jawn.example.comand recreate the container. Verify withcurl https://helicone.example.com/__ENV.js | grep JAWN. - Infinite redirect loop on the dashboard:
NEXT_PUBLIC_IS_ON_PREM=trueis 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
volumesblock 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.
