Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/parsar-server-release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Build the parsar-server image in CI and push releases to GHCR.
#
# This is the Phase 1 deliverable's "one command" enabler: docker-compose.local.yml
# This is the Phase 1 deliverable's "one command" enabler: docker-compose.yml
# (and the Phase 2 self-host compose) pull ghcr.io/<owner>/parsar-server by
# default. Same image, two compose profiles — profile not fork.
#
Expand Down
10 changes: 5 additions & 5 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ user is the administrator.

The script:

- uses `docker-compose.local.yml` from the current checkout, or downloads the
published copy to `~/.parsar/docker-compose.local.yml`
- uses `docker-compose.yml` from the current checkout, or downloads the
published copy to `~/.parsar/docker-compose.yml`
- creates `~/.parsar/.env`
- generates and persists `PARSAR_MASTER_KEY`
- generates and persists the Postgres password
Expand Down Expand Up @@ -63,9 +63,9 @@ make docker-build
## Manage The Stack

```bash
docker compose -f ~/.parsar/docker-compose.local.yml --env-file ~/.parsar/.env ps
docker compose -f ~/.parsar/docker-compose.local.yml --env-file ~/.parsar/.env logs -f parsar-server
docker compose -f ~/.parsar/docker-compose.local.yml --env-file ~/.parsar/.env down
docker compose -f ~/.parsar/docker-compose.yml --env-file ~/.parsar/.env ps
docker compose -f ~/.parsar/docker-compose.yml --env-file ~/.parsar/.env logs -f parsar-server
docker compose -f ~/.parsar/docker-compose.yml --env-file ~/.parsar/.env down
```

To remove all local data, stop the stack first and then delete `~/.parsar/`.
Expand Down
19 changes: 3 additions & 16 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,13 @@ SHELL := /bin/bash
PARSAR_IMAGE ?= parsar
PARSAR_IMAGE_TAG ?= dev

.PHONY: setup dev check reset-dev clean-dev paths seed-dev migrate-dev sqlc-generate server web cli devgateway http-runner-once http-runner-loop dev-all smoke e2e-http-agent e2e-feishu-gateway dev-server-up dev-server-down dev-server-log bootstrap docker-build docker-build-no-cache docker-image-info openapi
.PHONY: setup dev check reset-dev clean-dev paths migrate-dev sqlc-generate server web cli devgateway http-runner-once http-runner-loop dev-all smoke e2e-http-agent e2e-feishu-gateway dev-server-up dev-server-down dev-server-log bootstrap docker-build docker-build-no-cache openapi

setup:
./scripts/setup.sh

paths:
./scripts/parsar-paths.sh

seed-dev:
pnpm --filter @parsar/cli parsar -- seed-dev
./scripts/setup.sh paths

migrate-dev:
cd server && go run ./cmd/migrate
Expand Down Expand Up @@ -73,7 +70,7 @@ web:
pnpm --filter @parsar/web dev

cli:
pnpm --filter @parsar/cli parsar -- --help
pnpm --filter @parsar/cli parsar --help

devgateway:
cd server && go run ./cmd/devgateway --help
Expand Down Expand Up @@ -123,16 +120,6 @@ docker-build-no-cache:
-f Dockerfile \
.

# Quick sanity print so operators can confirm what they just built
# without piecing together `docker images | grep ...`. The size
# column is the strongest "I shipped the right thing" signal — if
# the runtime image is >300 MB the multi-stage build broke and
# someone is shipping the Go toolchain.
docker-image-info:
@docker image inspect \
--format 'image={{.RepoTags}} id={{.Id}} created={{.Created}} size={{.Size}}' \
$(PARSAR_IMAGE):$(PARSAR_IMAGE_TAG)

# --- OpenAPI spec (generated from swaggo annotations) ------------------
#
# Runs `swag init` over server/**/*.go. Every handler carrying a
Expand Down
2 changes: 1 addition & 1 deletion deploy/compose/compose.selfhost.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ services:
start_period: 10s

