Self-hosting Guide

This guide covers a production-grade OpenCan deployment: HTTPS via a reverse proxy, persistent data volumes, backups, and update procedures. For a quick local run, see Getting Started.

Prerequisites

  • A Linux server (Ubuntu 22.04 LTS recommended) with at least 1 GB RAM
  • Docker Engine 24+ and Docker Compose v2
  • A domain name with an A record pointing to your server's IP
  • A Resend account (or any SMTP provider) for transactional email
  • Ports 80 and 443 open in your firewall

Docker Compose setup

1. Clone the repository

git clone https://github.com/sriramgopalan/opencan.git
cd opencan

2. Configure environment variables

cp .env.example .env
nano .env   # or your editor of choice

Set every required variable. In particular:

  • NEXT_PUBLIC_APP_URL — your public domain, e.g. https://feedback.example.com
  • AUTH_URL — same value as NEXT_PUBLIC_APP_URL
  • AUTH_SECRETopenssl rand -base64 32
  • IP_HASH_SECRETopenssl rand -hex 32
  • MINIO_USE_SSL — set to true in production
  • NODE_ENV — set to production

See the full Environment Variables reference for every option.

3. Start the stack

docker compose up -d

Docker Compose brings up four services: app (Next.js), postgres, redis, and minio. On first boot, database migrations run automatically inside the app container.

4. Verify everything is running

docker compose ps
docker compose logs app --tail=50

The app listens on port 3000 by default. You'll put a reverse proxy in front of it for HTTPS.

HTTPS with a reverse proxy

Run a reverse proxy on your server to terminate TLS and forward traffic to the app container.

Option A — Caddy (recommended)

Caddy handles certificate provisioning automatically via Let's Encrypt.

Create a Caddyfile alongside your docker-compose.yml:

feedback.example.com {
    reverse_proxy app:3000
}

Add Caddy to your docker-compose.yml:

services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - opencan

volumes:
  caddy_data:
  caddy_config:

Option B — nginx + Certbot

sudo apt install nginx certbot python3-certbot-nginx -y
sudo certbot --nginx -d feedback.example.com

Add an nginx site config to proxy to localhost:3000.

Environment variables reference

See the dedicated Environment Variables page for a full table of every variable, whether it is required, and what it does.

First admin user

Register an account at /auth/register, then promote it to admin:

docker compose exec postgres psql -U postgres -d opencan -c \
  "UPDATE \"User\" SET role = 'ADMIN' WHERE email = 'your@email.com';"

Log in and navigate to /admin to access the admin dashboard.

Production considerations

Persistent volumes

Docker Compose creates named volumes for Postgres data, Redis data, and MinIO objects. These persist across container restarts. Never remove volumes without backing up first.

Backups

Database
# Dump
docker compose exec postgres pg_dump -U postgres opencan | gzip > opencan-$(date +%Y%m%d).sql.gz

# Restore
gunzip -c opencan-20260101.sql.gz | docker compose exec -T postgres psql -U postgres opencan
MinIO objects

Use the MinIO Client (mc) to mirror your bucket to an external location:

mc alias set local http://localhost:9000 MINIO_ACCESS_KEY MINIO_SECRET_KEY
mc mirror local/your-bucket-name s3/backup-bucket

Schedule both backup commands via cron to run nightly. Store backups off-server.

Updates

git pull origin main
docker compose pull app
docker compose up -d app

Migrations run automatically on container startup. Review the release notes before upgrading to catch any breaking changes.

Resource limits

For small teams (under 50 users), 1 vCPU and 1 GB RAM is sufficient. For larger deployments, consider scaling Postgres to a managed database (Supabase, Railway, Neon) and Redis to a managed cache (Upstash). MinIO can be replaced with any S3-compatible storage.

Session security

OpenCan includes a session blocklist backed by Redis. Revoking a user in the admin dashboard immediately invalidates their session without waiting for token expiry. This requires REDIS_URL to be set correctly.