Deployment

Self-Hosting

Deploy Octopus on your own infrastructure and choose the AI services that process your code. External AI services receive code when configured.

Prerequisites

PostgreSQL 15+

Primary database for all application data.

Qdrant

Vector database for code embeddings and search.

Node.js 20+ or Bun

Runtime for the Next.js application.

OpenAI API key (default embeddings)

Used for code embeddings (text-embedding-3-large) — or run fully local via Ollama (see the all-local section).

You'll also need an AI provider key (Anthropic Claude or OpenAI) for the review engine.

Quick Start

The fastest way to run Octopus is the prebuilt Docker image — it needs no build step and brings up the app, PostgreSQL, and Qdrant together. Prefer to run against your own services instead? Switch to Without Docker.

1

Clone the repository

Code
git clone https://github.com/octopusreview/octopus.git
cd octopus
2

Create your .env file

Use the environment generator below to create a .env file with a pre-generated auth secret, then save it to the project root. Fill in your API keys before continuing.

3

Review docker-compose.selfhost.yml

The repository's docker-compose.selfhost.yml pulls the prebuilt public image ghcr.io/octopusreview/octopus-selfhost (no local build) and runs the webservice, PostgreSQL, and Qdrant together, with the database and Qdrant URLs wired for Docker's internal network. Use it as-is:

docker-compose.selfhost.yml
# Self-host deployment compose — pulls the prebuilt public image instead of
# building from source (see docker-compose.yml for the build-from-source dev
# variant). The image bakes NEXT_PUBLIC_OCTOPUS_SELF_HOSTED=true, so
# email/password sign-in is enabled out of the box.
#
#   docker compose -f docker-compose.selfhost.yml pull
#   docker compose -f docker-compose.selfhost.yml up -d
#
# Pin a release by exporting OCTOPUS_VERSION (e.g. OCTOPUS_VERSION=1.0.27);
# defaults to :latest. Migrations are NOT in the runtime image — run them from
# a checkout of the matching tag (see docs/self-hosting).
#
# Change the published port by exporting OCTOPUS_PORT (defaults to 43300):
#   OCTOPUS_PORT=8080 docker compose -f docker-compose.selfhost.yml up -d
services:
  web:
    image: ghcr.io/octopusreview/octopus-selfhost:${OCTOPUS_VERSION:-latest}
    ports:
      - "${OCTOPUS_PORT:-43300}:3000"
    # env_file delivers the operator's secrets into the container (the
    # environment: block only overrides the compose-internal service URLs).
    env_file:
      - .env
    environment:
      - DATABASE_URL=postgresql://octopus:octopus@postgres:5432/octopus
      - QDRANT_URL=http://qdrant:6333
      - ENABLE_REVIEW_WORKERS=true
    depends_on:
      postgres:
        condition: service_healthy
      qdrant:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: postgres:17-alpine
    ports:
      - "43332:5432"
    environment:
      POSTGRES_USER: octopus
      POSTGRES_PASSWORD: octopus
      POSTGRES_DB: octopus
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U octopus"]
      interval: 5s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  qdrant:
    image: qdrant/qdrant:v1.17.0
    ports:
      - "43333:6333"
      - "43334:6334"
    volumes:
      - qdrant_data:/qdrant/storage
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:6333/readyz"]
      interval: 5s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  postgres_data:
  qdrant_data:
4

Pull and run

Code
export OCTOPUS_VERSION=latest   # or a pinned release, e.g. 1.0.27
docker compose -f docker-compose.selfhost.yml pull
docker compose -f docker-compose.selfhost.yml up -d

The public image is built with NEXT_PUBLIC_OCTOPUS_SELF_HOSTED=true already baked in, so email/password sign-in and the first-boot admin work out of the box — no build step and no build args needed. (To build from source instead, use docker-compose.yml with docker compose build --build-arg NEXT_PUBLIC_OCTOPUS_SELF_HOSTED=true.)

Octopus is then available at http://localhost:43300. To publish on a different port, set OCTOPUS_PORT (defaults to 43300):

