Deploying to Hetzner VPS#
Automated Deployment (Recommended)#
Single-command deployment using infra/deploy.sh:
cd infra
cp secrets.env.example secrets.env
nano secrets.env # Fill in your values
./deploy.sh
See infra/README.md for prerequisites (hcloud CLI, Cloudflare token, Tailscale OAuth client).
The script handles: server creation, Docker setup, Tailscale, DNS, SSL, Telegram webhook, and GitHub Actions deploy key.
Only manual step: Complete Anthropic OAuth via the URL shown in deploy output.
Manual Deployment#
Step-by-step deployment using Docker Compose + Caddy + GitHub Actions.
Prerequisites#
- Hetzner Cloud account
- Domain name (for HTTPS)
- GitHub repo access
- Telegram bot token (from @BotFather)
- OpenAI API key (for embeddings)
Create Hetzner VPS#
- Go to Hetzner Cloud Console
- Create new project or select existing
- Click Add Server
- Configure:
- Location: Falkenstein or Nuremberg (cheapest)
- Image: Debian 13
- Type: CX33 (4 vCPU, 8GB RAM) - €5.49/mo
- SSH Key: Add your public key
- Name:
assistant
- Click Create & Buy Now
- Note the IP address
Firewall (Optional)#
In Hetzner Console → Firewalls → Create Firewall:
- TCP 22 (SSH)
- TCP 80 (HTTP - for Let's Encrypt)
- TCP 443 (HTTPS)
Apply to your server.
Point Domain to Server#
Add DNS A record:
assistant.yourdomain.com → YOUR_SERVER_IP
Wait 5-10 minutes for propagation.
Server Setup#
SSH in:
ssh root@YOUR_SERVER_IP
Update system:
apt update && apt upgrade -y
Install Docker:
apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
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/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Install mosh and git:
apt install -y mosh git
Install Tailscale (for secure access to internal services):
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up
Follow the link to authenticate with your Tailscale account. Note your server's Tailscale IP (100.x.x.x).
Install GitHub CLI:
(type -p wget >/dev/null || (apt update && apt install wget -y)) && mkdir -p -m 755 /etc/apt/keyrings && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg && cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && apt update && apt install gh -y
Authenticate with GitHub:
gh auth login
Clone the repo:
mkdir -p /opt
cd /opt
gh repo clone YOUR_USERNAME/assistant
cd assistant
Configure Environment#
Copy the example and customize:
cp .env.example .env
vim .env
Generate random strings for secrets:
openssl rand -hex 32 # For ANTHROPIC_PROXY_SESSION_SECRET
openssl rand -hex 16 # For TELEGRAM_WEBHOOK_SECRET_TOKEN
Key values to set:
TELEGRAM_BOT_TOKEN- from @BotFatherTELEGRAM_WEBHOOK_URL-https://assistant.yourdomain.com/webhookTELEGRAM_WEBHOOK_SECRET_TOKEN- generated aboveANTHROPIC_PROXY_SESSION_SECRET- generated aboveOPENAI_API_KEY- your OpenAI key
Configure Caddy#
Copy the example and customize:
cp Caddyfile.example Caddyfile
vim Caddyfile
Replace the example domains with your actual domains.
Deploy#
Start all services (production compose file is included in the repo):
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
Check status:
docker compose ps
docker compose logs -f app
Anthropic OAuth#
Complete one-time OAuth setup:
-
Open SSH tunnel to access the proxy:
# From your local machine ssh -L 4001:localhost:4001 root@YOUR_SERVER_IP -
Open http://localhost:4001/auth/device in your browser
-
Complete the OAuth flow
-
Copy the session ID and update
.env:# On server nano /opt/assistant/.env # Set ANTHROPIC_PROXY_SESSION_ID=your_session_id -
Restart:
cd /opt/assistant docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Set Telegram Webhook#
curl -X POST "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook" \
-H "Content-Type: application/json" \
-d '{
"url": "https://assistant.yourdomain.com/webhook",
"secret_token": "YOUR_WEBHOOK_SECRET_TOKEN"
}'
Verify:
curl "https://api.telegram.org/bot<BOT_TOKEN>/getWebhookInfo"
Setup Auto-Deploy#
On Server: Make deploy script executable#
The deploy.sh script is included in the repo. Just make it executable:
chmod +x /opt/assistant/deploy.sh
On Server: Add deploy SSH key#
# Generate deploy key (no passphrase)
ssh-keygen -t ed25519 -f ~/.ssh/deploy_key -N ""
cat ~/.ssh/deploy_key.pub
# Add this to GitHub repo: Settings → Deploy keys (read-only is fine)
cat ~/.ssh/deploy_key
# Copy the PRIVATE key for GitHub Actions secret
On GitHub: Add secrets#
Go to repo Settings → Secrets and variables → Actions, add:
HOST: Your server IPSSH_KEY: The private key from above
The workflow file (.github/workflows/deploy.yml) is already in the repo.
Now every push to main triggers automatic deployment.
Verify#
Test the bot:
- Open Telegram
- Message your bot
- Should respond!
Check health:
curl https://assistant.yourdomain.com/health
Letta Access via Tailscale#
Letta is accessible via Tailscale for direct API access and debugging.
Access Letta#
From any device on your Tailscale network, open:
http://YOUR_TAILSCALE_IP:8283
Or use the Tailscale hostname:
http://assistant:8283
Letta ADE (Web UI)#
You can also connect to Letta using the official ADE (Agent Development Environment):
- Go to https://app.letta.com
- Click "Self-Hosted Server"
- Enter:
http://YOUR_TAILSCALE_IP:8283 - If
LETTA_SERVER_PASSWORDis set, enter the password
API Examples#
# Health check
curl http://YOUR_TAILSCALE_IP:8283/v1/health
# List agents (with password)
curl -H "Authorization: Bearer YOUR_LETTA_SERVER_PASSWORD" \
http://YOUR_TAILSCALE_IP:8283/v1/agents
Monitoring#
Netdata is included for real-time monitoring, accessible via Tailscale.
Access Netdata#
From any device on your Tailscale network, open:
http://YOUR_TAILSCALE_IP:19999
Or use the Tailscale hostname:
http://assistant:19999
What you get#
- CPU, RAM, disk, network graphs (1-second resolution)
- Per-container Docker metrics (CPU, memory, I/O)
- ~2 weeks retention by default
Optional: Netdata Cloud#
For alerts and multi-server dashboards:
- Sign up at https://app.netdata.cloud
- Get your claim token
- Add
NETDATA_CLAIM_TOKEN=your_tokento.env - Restart:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Maintenance#
View logs#
cd /opt/assistant
docker compose logs -f # All services
docker compose logs -f app # Just the bot
docker compose logs -f letta # Just Letta
Restart services#
docker compose -f docker-compose.yml -f docker-compose.prod.yml restart
Manual deploy#
/opt/assistant/deploy.sh
Update SSL cert (automatic)#
Caddy handles Let's Encrypt automatically. No action needed.
Backup data#
# SQLite database
cp /opt/assistant/data/assistant.db ~/backup/
# Letta PostgreSQL (if needed)
docker compose exec letta pg_dump -U letta letta > ~/backup/letta.sql
Troubleshooting#
Container won't start#
docker compose logs <service>
docker compose ps -a
SSL not working#
docker compose logs caddy
# Ensure ports 80/443 are open in Hetzner firewall
# Ensure DNS is pointing to your server
Webhook not receiving#
# Test manually
curl -X POST https://assistant.yourdomain.com/webhook \
-H "Content-Type: application/json" \
-H "X-Telegram-Bot-Api-Secret-Token: YOUR_SECRET" \
-d '{"update_id": 1}'
Out of memory#
docker stats
free -h
# CX33 has 8GB, should be plenty
Cost#
| Item | Monthly |
|---|---|
| Hetzner CX33 | €5.49 |
| Domain | ~€1 |
| APIs | Usage-based |
| Total | ~€6.50 + API |
Architecture#
Internet Tailscale Network
│ │
▼ ▼
┌────────────────────────────────────────────────────────────┐
│ Hetzner CX33 (Debian 13 + Docker + Tailscale) │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Caddy (:80/:443) - auto-SSL │ │
│ │ ├─► app:3000 (assistant.*) │ │
│ │ └─► letta:8283 (letta.*) │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ ┌───────────────────────┐ │
│ │ app (Bun :3000) │ │ netdata (:19999) │ │
│ │ Telegram webhook │ │ via Tailscale │ │
│ └────────┬─────────┘ └───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ letta (:8283) │◄─── also via Tailscale │
│ │ Agent + Memory │ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ litellm (:4000) │ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ auth-adapter │ │
│ │ (:4002) headers │ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ anthropic-proxy │ │
│ │ (:4001) OAuth │ │
│ └────────┬─────────┘ │
└───────────────────┼────────────────────────────────────────┘
│
▼
Anthropic API