This guide covers deploying CLAHub to production. Choose the method that fits your infrastructure.
Docker is the simplest way to self-host CLAHub. The included Dockerfile produces a minimal, multi-stage image based on node:20-alpine.
-
Copy and fill in the environment file:
cp .env.docker.example .env
Edit
.envwith your values — see the Configuration Guide for details. -
Start the container:
docker compose up -d
-
Verify it's running:
curl http://localhost:3000/api/health
That's it. The entrypoint script automatically runs prisma db push on every container start, so the database schema is always up to date.
- Data persistence: SQLite is stored at
/app/data/clahub.dbinside the container. Theclahub-datanamed volume is mounted to/app/data, ensuring data survives container restarts and upgrades. - Health checks: Docker Compose includes a built-in health check that polls
/api/healthevery 30 seconds. - Port: The container listens on port
3000. Map it to any host port indocker-compose.yml. - Restart policy:
unless-stopped— the container restarts automatically unless you explicitly stop it.
Set these optional variables in your .env file:
APP_NAME="MyCLA"
APP_LOGO_URL="https://example.com/logo.png"
APP_PRIMARY_COLOR="#0066cc"If you've forked CLAHub or need a custom build:
docker build -t my-clahub .Then reference my-clahub instead of the default build in docker-compose.yml.
CLAHub is a Next.js app, so Vercel auto-detects the framework.
- Import the repository on vercel.com.
- Set environment variables in the Vercel dashboard (see Configuration Guide).
- Deploy. Vercel handles building and hosting automatically.
Important: Vercel uses ephemeral, read-only file systems in production. SQLite databases are not persisted across deployments. This makes Vercel suitable for demos and previews, but not for production use with real data. For persistent data, use Docker or another self-hosted option.
Railway can deploy directly from a Dockerfile.
-
Create a new project and connect your GitHub repository.
-
Railway auto-detects the
Dockerfileand builds accordingly. -
Add a persistent volume and mount it to
/app/dataso SQLite data persists. -
Set environment variables in the Railway dashboard. Make sure
DATABASE_URLpoints to the mounted volume path (e.g.file:/app/data/clahub.db). -
Deploy:
railway up
-
Launch the app:
fly launch
Fly detects the
Dockerfileautomatically. -
Create and attach a persistent volume:
fly volumes create clahub_data --size 1 --region <your-region>
Add the volume mount to your
fly.toml:[mounts] source = "clahub_data" destination = "/app/data"
-
Set secrets:
fly secrets set NEXTAUTH_SECRET="..." GITHUB_APP_ID="..." # ... etc
Ensure
DATABASE_URLis set tofile:/app/data/clahub.db. -
Deploy:
fly deploy
For bare-metal or VM deployments without Docker.
npm ci
npx prisma generate
npm run buildnpx prisma db pushpm2 start .next/standalone/server.js --name clahub
pm2 saveCreate /etc/systemd/system/clahub.service:
[Unit]
Description=CLAHub
After=network.target
[Service]
Type=simple
User=clahub
WorkingDirectory=/opt/clahub
ExecStart=/usr/bin/node .next/standalone/server.js
Restart=on-failure
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=/opt/clahub/.env
[Install]
WantedBy=multi-user.targetThen enable and start:
sudo systemctl enable clahub
sudo systemctl start clahubPut CLAHub behind nginx for HTTPS termination:
server {
listen 443 ssl http2;
server_name cla.example.com;
ssl_certificate /etc/letsencrypt/live/cla.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cla.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}A full walkthrough for deploying CLAHub on a Google Cloud Platform VM with nginx and Let's Encrypt.
Create an e2-small instance running Ubuntu 24.04:
gcloud compute instances create clahub \
--zone=us-central1-a \
--machine-type=e2-small \
--image-family=ubuntu-2404-lts-amd64 \
--image-project=ubuntu-os-cloud \
--boot-disk-size=20GB \
--tags=http-server,https-serverOpen ports 80 and 443:
gcloud compute firewall-rules create allow-http \
--allow=tcp:80 --target-tags=http-server
gcloud compute firewall-rules create allow-https \
--allow=tcp:443 --target-tags=https-servergcloud compute ssh clahub --zone=us-central1-a# Node.js 20 (NodeSource)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# nginx, certbot, git
sudo apt-get install -y nginx certbot python3-certbot-nginx gitCreate a system user and clone the repo:
sudo useradd --system --create-home --home-dir /opt/clahub --shell /usr/sbin/nologin clahub
sudo -u clahub git clone https://github.com/clahub/clahub.git /opt/clahub/app
cd /opt/clahub/appInstall, generate, and build:
sudo -u clahub npm ci
sudo -u clahub npx prisma generate
sudo -u clahub npm run buildCreate the .env file (see Configuration Guide for all variables):
sudo -u clahub cp .env.example /opt/clahub/app/.env
sudo -u clahub nano /opt/clahub/app/.env # fill in your valuesSet up the data directory and initialize the database:
sudo -u clahub mkdir -p /opt/clahub/app/data
sudo -u clahub npx prisma db pushUse the same unit file from Generic Node.js > Run with systemd, adjusting WorkingDirectory and EnvironmentFile to match the paths above:
WorkingDirectory=/opt/clahub/app
EnvironmentFile=/opt/clahub/app/.envThen enable and start the service:
sudo systemctl enable clahub
sudo systemctl start clahubGet the VM's external IP:
gcloud compute instances describe clahub \
--zone=us-central1-a \
--format='get(networkInterfaces[0].accessConfigs[0].natIP)'Add an A record in your DNS provider pointing your domain (e.g. cla.example.com) to that IP. Verify propagation:
dig +short cla.example.comOnce DNS has propagated, run certbot:
sudo certbot --nginx -d cla.example.comCertbot automatically configures nginx with your certificate and sets up a systemd timer for auto-renewal. Verify the timer is active:
sudo systemctl list-timers | grep certbotReplace the default site with a production config. Create /etc/nginx/sites-available/clahub:
# Redirect HTTP to HTTPS
server {
listen 80;
server_name cla.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name cla.example.com;
ssl_certificate /etc/letsencrypt/live/cla.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cla.example.com/privkey.pem;
# SSL hardening
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Enable the site, remove the default, and reload:
sudo ln -s /etc/nginx/sites-available/clahub /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginxOpen https://cla.example.com in your browser. If something isn't working, check the logs:
# Application logs
sudo journalctl -u clahub -f
# nginx logs
sudo tail -f /var/log/nginx/error.logYou can also hit the health endpoint:
curl -s https://cla.example.com/api/health- Configuration Guide — environment variable reference
- Upgrading Guide — update to new versions safely