Skip to content
AI/LLM: This documentation page is available in plain markdown format at /docs/self-hosting.md

Self-Hosting Tank

Deploy your own Tank registry for complete control over AI agent skill distribution and security scanning. Self-hosting provides data sovereignty, air-gapped support, custom security policies, and the ability to meet compliance requirements (SOC2, HIPAA, FedRAMP).

Architecture Overview

A self-hosted Tank deployment runs four core services:

┌─────────────────────────────────────────────────────────────────┐
│                       Your Infrastructure                        │
│                                                                  │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────────────┐│
│  │  Web app     │──▶│  PostgreSQL  │   │   MinIO / S3         ││
│  │  Web + API   │   │   17+        │   │   (tarball storage)  ││
│  │  (port 3000) │   │  (port 5432) │   │   (port 9000)        ││
│  └──────┬───────┘   └──────────────┘   └──────────────────────┘│
│         │                                                        │
│         │           ┌──────────────┐                            │
│         └──────────▶│   FastAPI    │                            │
│                     │   Security   │                            │
│                     │   Scanner    │                            │
│                     │  (port 8000) │                            │
│                     └──────────────┘                            │
└─────────────────────────────────────────────────────────────────┘
ServiceTechnologyPurpose
Web appTanStack StartRegistry UI, REST API, CLI backend
Security scannerFastAPI + Python 3.146-stage security scanning pipeline
DatabasePostgreSQL 17+Skills, versions, users, audit logs
Object storageMinIO (S3-compatible)Skill tarballs

Deployment Modes

Tank uses TANK_MODE to separate cloud and self-hosted behavior:

ModeValueDescription
Cloudcloud (default)Used for tankpkg.dev. Setup wizard disabled, all config from env vars.
Self-HostedselfhostedEnables setup wizard, automatic DB migrations, filesystem storage.

Docker Compose sets TANK_MODE=selfhosted automatically. If you deploy to Vercel or another PaaS, leave it unset (defaults to cloud).

Prerequisites

  • Docker and Docker Compose (for Docker deployment)
  • Helm 3+ and kubectl (for Kubernetes deployment)
  • 4 GB RAM minimum (8 GB recommended for production)
  • 20 GB disk (for database and storage)
  • Node.js 24+ and Python 3.14+ (for source builds only)

Docker Compose Deployment

1) Download and Configure

You don't need to clone the repository. Download the production compose file:

mkdir tank && cd tank
curl -fsSL https://raw.githubusercontent.com/tankpkg/tank/main/infra/docker-compose.production.yml -o docker-compose.yml

Create your .env file:

# Database
DATABASE_URL=postgresql://tank:password@postgres:5432/tank

# Auth
BETTER_AUTH_SECRET=$(openssl rand -base64 32)
APP_URL=https://tank.yourcompany.com
GITHUB_CLIENT_ID=your-github-oauth-app-id
GITHUB_CLIENT_SECRET=your-github-oauth-app-secret

# Storage (MinIO / S3)
STORAGE_BACKEND=s3
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
AWS_REGION=us-east-1
S3_BUCKET=tank-skills
S3_ENDPOINT=http://minio:9000

# Security scanner
PYTHON_API_URL=http://scanner:8000

# Admin bootstrap
FIRST_ADMIN_EMAIL=[email protected]

2) Docker Compose Services

The docker-compose.yml defines four services (plus optional Ollama for local LLM analysis):

version: "3.9"

services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: tank
      POSTGRES_USER: tank
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  minio:
    image: minio/minio:latest
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin
    volumes:
      - minio_data:/data
    ports:
      - "9000:9000"
      - "9001:9001"

  scanner:
    build:
      context: ./apps/python-api
    environment:
      - DATABASE_URL=${DATABASE_URL}
    ports:
      - "8000:8000"
    depends_on:
      - postgres

  web:
    build:
      context: .
      dockerfile: apps/registry/Dockerfile
    environment:
      - TANK_MODE=selfhosted
      - DATABASE_URL=${DATABASE_URL}
      - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
      - PYTHON_API_URL=${PYTHON_API_URL}
    ports:
      - "3000:3000"
    depends_on:
      - postgres
      - minio
      - scanner

volumes:
  postgres_data:
  minio_data:

Optional: Local LLM analysis with Ollama

# Start with LLM support (adds ollama service)
docker compose --profile llm-local up -d

3) Build and Run

# Start all services
docker compose up -d

# Check service health
docker compose ps

# View logs
docker compose logs -f web
docker compose logs -f scanner

4) Run Database Migrations

docker compose exec web bun --filter=@tankpkg/web drizzle-kit push

Or run as a one-shot migration container:

docker compose run --rm web bun --filter=@tankpkg/web drizzle-kit push

5) Bootstrap the Admin User

docker compose exec web bun --filter=@tankpkg/web admin:bootstrap

This promotes FIRST_ADMIN_EMAIL to the admin role. The user must sign in with GitHub OAuth first (creating their account), then run bootstrap.

Instead of manually configuring environment variables, the setup wizard provides a guided 7-step process:

  1. Visit http://localhost:3000/setup
  2. Configure database, URL, storage, admin account, auth providers, and scanner
  3. All settings are encrypted and stored in the database

The wizard is only available when TANK_MODE=selfhosted and no setup has been completed yet. After initial setup, the wizard is permanently locked (returns 403).

6) Health Verification

# API health check
curl http://localhost:3000/api/health

# Test skill listing
curl http://localhost:3000/api/v1/skills

# Test scanner connectivity
curl http://localhost:8000/health

# Verify CLI can connect
tank search hello

Kubernetes Helm Chart Deployment

For production Kubernetes deployments, use the included Helm chart.

Chart Location

