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:

Self-Hosted Deployment Topology Your Infrastructure CLI / Browser tank install, UI Web App + API TanStack Start :3000 PostgreSQL 17 :5432 RustFS (S3) :9000 (tarballs) Security Scanner FastAPI + Python 3.14 :8000 (6-stage pipeline) Ollama (optional) local LLM analysis Grafana + Loki :3001 (monitoring)
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 storageRustFS (S3-compatible)Skill tarballs

Docker Image Tags

TagSourceStabilityUse Case
latestLatest v* tagStableProduction self-hosted
v0.8.1Specific versionPinnedVersion-locked deployments
nightlyLatest main branchUnstableTesting upcoming features
sha-abc1234Specific commitPinnedDebugging specific builds

Images are published to ghcr.io/tankpkg/:

  • ghcr.io/tankpkg/tank-web — Registry web app + API
  • ghcr.io/tankpkg/tank-scanner — Security scanner (FastAPI)

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

Quick install: Run bash scripts/onprem-install.sh for an interactive guided setup that handles everything below automatically.

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 (RustFS / S3)
STORAGE_BACKEND=s3
S3_ACCESS_KEY=tank
S3_SECRET_KEY=changeme123456
AWS_REGION=us-east-1
S3_BUCKET=tank-skills
S3_ENDPOINT=http://rustfs: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"

  rustfs:
    image: rustfs/rustfs:latest
    environment:
      RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY:-tank}
      RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:-changeme123456}
      RUSTFS_VOLUMES: /data
      RUSTFS_ADDRESS: 0.0.0.0:9000
      RUSTFS_CONSOLE_ADDRESS: 0.0.0.0:9001
      RUSTFS_CONSOLE_ENABLE: "true"
    volumes:
      - rustfs_data:/data
    ports:
      - "9000:9000"
      - "9001:9001"

  scanner:
    image: ghcr.io/tankpkg/tank-scanner:latest
    environment:
      - DATABASE_URL=${DATABASE_URL}
    ports:
      - "8000:8000"
    depends_on:
      - postgres

  web:
    image: ghcr.io/tankpkg/tank-web:latest
    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
      - rustfs
      - scanner

volumes:
  postgres_data:
  rustfs_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

Option A: Setup Wizard (recommended for first-time setup)

Visit your registry URL — you'll be redirected to /setup automatically. The wizard walks through database verification, URL configuration, storage, admin account creation, auth providers, and scanner setup.

The wizard is only available when TANK_MODE=selfhosted. After initial setup completes, the wizard is permanently locked (returns 403).

Option B: Headless Bootstrap (for automated deployments)

Set environment variables before starting:

FIRST_ADMIN_EMAIL=[email protected] \
FIRST_ADMIN_PASSWORD=securepassword123 \
docker compose -f infra/docker-compose.yml up -d

Or bootstrap after startup:

docker compose exec registry bun run scripts/bootstrap-headless.ts

The bootstrap script:

  • Creates the admin user with emailVerified=true and role=admin
  • Idempotent — promotes existing user to admin if they already exist
  • Uses bcrypt password hashing (compatible with Better Auth credential login)

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
RustFSlatestS3-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:
    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

rustfs:
  enabled: true
  auth:
    accessKey: tank
    secretKey: "" # 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 Docker startfalse
FIRST_ADMIN_EMAILBootstrap admin on first boot
FIRST_ADMIN_PASSWORDAdmin password (min 8 chars)
TANK_REGISTRY_URLOverride default registry URLhttps://www.tankpkg.dev
SESSION_STOREmemory or redismemory

Storage

VariableDescriptionDefault
STORAGE_BACKENDs3, supabase, or filesystemsupabase
S3_BUCKETBucket name for tarballs
S3_ENDPOINTS3 endpoint (for RustFS)
S3_ACCESS_KEYS3 access key
S3_SECRET_KEYS3 secret key
S3_REGIONS3 regionus-east-1
STORAGE_FS_PATHLocal path for filesystem storage/app/data/packages

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 RustFS 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

RustFS bucket not found

RustFS auto-creates buckets on first write. If you need to create one manually:

docker compose exec rustfs mc alias set local http://localhost:9000 tank changeme123456
docker compose exec rustfs 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 packages, docs, and navigate Tank