Codeveira Installation Guide — Docker Compose Setup
Deploy Codeveira on your own server in under 10 minutes using Docker Compose. Supports GitLab, GitHub, Gitea, Forgejo, Bitbucket Cloud, Bitbucket Server, Azure DevOps, Gerrit and SVN — you can connect multiple platforms at the same time.
Prerequisites
Before you start, make sure the following are installed on your server:
- Docker 24+ and Docker Compose v2 (
docker compose) - A server with at least 1 GB RAM and 10 GB disk space
- Access to at least one supported repository platform (GitLab, GitHub, Gitea, Forgejo, Bitbucket Cloud, Bitbucket Server, Azure DevOps, Gerrit, or SVN)
- An outbound internet connection for pulling images
Get the source code
Codeveira is currently in early access. To receive the source code, contact us at hello@codeveira.com — we'll send you a download link and set you up.
Once you have the archive, extract it and enter the directory:
tar -xzf codeveira.tar.gz cd codeveira
Configure environment variables
Copy the example configuration file and fill in your values:
cp .env.example .env nano .env # or any editor you prefer
These variables are required regardless of which Git platform you use:
# Required — generate with: openssl rand -hex 64 SECRET_KEY_BASE=your_long_random_string_here # Public URL of your Codeveira instance (used in email links) APP_HOST=https://codeveira.yourdomain.com # PostgreSQL password DB_PASSWORD=choose_a_strong_db_password
.env file to version control. It contains secrets.
Then add the variables for the platforms you want to use. You only need to configure the ones you'll actually connect:
GitLab
# Base URL of your GitLab instance GITLAB_URL=https://gitlab.example.com # Personal access token with api scope (used as default for new repos) GITLAB_TOKEN=glpat-xxxxxxxxxxxx # Webhook verification token WEBHOOK_SECRET=your_gitlab_webhook_secret # OAuth — enables "Sign in with GitLab" (optional) GITLAB_APP_ID=your_app_id GITLAB_APP_SECRET=your_app_secret
GitHub
# Personal access token — used as default token for GitHub repos GITHUB_TOKEN=github_pat_xxxxxxxxxxxx # Webhook HMAC-SHA256 secret (recommended) GITHUB_WEBHOOK_SECRET=your_github_webhook_secret # OAuth — enables "Sign in with GitHub" (optional) GITHUB_APP_ID=your_oauth_app_client_id GITHUB_APP_SECRET=your_oauth_app_client_secret
Gitea / Forgejo
# Default base URL for Gitea/Forgejo instances # (can also be set per-repository) GITEA_URL=https://gitea.example.com # Webhook HMAC-SHA256 secret — shared by Gitea and Forgejo GITEA_WEBHOOK_SECRET=your_gitea_webhook_secret
Bitbucket Cloud
# Webhook HMAC-SHA256 secret (recommended) BITBUCKET_WEBHOOK_SECRET=your_bitbucket_webhook_secret
Also change the default admin credentials before going live:
ADMIN_EMAIL=admin@yourcompany.com ADMIN_PASSWORD=choose_a_strong_password
Start the application
Build and start all services with Docker Compose:
docker compose up -d
This will start four containers: app (Rails), db (PostgreSQL 18), redis and sidekiq (background jobs). The first run also runs database migrations automatically.
Check that all containers are running:
docker compose ps
To watch the application logs in real time:
docker compose logs -f app
docker compose build app && docker compose up -d app
First login
Open your browser and navigate to http://your-server:3000 (or the domain you configured).
Log in with the admin credentials you set in .env:
- Email: value of
ADMIN_EMAIL - Password: value of
ADMIN_PASSWORD
You will land on the dashboard. The repository list will be empty until you add your first repository in the next step.
Add a repository
Go to Repositories → New Repository. Select the source platform using the buttons at the top of the form — the fields change depending on what you select.
- Name — display name for the repository
- GitLab Project ID — found in GitLab → Settings → General
- GitLab Path — e.g.
mygroup/myrepo - GitLab URL — optional if
GITLAB_URLis already set in.env - Access Token — personal access token with
apiscope
- Name — display name for the repository
- Repo path —
owner/repo, e.g.octocat/Hello-World - Access Token — personal access token (fine-grained or classic) with
Contents: Readpermission for private repos
repo scope.
- Name — display name for the repository
- Gitea URL — base URL of your Gitea instance, e.g.
https://gitea.example.com - Owner/Repo — e.g.
myorg/myrepo - Access Token — personal access token from Gitea → Settings → Applications
- Name — display name for the repository
- Forgejo URL — base URL of your Forgejo instance, e.g.
https://forgejo.example.com - Owner/Repo — e.g.
myorg/myrepo - Access Token — personal access token from Forgejo → Settings → Applications
GITEA_WEBHOOK_SECRET is shared by both.
- Name — display name for the repository
- Workspace/Repo — e.g.
myworkspace/myrepo - App Password — format:
username:app_password. Create one in Bitbucket → Personal settings → App passwords (needs Repositories: Read)
- Name — display name for the repository
- Instance URL — required, e.g.
https://bitbucket.example.com - Path —
PROJECTKEY/repo-slug, e.g.PROJ/my-repo(project key is uppercase) - Token — HTTP access token, or
username:passwordfor Basic auth
- Name — display name for the repository
- Instance URL — leave blank for Azure DevOps cloud; set for on-premises Azure DevOps Server
- Path —
organization/project/repository, e.g.myorg/MyProject/MyRepo - Token — Personal Access Token (PAT) with Code: Read permission
- Name — display name for the repository
- Instance URL — required, e.g.
https://gerrit.example.com - Project Name — Gerrit project name, e.g.
my-projectororg/my-project - Token — format:
username:http_passwordfrom Gerrit → Settings → HTTP Credentials
- Name — display name for the repository
- Instance URL — repository root URL, required, e.g.
https://svn.example.com/repos/myprojectorsvn://svn.example.com/myproject - Path Identifier — a unique string matched against the
hooks/post-commitscript'spathfield; also doubles as the reviewable root, defaults totrunk - Credentials — optional
username:password; leave blank for anonymous read
hooks/post-commit script. SVN has no PR/branch model, so Pull Request Mode isn't offered for SVN repositories.
Once saved, click the repository on the dashboard to browse its recent commits and create code reviews.
Configure webhooks (optional but recommended)
Webhooks let Codeveira automatically create a code review on every push, without polling. Each platform has its own endpoint and verification method.
You can find the exact webhook URL and secret for each repository on its Repository → Show page in Codeveira.
GitLab
- Go to your GitLab project → Settings → Webhooks
- URL:
https://codeveira.yourdomain.com/webhooks/gitlab - Secret token: value of
WEBHOOK_SECRET - Trigger: Push events
GitHub
- Go to your GitHub repo → Settings → Webhooks → Add webhook
- Payload URL:
https://codeveira.yourdomain.com/webhooks/github - Content type:
application/json - Secret: value of
GITHUB_WEBHOOK_SECRET - Trigger: Just the push event
Gitea
- Go to your Gitea repo → Settings → Webhooks → Add webhook → Gitea
- Target URL:
https://codeveira.yourdomain.com/webhooks/gitea - Secret: value of
GITEA_WEBHOOK_SECRET - Trigger: Push events
Forgejo
- Go to your Forgejo repo → Settings → Webhooks → Add webhook → Forgejo
- Target URL:
https://codeveira.yourdomain.com/webhooks/forgejo - Secret: value of
GITEA_WEBHOOK_SECRET(shared with Gitea) - Trigger: Push events
Bitbucket Cloud
- Go to your Bitbucket repo → Repository settings → Webhooks → Add webhook
- URL:
https://codeveira.yourdomain.com/webhooks/bitbucket - Secret: value of
BITBUCKET_WEBHOOK_SECRET - Trigger: Repository push (
repo:push)
Bitbucket Server / Data Center
- Go to your Bitbucket Server repo → Repository settings → Webhooks → Create webhook
- URL:
https://codeveira.yourdomain.com/webhooks/bitbucket_server - Secret: value of
BITBUCKET_SERVER_WEBHOOK_SECRET - Events: Push (
repo:refs_changed)
Azure DevOps
- Go to Azure DevOps → Project Settings → Service hooks → + Create subscription
- Service: Web Hooks → Next
- Event: Code pushed, select your repository → Next
- URL:
https://codeveira.yourdomain.com/webhooks/azure - Basic auth: any username, password = value of
AZURE_WEBHOOK_SECRET→ Finish
Gerrit
- Install the
webhooksplugin on your Gerrit instance - Add to
$GERRIT_SITE/etc/webhooks.config:
[remote "codeveira"] url = https://codeveira.yourdomain.com/webhooks/gerrit event = ref-updated password = $GERRIT_WEBHOOK_SECRET
SVN
SVN has no native webhook mechanism, so an executable hooks/post-commit script on the SVN server notifies Codeveira after every commit:
#!/bin/sh REPOS="$1" REV="$2" PATH_ID="myrepo" # must match the repository's Path Identifier in Codeveira curl -s -X POST "https://codeveira.yourdomain.com/webhooks/svn" \ -u "x:$SVN_WEBHOOK_SECRET" \ -H "Content-Type: application/json" \ -d "{\"path\":\"$PATH_ID\",\"revision\":\"$REV\"}"
See the SVN guide for the full setup and troubleshooting.
openssl rand -hex 20
GitLab, GitHub, Gitea, Forgejo, and Bitbucket (Cloud and Server) verify webhook requests via HMAC-SHA256 signature, using ActiveSupport::SecurityUtils.secure_compare to prevent timing attacks. Azure DevOps, Gerrit, and SVN don't support HMAC signing on their webhook mechanisms, so their requests are authenticated via HTTP Basic Auth against AZURE_WEBHOOK_SECRET/GERRIT_WEBHOOK_SECRET/SVN_WEBHOOK_SECRET instead. Every one of these secrets is required — leaving the corresponding env var blank rejects all of that platform's webhook requests with 401 Unauthorized rather than accepting them unauthenticated.
Domain & HTTPS
Codeveira ships with a built-in nginx reverse-proxy container — nothing to install or configure on the host. It's already in docker-compose.yml, publishes ports 80/443, and boots with a zero-config self-signed certificate, so the app is reachable over HTTPS immediately after docker compose up -d. It also proxies the LSP server's raw TCP port (7777) for IDE integration.
To go live on a real domain, sign in as an admin and open Settings → Domain & HTTPS:
- Upload your own certificate — paste in a PEM cert/key pair from any CA, applied immediately.
- Let's Encrypt — HTTP-01 — simplest option if port 80 is reachable from the public internet.
- Let's Encrypt — DNS-01 — works without exposing port 80, and supports wildcard domains. Supported providers: Cloudflare, AWS Route 53, Google Cloud DNS.
Certificates issued via Let's Encrypt renew automatically twice a day — no cron job to set up yourself. Coordination between the app containers (which write the domain's config/certificate) and the nginx container (which serves them) goes entirely through shared Docker volumes plus a polling reload loop inside the nginx container — no Docker socket access, no cross-container RPC.
nginx container's ports, and point your own proxy at app:3000 (and the LSP container's raw TCP port for IDE integration). A reference host-nginx config, including the required LSP stream {} block, is included in the source at nginx.conf.example.
The X-Forwarded-For header (already set by the bundled nginx container, or required in your own config if running one) is what lets the Audit Log record real client IPs instead of the proxy address. Rails reads this header automatically via ActionDispatch::RemoteIp middleware — no application changes needed.
Environment variable reference
Full list of variables available in .env:
| Variable | Required | Description |
|---|---|---|
| Core | ||
| SECRET_KEY_BASE | required | Rails secret key. Generate with openssl rand -hex 64. |
| DB_PASSWORD | required | PostgreSQL password for the codereview user. |
| APP_HOST | required | Public URL of your Codeveira instance. Used in email links and webhook setup hints. |
| ADMIN_EMAIL | optional | Initial admin account email. Default: admin@example.com. |
| ADMIN_PASSWORD | optional | Initial admin account password. Default: changeme123. Change before going live. |
| TESTSERVICE | optional | Set to true to auto-seed demo data on first boot (5 users, 3 repos, 7 reviews). |
| GitLab | ||
| GITLAB_URL | optional | Base URL of your GitLab instance, e.g. https://gitlab.example.com. Used as default for GitLab repos. |
| GITLAB_TOKEN | optional | GitLab personal access token with api scope. Default token for fetching commits and diffs. |
| WEBHOOK_SECRET | optional | Secret token for verifying GitLab webhook requests (X-Gitlab-Token header). |
| GITLAB_APP_ID | optional | GitLab OAuth application ID. Enables "Sign in with GitLab". |
| GITLAB_APP_SECRET | optional | GitLab OAuth application secret. |
| GitHub | ||
| GITHUB_TOKEN | optional | GitHub personal access token. Used as the default token for GitHub repos and auto-creation on push. |
| GITHUB_WEBHOOK_SECRET | optional | GitHub webhook secret for HMAC-SHA256 signature verification (X-Hub-Signature-256). Recommended. |
| GITHUB_APP_ID | optional | GitHub OAuth App client ID. Enables "Sign in with GitHub". |
| GITHUB_APP_SECRET | optional | GitHub OAuth App client secret. |
| Gitea & Forgejo | ||
| GITEA_URL | optional | Default base URL for Gitea/Forgejo instances. Can be overridden per repository. |
| GITEA_WEBHOOK_SECRET | optional | Webhook HMAC-SHA256 secret shared by both Gitea (X-Gitea-Signature) and Forgejo webhooks. Recommended. |
| Bitbucket Cloud | ||
| BITBUCKET_WEBHOOK_SECRET | optional | Webhook HMAC-SHA256 secret for Bitbucket Cloud push events. Recommended. |
| Bitbucket Server / Data Center | ||
| BITBUCKET_SERVER_WEBHOOK_SECRET | optional | Webhook HMAC-SHA256 secret for Bitbucket Server push events (X-Hub-Signature). Recommended. |
| Azure DevOps | ||
| AZURE_WEBHOOK_SECRET | required for webhooks | HTTP Basic Auth password checked against the Service Hook subscription (Azure DevOps has no HMAC signing option). Blank rejects all Azure webhook requests with 401. |
| Gerrit | ||
| GERRIT_WEBHOOK_SECRET | required for webhooks | HTTP Basic Auth password checked against the webhooks plugin's remote config (Gerrit has no HMAC signing option). Blank rejects all Gerrit webhook requests with 401. |
| SVN (Subversion) | ||
| SVN_WEBHOOK_SECRET | required for webhooks | HTTP Basic Auth password checked against the hooks/post-commit script (SVN has no HMAC signing option). Blank rejects all SVN webhook requests with 401. |
| Email (SMTP) | ||
| SMTP_HOST | optional | SMTP server hostname for outgoing email. |
| SMTP_PORT | optional | SMTP port. Default: 587. |
| SMTP_USER | optional | SMTP authentication username. |
| SMTP_PASSWORD | optional | SMTP authentication password. |
| SMTP_FROM | optional | Sender address for notification emails. |
| LDAP / Active Directory | ||
| LDAP_HOST | optional | LDAP server hostname. Enables LDAP authentication when set. |
| LDAP_PORT | optional | LDAP port. Default: 389. |
| LDAP_BASE | optional | LDAP search base DN, e.g. dc=example,dc=com. |
| LDAP_BIND_DN | optional | DN of the bind user, e.g. cn=admin,dc=example,dc=com. |
| LDAP_PASSWORD | optional | Password for the LDAP bind user. |
| LDAP_DOMAIN | optional | Domain suffix used to construct email addresses from LDAP login names. |
| Symbol Indexer (Find Usages / Go to Declaration) | ||
| SYMBOL_INDEXER_TOKEN | optional | Shared secret required in the X-Indexer-Token header between app/sidekiq and the indexer container. Recommended. |
| INDEXER_MEM_LIMIT | optional | Memory limit for the indexer container. Default: 768m. |
| INDEXER_CPUS | optional | CPU limit for the indexer container. Default: 1.0. |
| Redis Sentinel (optional) | ||
| REDIS_SENTINELS | optional | host:port,host:port,...; port defaults to 26379 if omitted. Setting this switches Sidekiq/ActionCable from plain REDIS_URL to Sentinel-aware connections. |
| REDIS_MASTER_NAME | optional | Sentinel master group name to monitor. Default: mymaster. |
| REDIS_PASSWORD | optional | Auth for the actual Redis master/replica, if configured. |
| REDIS_SENTINEL_PASSWORD | optional | Auth for the Sentinel processes themselves, if configured. |
Scaling & High Availability
The shipped docker-compose.yml runs one replica of each service — enough for a single team on a single host. app, sidekiq, lsp, and indexer are stateless and already safe to scale horizontally:
- app — sessions use Rails' default cookie store (no server affinity needed) and the codebase makes no use of
Rails.cache, so there's no server-local cache to desync between replicas. Put a load balancer in front of multipleappcontainers. - sidekiq — scale with
docker compose up -d --scale sidekiq=3; Sidekiq is designed for multiple workers pulling from the same Redis-backed queues. - lsp / indexer — both stateless per-connection/per-request; add replicas if one becomes a bottleneck.
- Real-time updates (ActionCable) — already configured with the Redis adapter in production, so broadcasts fan out correctly across multiple
appreplicas.
db (PostgreSQL) and redis are single-node in this compose file, with no replication or automatic failover. For Postgres, point DATABASE_URL at an externally managed HA database (RDS Multi-AZ, Cloud SQL HA, or a self-managed Patroni cluster) instead of the bundled db service. Redis has a built-in HA option — see below.Redis Sentinel (optional)
The default redis service is a single non-HA instance. To run Redis with automatic master failover instead, merge the included Sentinel topology on top of the base compose file:
docker compose -f docker-compose.yml -f docker-compose.sentinel.yml up -d
This adds a replica and three Sentinel instances (quorum 2 of 3) and points app/sidekiq at them via REDIS_SENTINELS/REDIS_MASTER_NAME instead of a fixed host. Both Sidekiq's and ActionCable's Redis clients are Sentinel-aware — they auto-discover the current master and reconnect after a failover without needing a restart. The base docker-compose.yml is unmodified either way; switching back is just dropping the second -f flag.
| Variable | Default | Description |
|---|---|---|
| REDIS_SENTINELS | unset (plain Redis) | host:port,host:port,... — port defaults to 26379 if omitted. Setting this enables Sentinel mode; REDIS_URL is ignored once it's set. |
| REDIS_MASTER_NAME | mymaster | The Sentinel master group name to monitor. |
| REDIS_PASSWORD | blank | Auth for the actual Redis master/replica, if configured. |
| REDIS_SENTINEL_PASSWORD | blank | Auth for the Sentinel processes themselves, if configured (separate from the above). |
redis container as a replica of whichever node got promoted. Before reverting to plain docker-compose.yml, promote it back to a standalone master first — docker exec <redis container> redis-cli replicaof no one — otherwise it's left stuck read-only and Sidekiq will crash-loop.Monitoring (optional)
A ready-made Prometheus + Grafana + Loki/Promtail stack, merged the same way as Redis Sentinel above:
docker compose -f docker-compose.yml -f docker-compose.monitoring.yml up -d
One-time setup: create a service account (Settings → Users → New Service Account), copy its API token into monitoring/prometheus_token (see monitoring/prometheus_token.example — Prometheus reads the scrape credential from this file, never from a compose/env var), and set GRAFANA_ADMIN_PASSWORD in .env.
Grafana comes up at :3001 with two dashboards already provisioned — no manual datasource or import step:
- Codeveira — Review Health & Bottlenecks — review throughput, per-repository cycle time, and per-reviewer bottleneck detection (current backlog, assignments stale >7 days, assignment-to-decision time p50/p90), from the Prometheus metrics endpoint.
- Nginx — Connections & Security — who's connecting to the instance and any failed/suspicious requests by IP (a spike of 404s/401s from one address is what a scan or brute-force attempt looks like here). Sourced from nginx's own access log via Loki, not a Prometheus metric — client IP is unbounded-cardinality data, so it's shipped as logs rather than a label.
Prometheus itself is exposed at :9090 for ad-hoc queries. Loki and Promtail have no exposed ports — Grafana talks to them over the internal Docker network only.
Redis data & backup
There's no scheduled backup job for Redis, unlike the Postgres Backup & Restore feature — by design. Everything durable (reviews, comments, users, the symbol index, audit log) lives in Postgres; Redis only holds Sidekiq's job queues and ActionCable's pub/sub, which are transient in-flight state, not the source of truth.
It's already persisted: the redis_data volume survives container restarts, and Redis's default RDB snapshot policy is active out of the box. AOF is off by default, so a hard crash can lose up to the last snapshot window — in practice a handful of in-flight jobs, never committed application data.
For a point-in-time snapshot anyway (e.g. before a risky upgrade):
docker compose exec redis sh -c "tar czf - -C /data ." > redis-backup-$(date +%Y%m%d).tar.gz