Skip to content

Commit 54ed232

Browse files
Build once, pull everywhere: GHCR images + registry deploy mode + launcher
- ci-cd.yml: add publish job that builds api/web/worker on GHA and pushes to ghcr.io/<owner>/bidwright-<service>. Deploy now needs publish to complete before running, so a tag rollback is just `BIDWRIGHT_TAG=sha-x`. - docker-compose.prod-registry.yml: prod-shaped compose using image: refs. db-migrate runs the runtime API image with `pnpm db:push` instead of using a build target. - remote-deploy.sh: DEPLOY_MODE switch (build|registry, default build). Build mode is unchanged. Registry mode pulls images and skips local builds. - scripts/launcher/: standalone double-click launcher (start/stop/update for Windows + macOS) that pulls images from GHCR. Project namespace bidwright-launcher to avoid collision with dev or prod. - Provider keys live in OrganizationSettings (Settings → Integrations), not env vars. Launcher .env carries only port/tag/registry overrides. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent addaa5b commit 54ed232

12 files changed

Lines changed: 763 additions & 3 deletions

File tree

.github/workflows/ci-cd.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99

1010
permissions:
1111
contents: read
12+
packages: write
1213

1314
jobs:
1415
verify:
@@ -60,9 +61,80 @@ jobs:
6061
- name: Validate production compose
6162
run: docker compose -f docker-compose.prod.yml config > /dev/null
6263

64+
- name: Validate production registry compose
65+
run: docker compose -f docker-compose.prod-registry.yml config > /dev/null
66+
67+
- name: Validate launcher compose
68+
run: docker compose -f scripts/launcher/docker-compose.yml config > /dev/null
69+
70+
publish:
71+
needs:
72+
- verify
73+
runs-on: ubuntu-latest
74+
timeout-minutes: 60
75+
if: >
76+
github.event_name == 'workflow_dispatch' ||
77+
(github.event_name == 'push' && github.ref == 'refs/heads/main')
78+
permissions:
79+
contents: read
80+
packages: write
81+
strategy:
82+
fail-fast: false
83+
matrix:
84+
include:
85+
- service: api
86+
dockerfile: Dockerfile.api
87+
- service: web
88+
dockerfile: Dockerfile.web
89+
- service: worker
90+
dockerfile: Dockerfile.worker
91+
steps:
92+
- name: Check out repo
93+
uses: actions/checkout@v4
94+
95+
- name: Set up Docker Buildx
96+
uses: docker/setup-buildx-action@v3
97+
98+
- name: Log in to GHCR
99+
uses: docker/login-action@v3
100+
with:
101+
registry: ghcr.io
102+
username: ${{ github.actor }}
103+
password: ${{ secrets.GITHUB_TOKEN }}
104+
105+
- name: Lowercase repository owner
106+
id: repo
107+
run: echo "owner_lc=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_OUTPUT"
108+
109+
- name: Compute image metadata
110+
id: meta
111+
uses: docker/metadata-action@v5
112+
with:
113+
images: ghcr.io/${{ steps.repo.outputs.owner_lc }}/bidwright-${{ matrix.service }}
114+
tags: |
115+
type=raw,value=latest,enable={{is_default_branch}}
116+
type=sha,format=short
117+
type=ref,event=branch
118+
type=ref,event=tag
119+
type=semver,pattern={{version}}
120+
type=semver,pattern={{major}}.{{minor}}
121+
122+
- name: Build and push ${{ matrix.service }}
123+
uses: docker/build-push-action@v6
124+
with:
125+
context: .
126+
file: ${{ matrix.dockerfile }}
127+
push: true
128+
tags: ${{ steps.meta.outputs.tags }}
129+
labels: ${{ steps.meta.outputs.labels }}
130+
cache-from: type=gha,scope=${{ matrix.service }}
131+
cache-to: type=gha,mode=max,scope=${{ matrix.service }}
132+
provenance: false
133+
63134
deploy:
64135
needs:
65136
- verify
137+
- publish
66138
# Deploys run from a self-hosted runner that can reach the configured target host.
67139
runs-on: [self-hosted, linux, x64, bidwright-prod]
68140
timeout-minutes: 40