Code
OCTOPUS_PORT=8080 docker compose -f docker-compose.selfhost.yml up -d
5

Run database migrations

Migrations run from the repo checkout — the runtime image doesn't ship the Prisma CLI or migration files.

Code
cd packages/db
DATABASE_URL=postgresql://octopus:octopus@localhost:43332/octopus bunx prisma migrate deploy
6

Open Octopus

Visit http://localhost:43300 to access your self-hosted Octopus instance. Create your first account and connect a GitHub, GitLab, Bitbucket or Forgejo repository to get started.

All-local with Ollama (optional)

To run Octopus with no cloud API keys — both the review LLM and code embeddings served on your own hardware — start the optional Ollama overlay alongside the base compose file. It adds an ollama service and points the app at it.

Code
docker compose -f docker-compose.yml -f docker-compose.ollama.yml up -d

Then pull at least one chat model and the embedding model — from the UI (Settings → Models → Local models) or the shell:

Code
docker compose exec ollama ollama pull qwen2.5-coder:7b
docker compose exec ollama ollama pull nomic-embed-text

To also use Ollama for embeddings, set OCTOPUS_EMBED_PROVIDER=ollama, OCTOPUS_EMBED_MODEL=nomic-embed-text, and OCTOPUS_EMBED_DIM=768 in your .env before first indexing — switching providers afterward requires a re-index since different models produce non-comparable vectors. Ollama runs CPU-only by default; see the overlay file for enabling NVIDIA GPU acceleration.

Environment Variables

Generate a default .env file with pre-filled defaults for database, Qdrant, and auth. A unique BETTER_AUTH_SECRET is generated automatically. Configure your AI services before starting. GitHub App credentials are needed only if you connect GitHub repositories. Forgejo credentials are entered in Settings → Integrations after sign-in.

.env
# Database (overridden by docker-compose when using Docker)
DATABASE_URL=postgresql://octopus:octopus@localhost:43332/octopus

# Qdrant (overridden by docker-compose when using Docker)
QDRANT_URL=http://localhost:43333
QDRANT_API_KEY=

# Auth
BETTER_AUTH_SECRET=711881def823af44a118e5136c72c896a622ffc03c93ad376898987e9c86e5a3
BETTER_AUTH_URL=http://localhost:43300

# Data encryption key (32 bytes hex). Encrypts OAuth tokens and per-org AI
# provider keys at rest. Decoupled from BETTER_AUTH_SECRET so the auth secret
# can rotate without invalidating encrypted data.
OCTOPUS_DATA_KEY=a682a0c9788814a43688edba428e05ebfd15fa2a451ca57d0eb2f03f0f4bb6d9

# AI Providers
OPENAI_API_KEY=
ANTHROPIC_API_KEY=

# GitHub App (only when connecting GitHub repositories)
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
GITHUB_WEBHOOK_SECRET=
GITHUB_APP_CLIENT_ID=
GITHUB_APP_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
NEXT_PUBLIC_GITHUB_APP_SLUG=

# Private Forgejo (optional; requires web and review workers on the LAN/VPN)
# Comma-separated exact HTTPS origins without paths or wildcards.
# Runtime server flag for custom/prebuilt images. The official self-host image
# already has NEXT_PUBLIC_OCTOPUS_SELF_HOSTED=true baked in at build time.
OCTOPUS_SELF_HOSTED=true
FORGEJO_ALLOWED_PRIVATE_ORIGINS=
# Mounted trusted PEM CA file for internal certificates, if needed.
# Never disable TLS verification.
NODE_EXTRA_CA_CERTS=

# Optional
COHERE_API_KEY=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=

Required — fill these in

OPENAI_API_KEYrequired
Used for embeddings
ANTHROPIC_API_KEY
Claude for reviews

GitHub integration only

