Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Build context trimming for apps/node/Dockerfile.
# node_modules and dist are rebuilt inside the image; secrets must never enter it.

node_modules
**/node_modules
dist
**/dist
.wrangler
**/.wrangler

.git
.github
.changeset

.dev.vars
.env
.env.*
!.env.docker.example

*.log
npm-debug.log*
coverage
.vscode
.idea
.DS_Store

test
48 changes: 48 additions & 0 deletions .env.docker.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Codra self-hosted (Docker) environment example.
# Copy to .env before running `docker compose up -d`:
#
# cp .env.docker.example .env
#
# Keep real secrets in .env only. It is gitignored and excluded from the image.

# --- Database and queue ---
# These configure the bundled postgres service. Change POSTGRES_PASSWORD before
# deploying anywhere real; DATABASE_URL is built from these three values, so you
# do not have to keep a connection string in sync by hand.
POSTGRES_USER="postgres"
POSTGRES_PASSWORD="postgres"
POSTGRES_DB="codra"

# Set these two only to point at a database or Redis you host elsewhere.
# Left unset, they resolve to the bundled postgres and redis services.
# DATABASE_URL="postgres://user:password@host:5432/codra"
# REDIS_URL="redis://host:6379"

# --- Application URLs ---
# Set both to your public URL when deploying behind a domain.
APP_URL="http://localhost:3000"
AUTH_CALLBACK_URL="http://localhost:3000/auth/github/callback"
PORT="3000"
ENVIRONMENT="production"

# --- GitHub App (webhooks, checks, reviews) ---
GITHUB_APP_ID="REPLACE_WITH_YOUR_APP_ID"
GITHUB_APP_SLUG="REPLACE_WITH_YOUR_APP_SLUG"
GITHUB_APP_WEBHOOK_SECRET="REPLACE_WITH_YOUR_WEBHOOK_SECRET"
APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nREPLACE_WITH_YOUR_GITHUB_APP_PRIVATE_KEY_CONTENT\n-----END RSA PRIVATE KEY-----"

# --- GitHub OAuth (dashboard sign-in) ---
GITHUB_CLIENT_ID="REPLACE_WITH_YOUR_CLIENT_ID"
GITHUB_CLIENT_SECRET="REPLACE_WITH_YOUR_CLIENT_SECRET"

# Comma-separated GitHub usernames allowed into the dashboard.
# Set this to your own username, or nobody on your install can sign in.
DASHBOARD_ALLOWED_USERS="REPLACE_WITH_YOUR_GITHUB_USERNAME"

# The bot account name Codra posts reviews as (usually "<app-slug>[bot]").
BOT_USERNAME="REPLACE_WITH_YOUR_BOT_USERNAME"

# --- LLM provider config encryption ---
# Encrypts dashboard-managed provider API keys at rest. Generate with:
# openssl rand -base64 48
LLM_CONFIG_ENCRYPTION_KEY="REPLACE_WITH_A_LONG_RANDOM_ENCRYPTION_KEY"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ web_modules/
.env.*
!.env.example
!.env.test.example
!.env.docker.example

# parcel-bundler cache (https://parceljs.org/)
.cache
Expand Down
85 changes: 85 additions & 0 deletions apps/node/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# syntax=docker/dockerfile:1

# Production image for the Node deployment target (@codraoss/node-server).
# Build context is the repository root: docker build -f apps/node/Dockerfile .

# ── Stage 1: workspace install ────────────────────────────────────────────────
# Manifests are copied before the sources so npm ci stays cached until a
# dependency actually changes.
FROM node:22-alpine AS deps
WORKDIR /app

COPY package.json package-lock.json ./
COPY packages/api/package.json packages/api/
COPY packages/core/package.json packages/core/
COPY packages/db/package.json packages/db/
COPY packages/models/package.json packages/models/
COPY packages/provider-github/package.json packages/provider-github/
COPY packages/schema/package.json packages/schema/
COPY packages/ui/package.json packages/ui/
COPY apps/node/package.json apps/node/
COPY apps/worker/package.json apps/worker/

RUN npm ci

# ── Stage 2: build ────────────────────────────────────────────────────────────
# Two artifacts: the dashboard bundle at dist/client (served by the Node app)
# and the server bundle at apps/node/dist.
#
# `npx vite build` rather than `npm run build`: the root build script chains
# cf-typegen, which shells out to wrangler and has no place in this image.
FROM deps AS builder
WORKDIR /app