parsar-server:
# Defaults to the GHCR prebuilt image (same one docker-compose.local.yml
# Defaults to the GHCR prebuilt image (same one docker-compose.yml
# uses — profile not fork). Override PARSAR_SERVER_IMAGE in .env to pull
# from your own registry (e.g. an Aliyun ACR mirror) or a pinned tag.
image: ${PARSAR_SERVER_IMAGE:-ghcr.io/minimax-ai-dev/parsar-server:latest}
Expand Down
39 changes: 16 additions & 23 deletions docker-compose.local.yml → docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
#
# One command, single machine, zero secrets to generate:
#
# docker compose -f docker-compose.local.yml up
# docker compose up
#
# Brings up Postgres + a one-shot migration init + parsar-server. Open
# http://127.0.0.1:18080 and create the first owner account in the web setup
# flow.
# Brings up Postgres + parsar-server (which runs migrations on startup
# before serving). Open http://127.0.0.1:18080 and create the first owner
# account in the web setup flow.
#
# Profile not fork: this is the SAME image as the self-host deployment
# (deploy/compose/compose.selfhost.yml). Differences live only in this file
Expand All @@ -17,7 +17,7 @@
# - Local build fallback (before the tag is published, or to test your
# own changes):
# make docker-build PARSAR_IMAGE=parsar PARSAR_IMAGE_TAG=local
# PARSAR_SERVER_IMAGE=parsar:local docker compose -f docker-compose.local.yml up
# PARSAR_SERVER_IMAGE=parsar:local docker compose up
#
# Override knobs (all optional — defaults make `up` work with no .env):
# PARSAR_PROJECT_NAME compose project/network prefix (default: parsar)
Expand Down Expand Up @@ -59,33 +59,26 @@ services:
retries: 20
start_period: 10s

# One-shot: run migrations before the server starts. First owner creation
# happens in the web setup flow (POST /api/v1/bootstrap).
parsar-init:
parsar-server:
image: ${PARSAR_SERVER_IMAGE:-ghcr.io/minimax-ai-dev/parsar-server:latest}
container_name: ${PARSAR_PROJECT_NAME:-parsar}-local-init
container_name: ${PARSAR_PROJECT_NAME:-parsar}-local-server
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgres://parsar:${PARSAR_PG_PASSWORD:-parsar}@postgres:5432/parsar?sslmode=disable
# Run migrations before serving, in the same container as the server.
# goose migrations are idempotent (already-applied ones are skipped),
# so re-running this on every `restart: unless-stopped` cycle is a
# cheap no-op — no separate one-shot init container needed. `exec`
# replaces the shell with parsar-server so it stays tini's direct
# child for correct signal handling. First owner creation happens in
# the web setup flow (POST /api/v1/bootstrap).
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -e
parsar-migrate
echo "[parsar-init] migrations complete — create the first owner in the web setup flow"
restart: "no"

parsar-server:
image: ${PARSAR_SERVER_IMAGE:-ghcr.io/minimax-ai-dev/parsar-server:latest}
container_name: ${PARSAR_PROJECT_NAME:-parsar}-local-server
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
parsar-init:
condition: service_completed_successfully
exec /usr/local/bin/parsar-server
ports:
- "${PARSAR_BIND_ADDR:-127.0.0.1}:${PARSAR_LOCAL_PORT:-18080}:8080"
environment:
Expand Down
88 changes: 41 additions & 47 deletions docs/deploy/lan-deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,9 @@ PARSAR_FEISHU_VERIFICATION_TOKEN=xxxxxxxx # Verification Token from §2.1
PARSAR_FEISHU_DEFAULT_BOT_OPEN_ID=ou_xxxxxx # Bot Open ID from §2.1

# ---- Security ----
# PARSAR_MASTER_KEY encrypts every Bot credential in the DB. Leave blank on
# first run — parsar-init in step 6 will auto-generate one on `up`, print it
# to the logs, and then fatal-exit; copy the printed key back into .env and
# run `up` again. You can also pre-generate:
# PARSAR_MASTER_KEY encrypts every Bot credential in the DB. There is no
# auto-generation — parsar-server fatal-exits at startup if this is empty
# in production profile. Generate it before the first `up`:
# echo "PARSAR_MASTER_KEY=$(openssl rand -hex 32)" >> .env
# Once set, DO NOT change it: any Bot credentials already stored become
# undecryptable and every Bot must be re-bound.
Expand Down Expand Up @@ -241,23 +240,22 @@ sudo docker run --rm --entrypoint /bin/sh parsar-sandbox:local \
## 6. Bring up the service stack

```bash
sudo docker compose -f docker-compose.local.yml up -d
sudo docker compose -f docker-compose.yml up -d
```

