Storage & Self-hosting

Self-hosting Cap Web

How to deploy Cap Web on your own infrastructure

Cap Web is our web application for uploading and sharing Caps - it's what runs right here on Cap.so. You can upload videos to it from the dashboard or from Cap Desktop.

Quick Start (One Command)

The fastest way to self-host Cap is with Docker Compose. This single command will start everything you need:

git clone https://github.com/CapSoftware/Cap.git
cd Cap
docker compose up -d

That's it! Cap will be available at http://localhost:3000.

Security Notice for Production Deployments

The default docker-compose.yml includes placeholder secrets for quick local testing. If you're deploying to a public server or production environment, you must change these values. See the Production Checklist below.

Login codes will appear in the logs until email is configured, and the verify screen says "Email (Resend) is not configured":

docker compose logs cap-web

Deployment Options

Best for VPS, home servers, or any Docker-capable host.

What's included:

  • Cap Web application
  • Media server (FFmpeg processing)
  • MySQL database
  • MinIO (S3-compatible storage)

Steps:

  1. Clone the repository
  2. Run docker compose up -d
  3. Access Cap at http://localhost:3000

Note: The main docker-compose.yml builds the media server from source, so a full clone of the repository is required. The Coolify compose file uses a pre-built image from ghcr.io instead.

Custom configuration:

Create a .env file to customize your deployment:

# Public URL (required for production)
CAP_URL=https://cap.yourdomain.com
 
# S3 public URL (for video playback)
S3_PUBLIC_URL=https://s3.yourdomain.com
 
# Optional: Custom ports
CAP_PORT=3000
MINIO_PORT=9000
 
# Optional: Custom passwords (auto-generated if not set)
MYSQL_PASSWORD=your-secure-password
MINIO_ROOT_PASSWORD=your-minio-password

Browsers upload recordings straight to the storage bucket. If you point Cap at your own S3-compatible bucket instead of the bundled MinIO, configure the bucket's CORS for your Cap URL and expose the ETag response header (see the CORS sections for AWS S3 and Cloudflare R2). Without ETag, recordings from the web recorder and the Chrome extension fail to upload.

Option 2: Railway (One-Click)

Deploy on Railway

Railway provides a fully managed deployment with automatic SSL and scaling. Login credentials appear in Railway's Deploy Logs after deployment.

Option 3: Coolify

For Coolify users, use docker-compose.coolify.yml which includes environment variable placeholders:

  1. Create a new Docker Compose project in Coolify
  2. Point to the Cap repository
  3. Set compose file to docker-compose.coolify.yml
  4. Configure environment variables in Coolify's UI
  5. Deploy

Note: The Coolify compose file uses slightly different environment variable names: WEB_URL instead of CAP_URL, and S3_PUBLIC_ENDPOINT instead of S3_PUBLIC_URL.