GITHUB_APP_ID
123456
GITHUB_APP_PRIVATE_KEY
-----BEGIN RSA...
GITHUB_WEBHOOK_SECRET
whsec_...
GITHUB_APP_CLIENT_ID
GitHub App user authorization
GITHUB_APP_CLIENT_SECRET
GitHub App user authorization
GITHUB_CLIENT_ID
Iv1.abc123
GITHUB_CLIENT_SECRET
secret

Pre-filled defaults

DATABASE_URLrequired
postgresql://octopus:octopus@localhost:43332/octopus
QDRANT_URLrequired
http://localhost:43333
BETTER_AUTH_SECRETrequired
Auto-generated (64-char hex)
BETTER_AUTH_URLrequired
http://localhost:43300

Optional

GOOGLE_API_KEY
Gemini models
GROK_API_KEY
xAI Grok models
OPENROUTER_API_KEY
OpenRouter — many model vendors via one key
DASHSCOPE_API_KEY
Alibaba Cloud Model Studio (Qwen) models
DASHSCOPE_BASE_URL
Optional: DashScope endpoint override (default is the international endpoint; set the China endpoint here)
OLLAMA_SERVER_URL
Self-hosted Ollama (ollama: model ids); optional OLLAMA_USERNAME / OLLAMA_PASSWORD for a proxied host
ACP_BASE_URL
ACPX gateway (acp: model ids); set together with ACP_API_KEY
OPENCODE_BASE_URL
OpenCode gateway (opencode: model ids); set together with OPENCODE_API_KEY
QDRANT_API_KEY
If Qdrant auth is enabled
FORGEJO_ALLOWED_PRIVATE_ORIGINS
Comma-separated exact HTTPS origins for private Forgejo instances. Self-hosted only: use the official self-host image or OCTOPUS_SELF_HOSTED=true at runtime. Configure on web and review workers.
NODE_EXTRA_CA_CERTS
Path to a mounted PEM CA certificate file if your Forgejo instance uses an internal CA. Keep TLS verification enabled.
COHERE_API_KEY
For reranking search results
STRIPE_SECRET_KEY
For billing

Database Setup

Run migrations to set up the database schema. Migrations run from the repo checkout — the runtime image doesn't ship the Prisma CLI or migration files.

Code
cd packages/db
DATABASE_URL=postgresql://octopus:octopus@localhost:43332/octopus bunx prisma migrate deploy

GitHub App Setup

If you use GitHub repositories, create a GitHub App to receive events:

  1. Go to GitHub Settings → Developer settings → GitHub Apps
  2. Create a new GitHub App with a webhook URL pointing to https://your-domain/api/github/webhook
  3. Enable permissions: Pull requests (read/write), Contents (read), Checks (read/write)
  4. Subscribe to events: Pull request, Pull request review
  5. Generate a private key and add it to your environment

Self-hosted Octopus + private Forgejo: direct connection

These steps apply when you run Octopus yourself. For Octopus Cloud with a private LAN/VPN instance, use the local connector setup instead. In your own Octopus Settings → Integrations, connect your HTTPS Forgejo instance with a personal access token. Configure a signed pull request webhook for each repository using the URL and secret shown in Octopus. See the Forgejo setup guide for token permissions and webhook setup.

To connect a private LAN or VPN-only instance, run self-hosted Octopus within that network. The official self-host image has NEXT_PUBLIC_OCTOPUS_SELF_HOSTED=true baked in at build time. For a custom or other prebuilt image, enable server-side self-hosting with OCTOPUS_SELF_HOSTED=true at runtime. Either enables self-hosted access; both flags are not required. Then set FORGEJO_ALLOWED_PRIVATE_ORIGINS to a comma-separated list of the exact HTTPS origins you operate, including a port if it is not 443. An origin contains the scheme and host, with no path or wildcard. The allowlist is ignored on Octopus Cloud.

Configure the same allowlist on the web application and review workers. Both need routes and DNS access to Forgejo; Forgejo must also be able to deliver webhooks to Octopus. If the webhook target is private, add its exact hostname or IP to Forgejo's [webhook] ALLOWED_HOST_LIST, preserving existing entries. Forgejo's default external setting blocks private destinations. Restart those processes after changing the environment. Private network access alone does not authorize a host; its origin must be listed explicitly.