**Startup order (automatic):**
1. `postgres` — PostgreSQL 16, wait for healthcheck.
2. `parsar-init` — master-key validation + database migration.
3. `parsar-server` — binds `0.0.0.0:18080`; Feishu WebSocket inbound + outbound worker start automatically.
2. `parsar-server` — runs `parsar-migrate` then starts serving; binds `0.0.0.0:18080`; Feishu WebSocket inbound + outbound worker start automatically.

> **First run will fatal-exit — that is intentional.**
> If you did not manually pre-fill `PARSAR_MASTER_KEY` in step 4, `parsar-init` generates one, prints it to its logs (`sudo docker logs parsar-local-init`), and then `parsar-server` refuses to start because env still has no key. **Copy the printed `PARSAR_MASTER_KEY=...` line from the init log into `.env`** and run `up -d` again to complete startup.
> On subsequent starts, if the key does not change, parsar-init only runs migrations and leaves the key alone.
> **If you left `PARSAR_MASTER_KEY` blank in step 4, startup will fatal-exit.**
> `parsar-server` refuses to start in production profile without a master key — it does not generate one for you. **Generate it yourself** (`echo "PARSAR_MASTER_KEY=$(openssl rand -hex 32)" >> .env`) **before** running `up -d`.
> Migrations re-run (as a fast no-op) on every restart, so this is safe to run `up -d` again after fixing `.env`.

**Verify:**

```bash
# Container status
sudo docker compose -f docker-compose.local.yml ps
sudo docker compose -f docker-compose.yml ps

# Health checks
curl -s http://YOUR_IP:18080/healthz # 200
Expand Down Expand Up @@ -320,16 +318,15 @@ sudo docker logs parsar-local-server 2>&1 | grep "feishu.*inbound.*ready"
### View logs

```bash
sudo docker logs -f parsar-local-server # server logs
sudo docker logs parsar-local-init # migration logs
sudo docker logs -f parsar-local-server # server logs (includes migration output on startup)
sudo docker logs parsar-local-postgres # DB logs
```

### Stop / clean up

```bash
sudo docker compose -f docker-compose.local.yml down # stop, keep data
sudo docker compose -f docker-compose.local.yml down -v # also delete data volumes
sudo docker compose -f docker-compose.yml down # stop, keep data
sudo docker compose -f docker-compose.yml down -v # also delete data volumes
```

### Upgrade
Expand All @@ -338,7 +335,7 @@ sudo docker compose -f docker-compose.local.yml down -v # also delete data vo
git pull
sudo docker build -t parsar:local .
sudo docker build -f infra/sandbox/Dockerfile.local -t parsar-sandbox:local .
sudo docker compose -f docker-compose.local.yml up -d --force-recreate
sudo docker compose -f docker-compose.yml up -d --force-recreate
```

### Change port
Expand All @@ -347,7 +344,7 @@ Edit `PARSAR_LOCAL_PORT` in `.env` and **also update**:
- the port in `PARSAR_FEISHU_REDIRECT_URI`
- the redirect URL configured on the Feishu Open Platform

Then `sudo docker compose -f docker-compose.local.yml up -d --force-recreate`.
Then `sudo docker compose -f docker-compose.yml up -d --force-recreate`.

---