Connecting Cap Desktop

  1. Open Cap Desktop settings
  2. Set 'Cap Server URL' to your deployment URL (e.g., https://cap.yourdomain.com)
  3. Uploads will now go to your self-hosted instance

Connecting the Chrome Extension

The Chrome Web Store extension signs in through your server. Outside of localhost development, Cap Web only hands an auth key to the extension ID set in CAP_CHROME_EXTENSION_ID, so set it to the Web Store extension's ID:

CAP_CHROME_EXTENSION_ID=fefjaffcodfiogbbngmjkcjpbpclbdcp

Without it, extension sign-in fails with a bad request. Then open the extension's options and set the Cap URL to your deployment URL.

Note: The compose files only pass a service the variables listed under its environment. If a variable from this page isn't listed there for the right service (cap-web or media-server), add it, not just to .env.

Restricting Who Can Sign In

# Only these domains can create an account
CAP_ALLOWED_SIGNUP_DOMAINS=yourcompany.com,yourcompany.org
 
# Domains or full addresses that can't sign in
CAP_BLOCKED_SIGNUP_DOMAINS=former-partner.com,someone@yourcompany.com
 
# New videos are private unless shared
CAP_VIDEOS_DEFAULT_PUBLIC=false
  • CAP_ALLOWED_SIGNUP_DOMAINS applies to new accounts only. Users who already have an account can still sign in from any domain.
  • CAP_BLOCKED_SIGNUP_DOMAINS applies to existing users too: a blocked address can't sign in, and its existing web sessions and Cap app sign-ins (desktop, Chrome extension) stop working.
  • Neither setting adds users to an organization. Unless WorkOS SSO adds them to its organization, new users who weren't invited start in their own organization.
  • CAP_VIDEOS_DEFAULT_PUBLIC defaults to true. An organization whose default video visibility is set to private keeps new videos private either way.

Email Configuration

Email is optional. Without it, login codes are printed to the cap-web logs, the verify screen shows "Email (Resend) is not configured", and invite or notification emails are skipped (invite links still work). Cap only sends email when both variables below are set; if Resend rejects a message, the error is logged and the sign-in fails instead of claiming a code was sent. To send emails:

  1. Create an account at Resend
  2. Add these environment variables:
    RESEND_API_KEY=re_xxxxx
    RESEND_FROM_DOMAIN=yourdomain.com
  3. Verify the domain in Resend. Login emails are sent from auth@RESEND_FROM_DOMAIN.

Media Server Capacity

The media server runs a limited number of video jobs at once. Unless you set a limit, it uses the smallest of 4 and half the CPUs, and, if the container has a memory limit, the memory limit × 0.85 ÷ 768 MB (at least 1). A media server with 2 CPUs therefore processes one video at a time.

When it is full, it answers 503 Server is busy. Video processing waits and retries until a slot frees up, so videos queue behind each other instead of failing. The faststart rewrite requested right after a browser upload is the exception: it isn't retried, so it's skipped if the server is full at that moment. To process more at once, set the limits on the media server container:

MEDIA_SERVER_MAX_CONCURRENT_VIDEO_PROCESSES=2   # Video jobs at once
MEDIA_SERVER_MAX_CONCURRENT_DIRECT_CONVERSIONS=1 # Direct conversions at once (default 1)

Raise them only if the container has the memory for it: each job is budgeted 768 MB.

Optional Features

AI Features

For video transcription and AI features (titles, summaries, chapters, transcript translation, and docs Ask AI):

ASSEMBLY_API_KEY=your-key    # Transcription
GROQ_API_KEY=your-key        # AI features
OPENAI_API_KEY=your-key      # AI features
ANTHROPIC_API_KEY=your-key   # AI features

When AI_PROVIDER is not set, AI providers are auto-detected from whichever keys are present. Generation features (titles, summaries, chapters, translation) prefer Groq → OpenAI → Anthropic; chat features (the support messenger and docs Ask AI) prefer Anthropic → OpenAI → Groq. Earlier providers are preferred; later ones are automatic fallbacks.

To pick a provider explicitly, set AI_PROVIDER to one of assemblyai, openai, anthropic, groq, or openai-compatible. Any other configured providers stay in the chain as fallbacks.

AI_PROVIDER=assemblyai

assemblyai reuses your existing ASSEMBLY_API_KEY via the AssemblyAI LLM Gateway, so one key covers transcription and AI features. It is never auto-detected: transcription-only instances don't start incurring LLM charges unless you opt in, and opting in bills LLM tokens to the same AssemblyAI account as transcription.

openai-compatible works with any OpenAI-compatible endpoint such as Ollama, OpenRouter, or LM Studio. It requires AI_BASE_URL and AI_MODEL (and AI_API_KEY if your endpoint needs one):

AI_PROVIDER=openai-compatible
AI_BASE_URL=http://localhost:11434/v1
AI_MODEL=llama3.3
AI_API_KEY=your-key          # If the endpoint requires one

Each provider has sensible default models. You can override them per role for the provider selected by AI_PROVIDER:

AI_MODEL=your-model          # Generation: titles, summaries, chapters, translation
AI_CHAT_MODEL=your-model     # Support chat
AI_STREAM_MODEL=your-model   # Streaming docs Ask AI

To keep transcription in the EU, set:

ASSEMBLY_BASE_URL=https://api.eu.assemblyai.com

For the AssemblyAI EU gateway, set AI_BASE_URL=https://llm-gateway.eu.assemblyai.com/v1. The EU gateway only serves Claude and Gemini models, so you must also override AI_MODEL and AI_STREAM_MODEL (the defaults are OpenAI-family models that are US-only).

Google OAuth

GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-secret

Apple OAuth

Create a Sign in with Apple Services ID, register https://your-domain.example/api/auth/callback/apple as its return URL, and generate a client-secret JWT:

APPLE_CLIENT_ID=your-services-id
APPLE_CLIENT_SECRET=your-client-secret-jwt

Apple client-secret JWTs expire, so rotate the secret before its configured expiration date.

Production Checklist

Critical: Change Default Secrets Before Going Public

The default docker-compose.yml contains hardcoded placeholder secrets that are visible in the public repository. Anyone who knows you're using Cap with defaults could potentially:

  • Forge authentication sessions (via NEXTAUTH_SECRET)
  • Decrypt sensitive database fields (via DATABASE_ENCRYPTION_KEY)
  • Spoof webhook requests (via MEDIA_SERVER_WEBHOOK_SECRET)

This is fine for local development or testing on a private network, but you must generate unique secrets before exposing Cap to the internet.

Generate secure secrets:

openssl rand -hex 32

Run this command three times to generate values for:

  • NEXTAUTH_SECRET
  • DATABASE_ENCRYPTION_KEY
  • MEDIA_SERVER_WEBHOOK_SECRET

Full production checklist:

  • Set secure passwords: MYSQL_PASSWORD, MINIO_ROOT_PASSWORD
  • Set secure secrets: DATABASE_ENCRYPTION_KEY, NEXTAUTH_SECRET, MEDIA_SERVER_WEBHOOK_SECRET
  • Media server status endpoints (/video/status, /audio/status, /live-transcribe/status, /video/process/:jobId/status) require MEDIA_SERVER_WEBHOOK_SECRET by default. Set MEDIA_SERVER_PUBLIC_STATUS=true only if an external monitor needs them without the secret; anonymous callers then see counts and job phase/progress, never video ids or errors.
  • HLS/DASH sources are only accepted from MEDIA_SERVER_STREAMING_HOSTS (default loom.com).
  • Set CAP_URL to your public URL
  • Set S3_PUBLIC_URL to your MinIO/S3 public URL
  • Configure a reverse proxy (nginx, Caddy, Traefik) with SSL
  • Set up email with Resend
  • Configure backups for MySQL and MinIO volumes

Architecture

┌─────────────────┐     ┌─────────────────┐
│   Cap Desktop   │────▶│    Cap Web      │
└─────────────────┘     │   (port 3000)   │
                        └────────┬────────┘
                                 │
                    ┌────────────┼────────────┐
                    ▼            ▼            ▼
             ┌──────────┐ ┌──────────┐ ┌──────────┐
             │  MySQL   │ │  MinIO   │ │  Media   │
             │ (3306)   │ │ (9000)   │ │  Server  │
             └──────────┘ └──────────┘ └──────────┘

Environment Variables Reference

See packages/env/server.ts for all available options.

Troubleshooting

Can't access Cap after starting:

docker compose logs cap-web

Database migration issues:

Don't delete volumes to fix a migration error: docker compose down -v deletes your MySQL and MinIO volumes, so every account and video is gone.

  1. Read the error: docker compose logs cap-web.
  2. Back up before changing anything:
    docker compose exec mysql sh -c 'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction cap' > cap-backup.sql
    Also keep a copy of your MinIO data (or bucket) and of DATABASE_ENCRYPTION_KEY: encrypted fields in the backup can't be read without the same key.
  3. Open a GitHub issue with the log.

Reset everything (deletes all data):

Only for a test install you don't need anymore. This deletes every account, video and setting:

docker compose down -v
docker compose up -d

Support

If you encounter issues with self-hosting:

We do not offer direct support for self-hosted deployments.

Can't find what you need?