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.gitcd Capdocker 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
Option 1: Docker Compose (Recommended)
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:
Clone the repository
Run docker compose up -d
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 portsCAP_PORT=3000MINIO_PORT=9000# Optional: Custom passwords (auto-generated if not set)MYSQL_PASSWORD=your-secure-passwordMINIO_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)
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:
Create a new Docker Compose project in Coolify
Point to the Cap repository
Set compose file to docker-compose.coolify.yml
Configure environment variables in Coolify's UI
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
Open Cap Desktop settings
Set 'Cap Server URL' to your deployment URL (e.g., https://cap.yourdomain.com)
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:
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 accountCAP_ALLOWED_SIGNUP_DOMAINS=yourcompany.com,yourcompany.org# Domains or full addresses that can't sign inCAP_BLOCKED_SIGNUP_DOMAINS=former-partner.com,someone@yourcompany.com# New videos are private unless sharedCAP_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:
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 onceMEDIA_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 # TranscriptionGROQ_API_KEY=your-key # AI featuresOPENAI_API_KEY=your-key # AI featuresANTHROPIC_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-compatibleAI_BASE_URL=http://localhost:11434/v1AI_MODEL=llama3.3AI_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, translationAI_CHAT_MODEL=your-model # Support chatAI_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).
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-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:
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 │
└──────────┘ └──────────┘ └──────────┘