docker-compose.prod-registry.yml

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Production compose — registry mode.
2+
#
3+
# Mirrors docker-compose.prod.yml but pulls prebuilt images from GHCR
4+
# instead of building them on the deploy host. Pin BIDWRIGHT_TAG in
5+
# .env.server to a specific :sha-* tag for immutable releases; rolling
6+
# back is then a tag swap + `docker compose up -d`.
7+
#
8+
# The build-mode compose (docker-compose.prod.yml) is retained as a
9+
# documented fallback path. remote-deploy.sh selects between them via
10+
# the COMPOSE_FILE / DEPLOY_MODE variables.
11+
12+
services:
13+
postgres:
14+
image: pgvector/pgvector:pg16
15+
restart: unless-stopped
16+
environment:
17+
POSTGRES_USER: "${POSTGRES_USER:-bidwright}"
18+
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-bidwright}"
19+
POSTGRES_DB: "${POSTGRES_DB:-bidwright}"
20+
volumes:
21+
- ${POSTGRES_DATA_PATH:-pgdata}:/var/lib/postgresql/data
22+
- ./scripts/db/init-pgvector.sql:/docker-entrypoint-initdb.d/01-pgvector.sql:ro
23+
healthcheck:
24+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-bidwright} -d ${POSTGRES_DB:-bidwright}"]
25+
interval: 5s
26+
timeout: 5s
27+
retries: 5
28+
29+
redis:
30+
image: redis:7-alpine
31+
restart: unless-stopped
32+
volumes:
33+
- ${REDIS_DATA_PATH:-redisdata}:/data
34+
healthcheck:
35+
test: ["CMD", "redis-cli", "ping"]
36+
interval: 5s
37+
timeout: 5s
38+
retries: 5
39+
40+
ollama:
41+
image: ollama/ollama
42+
profiles:
43+
- embeddings
44+
restart: unless-stopped
45+
volumes:
46+
- ${OLLAMA_DATA_PATH:-ollama_data}:/root/.ollama
47+
healthcheck:
48+
test: ["CMD-SHELL", "ollama list >/dev/null 2>&1 || exit 1"]
49+
interval: 10s
50+
timeout: 5s
51+
retries: 12
52+
start_period: 30s
53+
54+
ollama-init:
55+
image: curlimages/curl:latest
56+
profiles:
57+
- embeddings
58+
depends_on:
59+
ollama:
60+
condition: service_healthy
61+
restart: "no"
62+
entrypoint: >
63+
sh -c "curl -sf -X POST http://ollama:11434/api/pull -d '{\"name\":\"${EMBEDDING_MODEL:-snowflake-arctic-embed}\"}' && echo 'Model ready'"
64+
65+
# Runs Prisma db:push using the runtime API image (which carries the schema
66+
# and pnpm tooling because it COPYs the full /app from the builder stage).
67+
# No build target needed — we run the same image that powers the API service.
68+
db-migrate:
69+
image: "${BIDWRIGHT_REGISTRY:-ghcr.io/braedonsaunders}/bidwright-api:${BIDWRIGHT_TAG:-latest}"
70+
depends_on:
71+
postgres:
72+
condition: service_healthy
73+
environment:
74+
DATABASE_URL: "${DATABASE_URL:-postgresql://bidwright:bidwright@postgres:5432/bidwright}"
75+
restart: "no"
76+
command: ["pnpm", "--filter", "@bidwright/db", "db:push"]
77+
78+
api:
79+
image: "${BIDWRIGHT_REGISTRY:-ghcr.io/braedonsaunders}/bidwright-api:${BIDWRIGHT_TAG:-latest}"
80+
restart: unless-stopped
81+
depends_on:
82+
postgres:
83+
condition: service_healthy
84+
redis:
85+
condition: service_healthy
86+
db-migrate:
87+
condition: service_completed_successfully
88+
environment:
89+
DATABASE_URL: "${DATABASE_URL:-postgresql://bidwright:bidwright@postgres:5432/bidwright}"
90+
REDIS_URL: "${REDIS_URL:-redis://redis:6379}"
91+
DATA_DIR: "${DATA_DIR:-/data}"
92+
API_PORT: "${API_PORT:-3001}"
93+
CLAUDE_CONFIG_DIR: "${CLAUDE_CONFIG_DIR:-/root/.claude}"
94+
CODEX_HOME: "${CODEX_HOME:-/root/.codex}"
95+
EMBEDDING_PROVIDER: "${EMBEDDING_PROVIDER:-local}"
96+
EMBEDDING_BASE_URL: "http://ollama:11434/v1"
97+
EMBEDDING_MODEL: "${EMBEDDING_MODEL:-snowflake-arctic-embed}"
98+
EMBEDDING_DIMENSIONS: "${EMBEDDING_DIMENSIONS:-1024}"
99+
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
100+
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
101+
OPENAI_MODEL: "${OPENAI_MODEL:-gpt-5}"
102+
volumes:
103+
- ${BIDWRIGHT_DATA_PATH:-./data/bidwright-api}:/data
104+
- ${CLAUDE_CONFIG_PATH:-/opt/bidwright/data/agent-home/claude}:/root/.claude
105+
- ${CODEX_HOME_PATH:-/opt/bidwright/data/agent-home/codex}:/root/.codex
106+
ports:
107+
- "${API_PUBLIC_PORT:-3001}:3001"
108+
healthcheck:
109+
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3001/health >/dev/null || exit 1"]
110+
interval: 10s
111+
timeout: 5s
112+
retries: 12
113+
start_period: 20s
114+
115+
web:
116+
image: "${BIDWRIGHT_REGISTRY:-ghcr.io/braedonsaunders}/bidwright-web:${BIDWRIGHT_TAG:-latest}"
117+
restart: unless-stopped
118+
depends_on:
119+
- api
120+
environment:
121+
NEXT_PUBLIC_API_BASE_URL: "${NEXT_PUBLIC_API_BASE_URL:-http://localhost:3001}"
122+
INTERNAL_API_BASE_URL: "http://api:3001"
123+
ports:
124+
- "${WEB_PUBLIC_PORT:-3000}:3000"
125+
healthcheck:
126+
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000 >/dev/null || exit 1"]
127+
interval: 10s
128+
timeout: 5s
129+
retries: 12
130+
start_period: 20s
131+
132+
worker:
133+
image: "${BIDWRIGHT_REGISTRY:-ghcr.io/braedonsaunders}/bidwright-worker:${BIDWRIGHT_TAG:-latest}"
134+
restart: unless-stopped
135+
depends_on:
136+
postgres:
137+
condition: service_healthy
138+
redis:
139+
condition: service_healthy
140+
db-migrate:
141+
condition: service_completed_successfully
142+
environment:
143+
DATABASE_URL: "${DATABASE_URL:-postgresql://bidwright:bidwright@postgres:5432/bidwright}"
144+
REDIS_URL: "${REDIS_URL:-redis://redis:6379}"
145+
DATA_DIR: "${DATA_DIR:-/data}"
146+
API_PORT: "${API_PORT:-3001}"
147+
EMBEDDING_PROVIDER: "${EMBEDDING_PROVIDER:-local}"
148+
EMBEDDING_BASE_URL: "http://ollama:11434/v1"
149+
EMBEDDING_MODEL: "${EMBEDDING_MODEL:-snowflake-arctic-embed}"
150+
EMBEDDING_DIMENSIONS: "${EMBEDDING_DIMENSIONS:-1024}"
151+
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
152+
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
153+
volumes:
154+
- ${BIDWRIGHT_DATA_PATH:-./data/bidwright-api}:/data
155+
156+
volumes:
157+
pgdata:
158+
redisdata:
159+
ollama_data:

scripts/deploy/remote-deploy.sh

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,32 @@ set -a
1717
. "${ENV_FILE}"
1818
set +a
1919

20+
# DEPLOY_MODE selects which compose file backs the stack:
21+
# build — docker-compose.prod.yml, builds images on this host (legacy)
22+
# registry — docker-compose.prod-registry.yml, pulls images from GHCR
23+
# Default is build so an unconfigured server keeps the legacy behaviour.
24+
DEPLOY_MODE="${DEPLOY_MODE:-build}"
25+
26+
if [[ "${DEPLOY_MODE}" == "registry" ]]; then
27+
COMPOSE_FILE="${COMPOSE_FILE:-${APP_DIR}/docker-compose.prod-registry.yml}"
28+
elif [[ "${DEPLOY_MODE}" == "build" ]]; then
29+
COMPOSE_FILE="${COMPOSE_FILE:-${APP_DIR}/docker-compose.prod.yml}"
30+
else
31+
echo "Unsupported DEPLOY_MODE: ${DEPLOY_MODE} (expected 'build' or 'registry')" >&2
32+
exit 1
33+
fi
34+
35+
if [[ ! -f "${COMPOSE_FILE}" ]]; then
36+
echo "Missing compose file: ${COMPOSE_FILE}" >&2
37+
exit 1
38+
fi
39+
40+
echo "Deploy mode: ${DEPLOY_MODE}"
41+
echo "Compose file: ${COMPOSE_FILE}"
42+
if [[ "${DEPLOY_MODE}" == "registry" ]]; then
43+
echo "Image tag: ${BIDWRIGHT_TAG:-latest} from ${BIDWRIGHT_REGISTRY:-ghcr.io/braedonsaunders}"
44+
fi
45+
2046
mkdir -p \
2147
"${DEPLOY_BASE}/releases" \
2248
"${BIDWRIGHT_DATA_PATH:-${DEPLOY_BASE}/data/app}" \
@@ -30,7 +56,7 @@ compose() {
3056
docker compose \
3157
-p "${COMPOSE_PROJECT_NAME}" \
3258
--env-file "${ENV_FILE}" \
33-
-f "${APP_DIR}/docker-compose.prod.yml" \
59+
-f "${COMPOSE_FILE}" \
3460
"$@"
3561
}
3662