COPY . .

RUN npx vite build
RUN npm run build --workspace=@codraoss/node-server

# ── Stage 3: runtime dependencies ─────────────────────────────────────────────
# tsup bundles every @codraoss/* package into the server bundle, so only the
# node-server workspace's own runtime deps are installed here.
FROM node:22-alpine AS prod-deps
WORKDIR /app

COPY package.json package-lock.json ./
COPY packages/api/package.json packages/api/
COPY packages/core/package.json packages/core/
COPY packages/db/package.json packages/db/
COPY packages/models/package.json packages/models/
COPY packages/provider-github/package.json packages/provider-github/
COPY packages/schema/package.json packages/schema/
COPY packages/ui/package.json packages/ui/
COPY apps/node/package.json apps/node/
COPY apps/worker/package.json apps/worker/

RUN npm ci --omit=dev --workspace=@codraoss/node-server

# ── Stage 4: runner ───────────────────────────────────────────────────────────
FROM node:22-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV PORT=3000

# tsup emits ESM. Node 22 only runs it unflagged because of module syntax
# auto-detection; this marks the bundle explicitly so an older base image or a
# stricter loader cannot turn startup into "Cannot use import statement outside
# a module".
RUN printf '{"type":"module"}\n' > package.json

COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=builder /app/apps/node/dist ./dist
COPY --from=builder /app/dist/client ./dist/client

# The migrate service in docker-compose.yml runs from this same image; the
# migration runner is a plain script and is not part of the server bundle.
COPY --from=builder /app/packages/db/package.json ./packages/db/package.json
COPY --from=builder /app/packages/db/scripts ./packages/db/scripts
COPY --from=builder /app/packages/db/migrations ./packages/db/migrations

USER node

EXPOSE 3000

CMD ["node", "dist/index.js"]
94 changes: 94 additions & 0 deletions apps/node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# @codraoss/node-server

The Node.js deployment target for Codra. Runs the same API, dashboard, and review
engine as the Cloudflare Worker, backed by Postgres and Redis instead of
Hyperdrive, KV, and Cloudflare Queues.

Use this when you want Codra on a VM, a VPS, or any platform that runs Docker
(Coolify, Railway, Render, Fly.io).

## Run the stack with Docker

From the repository root:

```bash
cp .env.docker.example .env
# fill in the GitHub App and OAuth values, then:
docker compose up -d
```

The dashboard is at <http://localhost:3000>.

`docker compose up` starts four things:

| Service | What it does |
| ----------- | --------------------------------------------------------- |
| `postgres` | Database, on a named volume so data survives restarts |
| `redis` | Config cache and the review job queue |
| `migrate` | Applies `packages/db/migrations`, then exits |
| `codra-app` | The API, dashboard, and webhook receiver on port 3000 |

The app waits for Postgres and Redis to report healthy and for `migrate` to
finish, so a first boot lands on a ready schema.

Postgres and Redis are published on `127.0.0.1` only, so deploying this file to
a VPS does not expose them to the internet. They are still reachable by anything
else on the host, and they ship with development credentials, so change
`POSTGRES_PASSWORD` before you deploy and drop the `ports:` entries from
`docker-compose.yml` if you do not need to reach them from the host; the
services find each other over the compose network either way.

Useful commands:

```bash
docker compose logs -f codra-app # follow the app logs
docker compose ps # health of each service
docker compose down # stop (volumes are kept)
docker compose down -v # stop and delete the database
```

## Configuration

Every value lives in `.env`; see `.env.docker.example` for the full list with
comments. The ones you must set before the app will boot:

| Variable | Notes |
| --------------------------- | -------------------------------------------------- |
| `GITHUB_APP_ID` | From your GitHub App settings page |
| `GITHUB_APP_WEBHOOK_SECRET` | The webhook secret on that same page |
| `APP_PRIVATE_KEY` | The App private key, newlines written as `\n` |
| `GITHUB_CLIENT_ID` | OAuth client, for dashboard sign-in |
| `GITHUB_CLIENT_SECRET` | OAuth client secret |
| `DASHBOARD_ALLOWED_USERS` | Your GitHub username; nobody else can sign in |
| `LLM_CONFIG_ENCRYPTION_KEY` | `openssl rand -base64 48`, encrypts provider keys |
| `APP_URL` | Public URL; set with `AUTH_CALLBACK_URL` for a domain |

