The CI workflow (projecte-runner) was never registered on the Gitea instance, leaving quality/e2e/deploy/smoke jobs stuck at 'Waiting to run'. Document the act_runner install/register/start flow so the pipeline can be unblocked from the deploy host.
7.8 KiB
Project E — Deploy Guide
Production runs as a docker-compose stack on the deploy host (10.0.0.204). The stack has four services: PostgreSQL, the Hono API, the Vite SPA served by Caddy, and the Bun worker. Deploys are normally driven by Gitea Actions, but every step below can be run by hand.
Architecture
Internet → :3000 (SPA container, Caddy)
├── /api/* → api:3000 (Hono/Bun)
├── /mcp* → api:3000 (Hono/Bun)
└── /* → index.html (SPA fallback)
API (:3001, direct) → PostgreSQL (:5432)
Worker → PostgreSQL
Caddy (in the spa container) serves the built SPA and reverse-proxies /api/* and /mcp* to the api service. The API is also exposed directly on :3001 for debugging.
Services
| Service | Container | Image / Build | Ports | Notes |
|---|---|---|---|---|
db |
project-e-db |
postgres:16-alpine |
5432 | Data in the project-e-pg-data volume |
api |
project-e-api |
Dockerfile.api |
3001 → 3000 | Hono on Bun |
spa |
project-e-spa |
Dockerfile.spa |
3000 → 80 | Built Vite SPA + Caddy |
worker |
project-e-worker |
Dockerfile.worker |
none | Bun worker |
All services share the project-e-network bridge and restart unless stopped.
CI/CD pipeline (Gitea Actions)
The workflow lives at .gitea/workflows/ci.yml and runs on the self-hosted runner projecte-runner, which is colocated with the deploy host. The runner must be registered once (see "Setting up the runner" below) before any job can run.
quality— runs on every push and pull request:bun install --frozen-lockfile→bun run typecheck→ web build (cd apps/web && bun run build) →docker compose build. A failed quality gate blocks the deploy job.deploy— runs on pushes tomainand onworkflow_dispatch. It checks out the code and runsbash script/deploy.shwithDEPLOY_DIRdefaulting to/home/projecte/ProjectE.smoke— runs afterdeploy(also on deploy failure): API health athttp://localhost:3000/api/health, SPA root returns HTML, and a login POST to/api/auth/credentialsusingINITIAL_ADMIN_EMAIL/INITIAL_ADMIN_PASSWORDfrom the host.env.
What script/deploy.sh does
Idempotent, safe to re-run. It:
- Syncs the CI checkout into
DEPLOY_DIR(rsync, excluding.git,node_modules, build artifacts, and.env) - Loads secrets from the host
.env(never overwrites it) - Installs dependencies with
bun install --frozen-lockfile - Runs
bun run db:migrate - Runs
docker compose buildanddocker compose up -d - Waits up to 60s for
http://localhost:3000/api/healthto return HTTP 200, dumping recent API logs if it times out
Setting up the runner (one-time)
The workflow runs on the self-hosted runner projecte-runner, which must be
registered on the Gitea instance before any job can start (otherwise jobs stay
stuck at "Waiting to run"). Do this once on the deploy host (10.0.0.204), which
is where the deploy and smoke jobs act on the local docker-compose stack:
-
Install
act_runner— download theact_runnerbinary from the Gitea Releases page matching your server version, or use the officialgitea/act_runnerDocker image. (Gitea Actions usesact_runner; the exact binary/URL depends on your Gitea version.) -
Create a registration token — in the Gitea web UI go to Repository → Settings → Actions → Runners → Create new runner and copy the token.
-
Register with a label that matches the workflow's
runs-on:./act_runner register \ --instance https://git.buzzbee.dev \ --token <REGISTRATION_TOKEN> \ --name projecte-runner \ --labels projecte-runner:hostThe label name
projecte-runnermust match.gitea/workflows/ci.yml; the executor (hostordocker) is your choice —hostis simplest for a single colocated runner. -
Start it — run
./act_runner daemon(or install it as a systemd service so it survives reboots).
Verify: the Runners page shows it online, then re-trigger the pipeline (a
push to main, or "Re-run" on the Actions tab). The quality job should leave
"Waiting to run". If jobs stay queued, the runner is offline or its label does
not match runs-on: projecte-runner — check the act_runner logs.
Secrets
Secrets live in the host .env at /home/projecte/ProjectE/.env. This file is gitignored; never commit it. To set it up:
cp .env.example .env
Fill in POSTGRES_PASSWORD, DATABASE_URL, AUTH_SECRET (or NEXTAUTH_SECRET), INITIAL_ADMIN_EMAIL, INITIAL_ADMIN_PASSWORD, and, as needed, NODE_ENV, PUBLIC_URL, COOKIE_SECURE, and ALLOWED_HOSTS. deploy.sh sources it, and docker-compose reads the POSTGRES_PASSWORD and DATABASE_URL values from it.
Manual deploy
On the deploy host:
cd /home/projecte/ProjectE
# Pull latest
git pull origin main
# Install dependencies
bun install
# Apply schema + triggers (idempotent)
bun run db:migrate
# Build images
docker compose build
# Restart the stack
docker compose up -d
# Wait for API health
until curl -s http://localhost:3000/api/health | grep -q '"status"'; do sleep 2; done
# Check status
docker compose ps
Database migrations
bun run db:migrate chains two idempotent steps:
db:sync—drizzle-kit push --force, which syncs the schema inpackages/db/src/schema.tsto the databasedb:triggers—script/apply-triggers.ts, which applies the search-vector triggers fromdrizzle/0005_search_vector_trigger.sql(CREATE OR REPLACE FUNCTION+DROP TRIGGER IF EXISTS)
Both are safe to run on every deploy. Schema changes go through bun run db:generate in development, then land in drizzle/ before the next deploy.
Rollback
Compose images are rebuilt from the checkout, so there are no pinned image tags to restore. To roll back a bad release:
- Revert the checkout to the previous good commit:
git revert <sha>(orgit checkout <sha>) and push tomain - Re-run the manual deploy steps (
docker compose build,docker compose up -d)
The project-e-pg-data volume is untouched by deploys and rollbacks, so the database survives both. If a deploy failed, deploy.sh exits non-zero with the recent API logs; do not force it past a failing health check.
Logs
# All services
docker compose logs --tail=50 -f
# Specific service
docker compose logs --tail=50 -f api
docker compose logs --tail=50 -f spa
docker compose logs --tail=50 -f worker
docker compose logs --tail=50 -f db
Debugging
API health (direct, :3001)
curl http://localhost:3001/api/health
Returns {"status":"ok", ...} with a database ping (database.connected, database.ping_ms).
API health (through Caddy)
curl http://localhost:3000/api/health
SPA health check
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/
Login test
curl -s -X POST http://localhost:3000/api/auth/credentials \
-H "Content-Type: application/json" \
-d '{"email":"<INITIAL_ADMIN_EMAIL>","password":"<INITIAL_ADMIN_PASSWORD>"}'
Expect HTTP 200 and a session cookie.
Container health
docker inspect project-e-db --format '{{.State.Health.Status}}'
Restart or rebuild one service
docker compose restart api
docker compose build spa
docker compose up -d --force-recreate spa
Important notes
- The
project-e-pg-dataDocker volume contains the live database. Do not delete it. Back it up (volume snapshot orpg_dump) before major schema work. - Port 3000 is the SPA (Caddy); port 3001 is the API directly (for debugging).
- The MCP endpoint requires a valid API key (separate from JWT auth).
- The root
worker/directory is legacy. The active worker isapps/worker.