For an internal certificate authority, mount its trusted PEM certificate file and set NODE_EXTRA_CA_CERTS to that file's path in each process. HTTPS verification stays enabled. HTTP, redirects, loopback, link-local addresses and cloud metadata endpoints remain blocked, even for an allowed origin.

Self-hosting Forgejo controls where repositories live. Your Octopus deployment and configured AI services determine where code is processed for reviews. Use local AI services if processing must stay on your network.

Production Tips

Use connection pooling

Use PgBouncer or Supabase pooler for PostgreSQL connections. Multiple app instances and pg-boss worker processes can otherwise exhaust Postgres connection limits.

Secure Qdrant

Enable API key authentication on Qdrant and restrict network access. Never expose Qdrant directly to the internet.

Set a spend limit

Configure per-organization spend limits in the admin panel to control AI costs.

Enable HTTPS

Use a reverse proxy (nginx, Caddy, Traefik) with TLS termination. Required for OAuth callbacks.

Upgrading & rolling back

Check out a specific release tag in production rather than tracking master — that is what turns a rollback into re-checking-out the previous tag for the matching compose file and migrations. The runtime image itself is pulled from GHCR.

1

Upgrade

Check out the new tag (for the compose file + migration files), pull the new image, apply migrations, then roll. Octopus migrations are additive (expand-only) and therefore backward-compatible — the previous image keeps working against the new schema, which is exactly what makes the rollback below safe.

Code
git fetch --tags && git checkout vX.Y.Z
export OCTOPUS_VERSION=X.Y.Z
docker compose -f docker-compose.selfhost.yml pull
# migrate FIRST (expand-only, safe under the still-running old version) ...
cd packages/db && DATABASE_URL=postgresql://octopus:octopus@localhost:43332/octopus bunx prisma migrate deploy && cd ../..
# ... then roll to the new version
docker compose -f docker-compose.selfhost.yml up -d
2

Verify before sending traffic

Confirm the app is healthy before pointing users at the new version:

Code
curl -fsS http://localhost:43300/api/health    # expect {"status":"ok"}
curl -fsS http://localhost:43300/api/version   # confirm the new version
3

Roll back (if needed)

Point OCTOPUS_VERSION at the previous release and roll. Do not roll back the database. Because every migration is additive, the older image runs fine against the newer schema, so you keep all data and avoid a risky down-migration.

Code
export OCTOPUS_VERSION=X.Y.Z    # the previous release
docker compose -f docker-compose.selfhost.yml pull
docker compose -f docker-compose.selfhost.yml up -d

This expand-only discipline is enforced in CI: the migrate-checkworkflow fails any change whose migration drops or rewrites a table/column (without an explicit override), so the "roll back the code, keep the database" path stays safe from one release to the next. The same property is what lets a hosted, blue-green / rolling deploy run both versions against a single shared database during cutover.

Zero-downtime cutover (Cloudflare LB)

For zero-downtime deploys, front two legs (e.g. your datacenter and a cloud VM) with a Cloudflare Load Balancer. Both legs are stateless app containers pulling the same image and pointing at one shared database (and Qdrant/Redis) — Octopus is safe to run as multiple app instances on one DB, since pg-boss coordinates workers and dedupes the scheduled jobs across them.

  • Create a Cloudflare LB with one pool per leg and a monitor that probes /api/health (it returns 200 only when the leg can reach the database, else 503). Cloudflare drops an unhealthy leg from rotation automatically.
  • Deploy the new tag to the idle leg, let its /api/healthgo green, then point the LB's default pool at it. Keep the previous leg running — rollback is an instant flip back.
  • The included deployworkflow automates this (deploy → health-gate → Cloudflare cutover → verify); set the documented Cloudflare token / LB & pool IDs as repo secrets and variables.

Note: this is zero-downtime deploys against one shared DB. True survive-a-whole-site-outage HA additionally needs that database (and Qdrant) replicated across both legs with failover — a separate, larger piece of work.