`DATABASE_URL` and `REDIS_URL` already point at the bundled services. Point them
somewhere else to use a managed database or a hosted Redis.

LLM provider API keys are not environment variables: add them from the dashboard
Settings page once you can sign in.

## Running without Docker

```bash
npm ci
npx vite build # builds the dashboard into dist/client
npm run dev --workspace=@codraoss/node-server
```

The server reads `.dev.vars` from the repository root, and expects Postgres and
Redis to be reachable at `DATABASE_URL` and `REDIS_URL`.

## Current limitations

Dashboard sessions are held in memory, not in Redis, so restarting or
redeploying the container signs every dashboard user out. They sign back in
through GitHub; nothing else is lost.

Queued reviews are not processed yet. The webhook receiver, dashboard, and queue
producer all work, but the review runtime for Node is still being built
(issues [#106](https://github.com/devarshishimpi/codra/issues/106) and
[#107](https://github.com/devarshishimpi/codra/issues/107)), so jobs land in
Redis and wait there. Cloudflare Workers remains the deployment target that
completes reviews today.
11 changes: 8 additions & 3 deletions apps/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,14 @@ app.onError((err, c) => {
console.error('HONO ERROR:', err);
return c.text('Custom Error: ' + err.message, 500);
});
app.use('/assets/*', serveStatic({ root: process.cwd().endsWith('node') ? '../../dist/client' : 'dist/client' }));
app.use('/*.svg', serveStatic({ root: process.cwd().endsWith('node') ? '../../dist/client' : 'dist/client' }));
app.use('/*.ico', serveStatic({ root: process.cwd().endsWith('node') ? '../../dist/client' : 'dist/client' }));
const clientRoot = process.cwd().endsWith('node') ? '../../dist/client' : 'dist/client';

app.use('/assets/*', serveStatic({ root: clientRoot }));
// Hono has no suffix matching, so '/*.svg' and '/*.ico' never matched: the root-level
// icons vite copies out of public/ are mounted by their own paths instead.
app.use('/icons/*', serveStatic({ root: clientRoot }));
app.use('/favicon.ico', serveStatic({ root: clientRoot }));
app.use('/favicon.svg', serveStatic({ root: clientRoot }));
const port = parseInt(process.env.PORT || '3000', 10);

serve({
Expand Down
86 changes: 86 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Full Codra stack for self-hosting: Postgres, Redis, and the Node server.
#
# cp .env.docker.example .env
# docker compose up -d
#
# Postgres and Redis are published on the loopback interface only, so a VPS
# running this file does not expose its database to the internet.

services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
POSTGRES_DB: ${POSTGRES_DB:-codra}
ports:
- '127.0.0.1:5432:5432'
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-codra}']
interval: 5s
timeout: 5s
retries: 10

redis:
image: redis:7-alpine
restart: unless-stopped
command: ['redis-server', '--appendonly', 'yes']
ports:
- '127.0.0.1:6379:6379'
volumes:
- redis-data:/data
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 5s
retries: 10

# Applies packages/db/migrations against an empty database and exits. The app
# waits on it, so a fresh `docker compose up` boots against a ready schema.
migrate:
build:
context: .
dockerfile: apps/node/Dockerfile
image: codra-node:latest
restart: 'no'
command: ['node', 'packages/db/scripts/migrate.mjs']
environment:
DATABASE_URL: ${DATABASE_URL:-postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-codra}}
depends_on:
postgres:
condition: service_healthy

codra-app:
build:
context: .
dockerfile: apps/node/Dockerfile
image: codra-node:latest
restart: unless-stopped
env_file:
- .env
environment:
DATABASE_URL: ${DATABASE_URL:-postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-codra}}
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
PORT: '3000'
ports:
- '${PORT:-3000}:3000'
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
migrate:
condition: service_completed_successfully
healthcheck:
test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://127.0.0.1:3000/healthz']
interval: 15s
timeout: 5s
retries: 5
start_period: 20s

volumes:
postgres-data:
redis-data:
Loading