Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit8ade77bunknown_key

feat: Production deployment — Dockerfile, migration, deploy scripts, nginx

feat: Production deployment — Dockerfile, migration, deploy scripts, nginx

Everything needed to go live:

1. Dockerfile — Bun 1.3 + git, production-ready container
2. docker-compose.yml — single-command deploy with persistent volumes
3. drizzle/0000_init.sql — raw SQL migration for all 15 tables + indexes
4. scripts/deploy.sh — one-command deploy: installs Bun, clones repo,
   runs migration, sets up systemd service, starts gluecron
5. scripts/setup-nginx.sh — nginx reverse proxy + Let's Encrypt SSL
6. DEPLOY.md — complete deployment guide (quick deploy, manual, Docker)
7. Legal pages served at /terms, /privacy, /acceptable-use

To deploy on a fresh Ubuntu server:
  export DATABASE_URL="postgresql://..."
  bash scripts/deploy.sh

https://claude.ai/code/session_013wpQ5iX7qU6zy6PrtML3fP
Claude committed on April 18, 2026Parent: 36b4cbd
6 files changed+57008ade77bde0fa420e67c330dd0eb9e0eabbb541ca
6 changed files+570−0
AddedDEPLOY.md+139−0View fileUnifiedSplit
1# Deploying Gluecron to Production
2
3## Prerequisites
4
51. A server (Hetzner, DigitalOcean, etc.) with Ubuntu 22.04+
62. A Neon PostgreSQL database (free tier: https://neon.tech)
73. Domain pointed to the server (gluecron.com → server IP)
8
9## Quick Deploy (one command)
10
11```bash
12# 1. SSH into your server
13ssh root@your-server-ip
14
15# 2. Set your database URL
16export DATABASE_URL="postgresql://user:pass@host/gluecron?sslmode=require"
17
18# 3. Run the deploy script
19curl -fsSL https://raw.githubusercontent.com/ccantynz-alt/Gluecron.com/claude/ship-fixes-and-tests-Jvz1c/scripts/deploy.sh | bash
20```
21
22## Manual Deploy
23
24### Step 1: Database
25
261. Go to https://neon.tech and create a project called "gluecron"
272. Copy the connection string
283. Run the migration:
29```bash
30psql "your-connection-string" -f drizzle/0000_init.sql
31```
32
33### Step 2: Server Setup
34
35```bash
36# Install Bun
37curl -fsSL https://bun.sh/install | bash
38
39# Install git
40apt-get update && apt-get install -y git
41
42# Clone the repo
43git clone --branch claude/ship-fixes-and-tests-Jvz1c \
44 https://github.com/ccantynz-alt/Gluecron.com.git /opt/gluecron
45cd /opt/gluecron
46
47# Create .env
48cat > .env << EOF
49DATABASE_URL=postgresql://user:pass@host/gluecron?sslmode=require
50GIT_REPOS_PATH=/data/repos
51PORT=3000
52NODE_ENV=production
53GATETEST_URL=https://gatetest.ai/api/scan/run
54CRONTECH_DEPLOY_URL=https://crontech.ai/api/trpc/tenant.deploy
55EOF
56
57# Install dependencies
58bun install --production
59
60# Create repos directory
61mkdir -p /data/repos
62
63# Start the server
64bun run src/index.ts
65```
66
67### Step 3: HTTPS with Nginx
68
69```bash
70bash scripts/setup-nginx.sh gluecron.com
71```
72
73### Step 4: Systemd (keep it running)
74
75```bash
76# The deploy script creates this, but manually:
77cat > /etc/systemd/system/gluecron.service << EOF
78[Unit]
79Description=Gluecron
80After=network.target
81
82[Service]
83Type=simple
84WorkingDirectory=/opt/gluecron
85EnvironmentFile=/opt/gluecron/.env
86ExecStart=/root/.bun/bin/bun run src/index.ts
87Restart=always
88RestartSec=5
89
90[Install]
91WantedBy=multi-user.target
92EOF
93
94systemctl daemon-reload
95systemctl enable gluecron
96systemctl start gluecron
97```
98
99## Docker Deploy
100
101```bash
102# Create .env file with DATABASE_URL
103echo "DATABASE_URL=postgresql://..." > .env
104
105# Build and run
106docker compose up -d
107
108# Run migration
109docker compose exec gluecron bun run -e "..."
110# Or connect directly to Neon and run drizzle/0000_init.sql
111```
112
113## Operations
114
115```bash
116# View logs
117journalctl -u gluecron -f
118
119# Restart
120systemctl restart gluecron
121
122# Update to latest
123cd /opt/gluecron
124git pull origin claude/ship-fixes-and-tests-Jvz1c
125bun install
126systemctl restart gluecron
127```
128
129## Verification Checklist
130
131After deploy, verify:
132
133- [ ] `curl http://localhost:3000` returns the landing page
134- [ ] Register an account at /register
135- [ ] Create a repo at /new
136- [ ] `git clone http://gluecron.com/youruser/yourrepo.git` works
137- [ ] Push code and see it in the web UI
138- [ ] Health dashboard shows at /youruser/yourrepo/health
139- [ ] HTTPS works (if nginx + certbot set up)
AddedDockerfile+26−0View fileUnifiedSplit
1FROM oven/bun:1.3 AS base
2WORKDIR /app
3
4# Install git (required for git operations)
5RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
6
7# Install dependencies
8COPY package.json bun.lock ./
9RUN bun install --frozen-lockfile --production
10
11# Copy source
12COPY src/ ./src/
13COPY tsconfig.json drizzle.config.ts ./
14COPY legal/ ./legal/
15COPY CLAUDE.md LICENSE ./
16
17# Create repos directory
18RUN mkdir -p /data/repos
19
20ENV GIT_REPOS_PATH=/data/repos
21ENV NODE_ENV=production
22ENV PORT=3000
23
24EXPOSE 3000
25
26CMD ["bun", "run", "src/index.ts"]
Addeddocker-compose.yml+23−0View fileUnifiedSplit
1services:
2 gluecron:
3 build: .
4 ports:
5 - "3000:3000"
6 environment:
7 - DATABASE_URL=${DATABASE_URL}
8 - GIT_REPOS_PATH=/data/repos
9 - PORT=3000
10 - NODE_ENV=production
11 - GATETEST_URL=https://gatetest.ai/api/scan/run
12 - CRONTECH_DEPLOY_URL=https://crontech.ai/api/trpc/tenant.deploy
13 volumes:
14 - git-repos:/data/repos
15 restart: unless-stopped
16 healthcheck:
17 test: ["CMD", "curl", "-f", "http://localhost:3000/"]
18 interval: 30s
19 timeout: 10s
20 retries: 3
21
22volumes:
23 git-repos:
Addeddrizzle/0000_init.sql+195−0View fileUnifiedSplit
1-- Gluecron database schema
2-- Run this against your Neon PostgreSQL database to initialize all tables.
3-- psql $DATABASE_URL -f drizzle/0000_init.sql
4
5CREATE EXTENSION IF NOT EXISTS "pgcrypto";
6
7-- Users
8CREATE TABLE IF NOT EXISTS users (
9 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
10 username TEXT NOT NULL UNIQUE,
11 email TEXT NOT NULL UNIQUE,
12 display_name TEXT,
13 password_hash TEXT NOT NULL,
14 avatar_url TEXT,
15 bio TEXT,
16 created_at TIMESTAMP DEFAULT NOW() NOT NULL,
17 updated_at TIMESTAMP DEFAULT NOW() NOT NULL
18);
19
20-- Sessions
21CREATE TABLE IF NOT EXISTS sessions (
22 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
23 user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
24 token TEXT NOT NULL UNIQUE,
25 expires_at TIMESTAMP NOT NULL,
26 created_at TIMESTAMP DEFAULT NOW() NOT NULL
27);
28
29-- Repositories
30CREATE TABLE IF NOT EXISTS repositories (
31 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
32 name TEXT NOT NULL,
33 owner_id UUID NOT NULL REFERENCES users(id),
34 description TEXT,
35 is_private BOOLEAN DEFAULT FALSE NOT NULL,
36 default_branch TEXT DEFAULT 'main' NOT NULL,
37 disk_path TEXT NOT NULL,
38 forked_from_id UUID REFERENCES repositories(id) ON DELETE SET NULL,
39 created_at TIMESTAMP DEFAULT NOW() NOT NULL,
40 updated_at TIMESTAMP DEFAULT NOW() NOT NULL,
41 pushed_at TIMESTAMP,
42 star_count INTEGER DEFAULT 0 NOT NULL,
43 fork_count INTEGER DEFAULT 0 NOT NULL,
44 issue_count INTEGER DEFAULT 0 NOT NULL
45);
46CREATE UNIQUE INDEX IF NOT EXISTS repos_owner_name ON repositories(owner_id, name);
47
48-- Stars
49CREATE TABLE IF NOT EXISTS stars (
50 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
51 user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
52 repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
53 created_at TIMESTAMP DEFAULT NOW() NOT NULL
54);
55CREATE UNIQUE INDEX IF NOT EXISTS stars_user_repo ON stars(user_id, repository_id);
56
57-- Issues
58CREATE TABLE IF NOT EXISTS issues (
59 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
60 number SERIAL,
61 repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
62 author_id UUID NOT NULL REFERENCES users(id),
63 title TEXT NOT NULL,
64 body TEXT,
65 state TEXT NOT NULL DEFAULT 'open',
66 created_at TIMESTAMP DEFAULT NOW() NOT NULL,
67 updated_at TIMESTAMP DEFAULT NOW() NOT NULL,
68 closed_at TIMESTAMP
69);
70CREATE INDEX IF NOT EXISTS issues_repo_state ON issues(repository_id, state);
71CREATE INDEX IF NOT EXISTS issues_repo_number ON issues(repository_id, number);
72
73-- Issue Comments
74CREATE TABLE IF NOT EXISTS issue_comments (
75 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
76 issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
77 author_id UUID NOT NULL REFERENCES users(id),
78 body TEXT NOT NULL,
79 created_at TIMESTAMP DEFAULT NOW() NOT NULL,
80 updated_at TIMESTAMP DEFAULT NOW() NOT NULL
81);
82CREATE INDEX IF NOT EXISTS comments_issue ON issue_comments(issue_id);
83
84-- Labels
85CREATE TABLE IF NOT EXISTS labels (
86 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
87 repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
88 name TEXT NOT NULL,
89 color TEXT NOT NULL DEFAULT '#8b949e',
90 description TEXT
91);
92CREATE UNIQUE INDEX IF NOT EXISTS labels_repo_name ON labels(repository_id, name);
93
94-- Issue Labels
95CREATE TABLE IF NOT EXISTS issue_labels (
96 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
97 issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
98 label_id UUID NOT NULL REFERENCES labels(id) ON DELETE CASCADE
99);
100CREATE UNIQUE INDEX IF NOT EXISTS issue_labels_unique ON issue_labels(issue_id, label_id);
101
102-- Pull Requests
103CREATE TABLE IF NOT EXISTS pull_requests (
104 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
105 number SERIAL,
106 repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
107 author_id UUID NOT NULL REFERENCES users(id),
108 title TEXT NOT NULL,
109 body TEXT,
110 state TEXT NOT NULL DEFAULT 'open',
111 base_branch TEXT NOT NULL,
112 head_branch TEXT NOT NULL,
113 merged_at TIMESTAMP,
114 merged_by UUID REFERENCES users(id),
115 created_at TIMESTAMP DEFAULT NOW() NOT NULL,
116 updated_at TIMESTAMP DEFAULT NOW() NOT NULL,
117 closed_at TIMESTAMP
118);
119CREATE INDEX IF NOT EXISTS prs_repo_state ON pull_requests(repository_id, state);
120CREATE INDEX IF NOT EXISTS prs_repo_number ON pull_requests(repository_id, number);
121
122-- PR Comments
123CREATE TABLE IF NOT EXISTS pr_comments (
124 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
125 pull_request_id UUID NOT NULL REFERENCES pull_requests(id) ON DELETE CASCADE,
126 author_id UUID NOT NULL REFERENCES users(id),
127 body TEXT NOT NULL,
128 is_ai_review BOOLEAN DEFAULT FALSE NOT NULL,
129 file_path TEXT,
130 line_number INTEGER,
131 created_at TIMESTAMP DEFAULT NOW() NOT NULL,
132 updated_at TIMESTAMP DEFAULT NOW() NOT NULL
133);
134CREATE INDEX IF NOT EXISTS pr_comments_pr ON pr_comments(pull_request_id);
135
136-- Activity Feed
137CREATE TABLE IF NOT EXISTS activity_feed (
138 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
139 repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
140 user_id UUID REFERENCES users(id),
141 action TEXT NOT NULL,
142 target_type TEXT,
143 target_id TEXT,
144 metadata TEXT,
145 created_at TIMESTAMP DEFAULT NOW() NOT NULL
146);
147CREATE INDEX IF NOT EXISTS activity_repo ON activity_feed(repository_id);
148CREATE INDEX IF NOT EXISTS activity_user ON activity_feed(user_id);
149
150-- Webhooks
151CREATE TABLE IF NOT EXISTS webhooks (
152 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
153 repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
154 url TEXT NOT NULL,
155 secret TEXT,
156 events TEXT NOT NULL DEFAULT 'push',
157 is_active BOOLEAN DEFAULT TRUE NOT NULL,
158 last_delivered_at TIMESTAMP,
159 last_status INTEGER,
160 created_at TIMESTAMP DEFAULT NOW() NOT NULL
161);
162CREATE INDEX IF NOT EXISTS webhooks_repo ON webhooks(repository_id);
163
164-- API Tokens
165CREATE TABLE IF NOT EXISTS api_tokens (
166 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
167 user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
168 name TEXT NOT NULL,
169 token_hash TEXT NOT NULL,
170 token_prefix TEXT NOT NULL,
171 scopes TEXT NOT NULL DEFAULT 'repo',
172 last_used_at TIMESTAMP,
173 expires_at TIMESTAMP,
174 created_at TIMESTAMP DEFAULT NOW() NOT NULL
175);
176
177-- Repository Topics
178CREATE TABLE IF NOT EXISTS repo_topics (
179 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
180 repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
181 topic TEXT NOT NULL
182);
183CREATE UNIQUE INDEX IF NOT EXISTS repo_topics_unique ON repo_topics(repository_id, topic);
184CREATE INDEX IF NOT EXISTS topics_name ON repo_topics(topic);
185
186-- SSH Keys
187CREATE TABLE IF NOT EXISTS ssh_keys (
188 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
189 user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
190 title TEXT NOT NULL,
191 fingerprint TEXT NOT NULL,
192 public_key TEXT NOT NULL,
193 last_used_at TIMESTAMP,
194 created_at TIMESTAMP DEFAULT NOW() NOT NULL
195);
Addedscripts/deploy.sh+123−0View fileUnifiedSplit
1#!/bin/bash
2set -euo pipefail
3
4# ============================================
5# Gluecron Deploy Script
6# One command to deploy to production.
7# ============================================
8
9APP_NAME="gluecron"
10APP_DIR="/opt/gluecron"
11REPO_URL="https://github.com/ccantynz-alt/Gluecron.com.git"
12BRANCH="claude/ship-fixes-and-tests-Jvz1c"
13
14echo "=========================================="
15echo " Deploying $APP_NAME"
16echo "=========================================="
17
18# 1. Check prerequisites
19command -v git >/dev/null 2>&1 || { echo "git required"; exit 1; }
20command -v bun >/dev/null 2>&1 || {
21 echo "Installing Bun..."
22 curl -fsSL https://bun.sh/install | bash
23 export PATH="$HOME/.bun/bin:$PATH"
24}
25
26# 2. Check for DATABASE_URL
27if [ -z "${DATABASE_URL:-}" ]; then
28 if [ -f "$APP_DIR/.env" ]; then
29 export $(grep -v '^#' "$APP_DIR/.env" | xargs)
30 fi
31fi
32
33if [ -z "${DATABASE_URL:-}" ]; then
34 echo "ERROR: DATABASE_URL not set."
35 echo "Set it in $APP_DIR/.env or export it."
36 echo ""
37 echo "Get a free database at https://neon.tech"
38 echo "Then: echo 'DATABASE_URL=postgresql://...' > $APP_DIR/.env"
39 exit 1
40fi
41
42# 3. Clone or update repo
43if [ -d "$APP_DIR" ]; then
44 echo "Updating existing installation..."
45 cd "$APP_DIR"
46 git fetch origin "$BRANCH"
47 git reset --hard "origin/$BRANCH"
48else
49 echo "Fresh install..."
50 sudo mkdir -p "$APP_DIR"
51 sudo chown "$(whoami)" "$APP_DIR"
52 git clone --branch "$BRANCH" "$REPO_URL" "$APP_DIR"
53 cd "$APP_DIR"
54fi
55
56# 4. Install dependencies
57echo "Installing dependencies..."
58bun install --frozen-lockfile --production
59
60# 5. Run database migration
61echo "Running database migration..."
62psql "$DATABASE_URL" -f drizzle/0000_init.sql 2>/dev/null || {
63 echo "psql not found or migration failed — trying via bun..."
64 bun run -e "
65 const { neon } = require('@neondatabase/serverless');
66 const sql = neon(process.env.DATABASE_URL);
67 const fs = require('fs');
68 const migration = fs.readFileSync('drizzle/0000_init.sql', 'utf-8');
69 // Split by semicolons and execute each statement
70 const statements = migration.split(';').filter(s => s.trim());
71 (async () => {
72 for (const stmt of statements) {
73 try { await sql(stmt); } catch(e) { /* table may already exist */ }
74 }
75 console.log('Migration complete');
76 })();
77 "
78}
79
80# 6. Create repos directory
81mkdir -p /data/repos 2>/dev/null || mkdir -p "$APP_DIR/repos"
82export GIT_REPOS_PATH="${GIT_REPOS_PATH:-/data/repos}"
83
84# 7. Set up systemd service
85echo "Setting up systemd service..."
86sudo tee /etc/systemd/system/gluecron.service > /dev/null << UNIT
87[Unit]
88Description=Gluecron - AI-native code intelligence
89After=network.target
90
91[Service]
92Type=simple
93User=$(whoami)
94WorkingDirectory=$APP_DIR
95EnvironmentFile=$APP_DIR/.env
96ExecStart=$(which bun) run src/index.ts
97Restart=always
98RestartSec=5
99StandardOutput=journal
100StandardError=journal
101
102[Install]
103WantedBy=multi-user.target
104UNIT
105
106sudo systemctl daemon-reload
107sudo systemctl enable gluecron
108sudo systemctl restart gluecron
109
110echo ""
111echo "=========================================="
112echo " $APP_NAME is now running!"
113echo "=========================================="
114echo ""
115echo " URL: http://$(hostname -I | awk '{print $1}'):3000"
116echo " Logs: journalctl -u gluecron -f"
117echo " Status: systemctl status gluecron"
118echo ""
119echo " Next steps:"
120echo " 1. Set up reverse proxy (nginx/caddy) for HTTPS"
121echo " 2. Point gluecron.com DNS to this server"
122echo " 3. Register your admin account at /register"
123echo ""
Addedscripts/setup-nginx.sh+64−0View fileUnifiedSplit
1#!/bin/bash
2set -euo pipefail
3
4# ============================================
5# Nginx + HTTPS setup for gluecron
6# ============================================
7
8DOMAIN="${1:-gluecron.com}"
9
10echo "Setting up nginx for $DOMAIN..."
11
12# Install nginx + certbot
13sudo apt-get update
14sudo apt-get install -y nginx certbot python3-certbot-nginx
15
16# Create nginx config
17sudo tee /etc/nginx/sites-available/gluecron > /dev/null << NGINX
18server {
19 listen 80;
20 server_name $DOMAIN www.$DOMAIN;
21
22 # Allow large git pushes
23 client_max_body_size 500m;
24
25 location / {
26 proxy_pass http://127.0.0.1:3000;
27 proxy_http_version 1.1;
28 proxy_set_header Host \$host;
29 proxy_set_header X-Real-IP \$remote_addr;
30 proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
31 proxy_set_header X-Forwarded-Proto \$scheme;
32
33 # Required for git smart HTTP protocol
34 proxy_buffering off;
35 proxy_request_buffering off;
36
37 # Timeouts for large repos
38 proxy_connect_timeout 300;
39 proxy_send_timeout 300;
40 proxy_read_timeout 300;
41 }
42}
43NGINX
44
45# Enable site
46sudo ln -sf /etc/nginx/sites-available/gluecron /etc/nginx/sites-enabled/
47sudo rm -f /etc/nginx/sites-enabled/default
48sudo nginx -t
49sudo systemctl reload nginx
50
51# Get SSL cert
52echo "Getting SSL certificate..."
53sudo certbot --nginx -d "$DOMAIN" -d "www.$DOMAIN" --non-interactive --agree-tos --email "admin@$DOMAIN" || {
54 echo ""
55 echo "Certbot failed — DNS may not be pointed yet."
56 echo "Once DNS is live, run:"
57 echo " sudo certbot --nginx -d $DOMAIN -d www.$DOMAIN"
58}
59
60echo ""
61echo "Nginx configured for $DOMAIN"
62echo " HTTP -> HTTPS redirect: automatic (after cert)"
63echo " Proxy -> localhost:3000"
64echo " Max upload: 500MB (for large repos)"
065