Expand All @@ -361,7 +358,7 @@ HTTP_PROXY=http://your-proxy:port
HTTPS_PROXY=http://your-proxy:port
```

`docker-compose.local.yml` reads these variables and passes them to the
`docker-compose.yml` reads these variables and passes them to the
container. Leave them blank on machines that do not need a proxy.

You must also pass the proxy args at image-build time (see the `--build-arg`
Expand All @@ -373,12 +370,12 @@ snippets in §5) because `docker build` does not read `.env`.

| Symptom | Cause | Fix |
|---|---|---|
| Server fatal-exits with `secret.master_key is required in production` | `.env` has an empty `PARSAR_MASTER_KEY` | Let `parsar-init` run once → copy the generated key from its logs into `.env` → `up -d` again |
| Server fatal-exits with `secret.master_key is required in production` | `.env` has an empty `PARSAR_MASTER_KEY` | Generate one yourself (`echo "PARSAR_MASTER_KEY=$(openssl rand -hex 32)" >> .env`) → `up -d` again |
| Feishu login reports `redirect_uri mismatch` | `.env`'s `REDIRECT_URI` does not match the Feishu console | Keep both sides completely identical (scheme, IP, port, path) |
| Other machines cannot reach 18080 | Firewall blocking it | `sudo ufw allow 18080/tcp` or the equivalent firewall rule |
| Device pairing downloads daemon and hits **404** | The GHCR image has no embedded daemon | Build the server image locally (§5.1) |
| Agent reports **"no runtime yet — ask an admin to rebuild it"** | The sandbox image lacks the Agent CLI | Rebuild the sandbox image via `Dockerfile.local` (§5.2), then click Rebuild in the UI |
| Sandbox comes up but the daemon cannot reach the server | Compose project name is not `parsar`, so network names do not match | Use the repo's `docker-compose.local.yml` (has `name: parsar` pinned at the top); do not override with `-p <other name>` |
| Sandbox comes up but the daemon cannot reach the server | Compose project name is not `parsar`, so network names do not match | Use the repo's `docker-compose.yml` (has `name: parsar` pinned at the top); do not override with `-p <other name>` |
| Group @Bot **no response**, DM works fine | `PARSAR_FEISHU_DEFAULT_BOT_OPEN_ID` not set | Add the Bot's open_id to `.env` and restart the server |
| Bot receives messages but does not reply | Outbound worker is not up | Grep server logs for `feishu outbound`; confirm `PARSAR_FEISHU_OUTBOUND=true` (compose already sets it) |
| Server crash-loops with `owner URL not resolvable` | `PARSAR_AGENT_DAEMON_OWNER_URL` missing | Confirm compose has `PARSAR_AGENT_DAEMON_OWNER_URL: "http://parsar-server:8080"` |
Expand All @@ -390,32 +387,29 @@ snippets in §5) because `docker build` does not read `.env`.
## Architecture overview

```
┌─────────────────────────────────────────────────────────────────┐
│ Deploy machine (YOUR_IP) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌─────────────────────────┐ │
│ │ PostgreSQL │ │ parsar-init│ │ parsar-server │ │
│ │ :5432 │ │ (one-shot) │ │ SPA + API + WS │ │
│ │ data vol │ │ migrate+ │ │ Feishu WS inbound + │ │
│ │ │ │ onboard │ │ outbound worker │ │
│ └────────────┘ └────────────┘ │ Docker sandbox mgr │ │
│ └─────────┬───────────────┘ │
│ │ │
│ ┌──────────────────────┐ 0.0.0.0:18080 │
│ │ sandbox container │ │ │
│ │ (on demand) │ │ │
│ │ Claude Code + Codex │ │ │
│ │ parsar-daemon │ │ │
│ └──────────────────────┘ │ │
└────────────────────────────────────────────┼───────────────────┘
┌──────────────────────────────────┼─────────────┐
│ LAN / Feishu │ │
│ │ │
│ User browser ──── HTTP ────────┘ │
│ User device ────── WebSocket ──┘ │
│ Feishu group/DM ── Feishu WS ──┘ │
└────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Deploy machine (YOUR_IP) │
│ │
│ ┌──────────────┐ ┌──────────────────────────────────────┐ │
│ │PostgreSQL │ │parsar-server │ │
│ │:5432 │ │migrate on startup, then: │ │
│ │data vol │ │SPA + API + WS │ │
│ └──────────────┘ │Feishu WS inbound + outbound worker │ │
│ │Docker sandbox mgr │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │sandbox container (on demand) │ │
│ │Claude Code + Codex + parsar-daemon │ │
│ └──────────────────────────────────────┘ │
│ │
│ Listens on 0.0.0.0:18080 │
└─────────────────────────────────────────────────────────────────────┘

LAN / Feishu clients reach parsar-server on port 18080:
- User browser ── HTTP ──────→ parsar-server (web UI)
- User device ── WebSocket ─→ parsar-server (device pairing)
- Feishu group/DM ── Feishu WS ─→ parsar-server (bot)
```

---
Expand All @@ -434,7 +428,7 @@ sudo docker build -t parsar:local .
sudo docker build -f infra/sandbox/Dockerfile.local -t parsar-sandbox:local .

# 3. Bring it up
sudo docker compose -f docker-compose.local.yml up -d
sudo docker compose -f docker-compose.yml up -d

# 4. Verify
curl http://YOUR_IP:18080/healthz # 200
Expand Down
Loading