@@ -90,6 +116,13 @@ smoke_pdf_generation() {
90116
}
91117

92118
compose_up_profiles config >/dev/null
119+
120+
# Registry mode: fail fast if images aren't available before we touch the running stack.
121+
if [[ "${DEPLOY_MODE}" == "registry" ]]; then
122+
echo "Pulling images from registry..."
123+
compose_up_profiles pull
124+
fi
125+
93126
compose up -d postgres redis
94127
wait_for_postgres
95128

@@ -98,11 +131,20 @@ if is_local_embeddings; then
98131
compose_up_profiles run --rm ollama-init
99132
fi
100133

101-
compose build db-migrate
134+
# Build mode rebuilds the migrate image from source; registry mode reuses
135+
# the runtime API image, so no build step is needed.
136+
if [[ "${DEPLOY_MODE}" == "build" ]]; then
137+
compose build db-migrate
138+
fi
102139
cleanup_db_migrate_container
103140
compose run --rm db-migrate
104141
compose run --rm db-migrate sh -lc "pnpm --filter @bidwright/db exec tsx src/run-seed-datasets.ts && pnpm --filter @bidwright/db exec tsx src/run-seed-plugins.ts"
105-
compose_up_profiles up -d --build --remove-orphans api web worker
142+
143+
if [[ "${DEPLOY_MODE}" == "build" ]]; then
144+
compose_up_profiles up -d --build --remove-orphans api web worker
145+
else
146+
compose_up_profiles up -d --remove-orphans api web worker
147+
fi
106148
cleanup_db_migrate_container
107149

108150
wait_for_url "http://127.0.0.1:${API_PUBLIC_PORT:-3001}/health"

scripts/launcher/.env.example

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Bidwright launcher — optional overrides.
2+
#
3+
# Copy this file to ".env" only if you want to change the defaults.
4+
# AI provider keys (Anthropic, OpenAI, etc.) are NOT set here — configure
5+
# them in the app's Settings page after first launch.
6+
7+
# Pin to a specific image tag (default: latest).
8+
# BIDWRIGHT_TAG=latest
9+
10+
# Use a fork or alternate registry (default: ghcr.io/braedonsaunders).
11+
# BIDWRIGHT_REGISTRY=ghcr.io/your-org
12+
13+
# Local ports (change if 3000/3001 are already in use).
14+
# WEB_PUBLIC_PORT=3000
15+
# API_PUBLIC_PORT=3001
16+
17+
# URL the browser uses to reach the API. Only change for non-localhost setups
18+
# (e.g. a Hetzner box with a domain). Must be reachable from the user's browser.
19+
# NEXT_PUBLIC_API_BASE_URL=http://localhost:3001

0 commit comments

Comments
 (0)