infra/helm/tank/
├── Chart.yaml
├── values.yaml
└── templates/
    ├── web-deployment.yaml
    ├── scanner-deployment.yaml
    ├── ingress.yaml
    └── ...

Chart Dependencies

The infra/helm/tank/ chart includes:

DependencyVersionPurpose
PostgreSQL15.5.38Primary database
MinIO5.4.0S3-compatible object storage

Quick Start

# Update Helm dependencies
helm dependency update infra/helm/tank/

# Install into the tank namespace
helm install tank infra/helm/tank/ \
  --namespace tank \
  --create-namespace \
  --set secrets.betterAuthSecret="$(openssl rand -base64 32)" \
  --set web.env.GITHUB_CLIENT_ID="your-client-id" \
  --set web.env.GITHUB_CLIENT_SECRET="your-client-secret" \
  --set web.env.FIRST_ADMIN_EMAIL="[email protected]"

Key Helm Values

# values.yaml — key configuration options

web:
  replicaCount: 2
  image:
    repository: ghcr.io/tankpkg/tank-web
    tag: latest
  env:
    NEXT_PUBLIC_APP_URL: "https://tank.yourcompany.com"
    GITHUB_CLIENT_ID: ""
    GITHUB_CLIENT_SECRET: ""
    FIRST_ADMIN_EMAIL: ""

scanner:
  replicaCount: 1
  image:
    repository: ghcr.io/tankpkg/tank-scanner
    tag: latest
  resources:
    requests:
      memory: "512Mi"
      cpu: "250m"
    limits:
      memory: "2Gi"
      cpu: "1000m"

secrets:
  betterAuthSecret: "" # Required: openssl rand -base64 32

ingress:
  enabled: true
  className: "nginx"
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
  hosts:
    - host: tank.yourcompany.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: tank-tls
      hosts:
        - tank.yourcompany.com

autoscaling:
  enabled: false
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

postgresql:
  enabled: true
  auth:
    database: tank
    username: tank
    password: "" # Set via --set or sealed secret

minio:
  enabled: true
  auth:
    rootUser: minioadmin
    rootPassword: "" # Set via --set or sealed secret

Run Migrations on Helm Install

Enable the migration job in values.yaml:

dbMigration:
  enabled: true
  force: false

Or force-run on first install:

helm install tank infra/helm/tank/ \
  --namespace tank \
  --create-namespace \
  --set dbMigration.force=true \
  --set secrets.betterAuthSecret="$(openssl rand -base64 32)"

Upgrade

helm upgrade tank infra/helm/tank/ \
  --namespace tank \
  --reuse-values

Environment Variables Reference

Required

VariableDescription
DATABASE_URLPostgreSQL connection string
BETTER_AUTH_SECRETSession encryption key (min 32 chars)
GITHUB_CLIENT_IDGitHub OAuth App client ID
GITHUB_CLIENT_SECRETGitHub OAuth App client secret
PYTHON_API_URLSecurity scanner base URL

Self-Hosted Mode

VariableDescriptionDefault
TANK_MODEcloud or selfhostedcloud
AUTO_MIGRATERun DB migrations on startupfalse
FIRST_ADMIN_EMAILCreate admin on first boot
FIRST_ADMIN_PASSWORDAdmin password (min 8 chars)

Storage

VariableDescriptionDefault
STORAGE_BACKENDsupabase, s3, or filesystemsupabase
STORAGE_FS_PATHLocal path for filesystem storage/app/data/packages
S3_BUCKETBucket name for tarballs
S3_ENDPOINTS3 endpoint (for MinIO)
AWS_ACCESS_KEY_IDS3 access key
AWS_SECRET_ACCESS_KEYS3 secret key
AWS_REGIONS3 regionus-east-1

Optional

VariableDescriptionDefault
FIRST_ADMIN_EMAILBootstraps admin role on first run
OIDC_ISSUEROIDC SSO issuer URL
OIDC_CLIENT_IDOIDC client ID
OIDC_CLIENT_SECRETOIDC client secret
RESEND_API_KEYResend email service key

Operational Notes

  • Scanner: Security scanner code lives in apps/python-api/.
  • Turbo builds: Use bun turbo build --filter=@tankpkg/web... for dependency-aware monorepo builds.
  • Auth secret: Never omit BETTER_AUTH_SECRET in production. Sessions fail silently without it.
  • Storage backend: Supabase is for cloud deployments. Use s3 with MinIO for self-hosted.

Monitoring

Access the included observability stack (Docker Compose only):

  • Grafana: http://localhost:3001 (default: admin/admin)
  • Loki: Log aggregation (configured in infra/loki/)
  • Prometheus: Metrics collection

Default dashboards include:

  • Tank API performance
  • Security scan latency and throughput
  • Skill publish and download rates
  • Active user sessions

Troubleshooting

Build fails with workspace package resolution

Use the monorepo-aware build command:

bun turbo build --filter=@tankpkg/web...

Auth runtime errors

Ensure BETTER_AUTH_SECRET is explicitly set in your deployment environment. It must be identical across all web replicas.

Scanner connectivity errors

Verify PYTHON_API_URL points to the scanner service. In Docker Compose, use the service name (http://scanner:8000). Check scanner health:

curl $PYTHON_API_URL/health

MinIO bucket not found

Create the bucket before starting the web app:

docker compose exec minio mc alias set local http://localhost:9000 minioadmin minioadmin
docker compose exec minio mc mb local/tank-skills

Helm chart: pods stuck in Pending

Check resource requests against available node capacity:

kubectl describe pod -n tank -l app=tank-web
kubectl get nodes -o wide

Adjust resources.requests in values.yaml if needed.

Command Palette

Search skills, docs, and navigate Tank