> ## Documentation Index
> Fetch the complete documentation index at: https://dev.haico.gr/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Deployment

> How HAI-Co² ships: the two-branch tag-driven release model, the single CI/CD pipeline, the dev and production environments, and the secrets they need.

HAI-Co² builds two Docker images (`haico-backend`, `haico-frontend`), pushes them to the GitHub
Container Registry (GHCR), and runs them on two self-hosted environments via `docker compose`. The
deployment trees live under [`deployment/`](https://github.com/petrosrapto/HAICO/blob/main/deployment): `dev/` and `prod/` are structurally identical (they pull
images), while `local/` builds from source (see [Local installation](/docs/local-installation.md)).

## Release model: two branches, tag-driven

The flow (recorded in [ADR-0001](/docs/adr/0001-two-branch-model.md)) has two long-lived branches:
`develop` integrates all PRs and deploys to **dev**; `main` is stable, only ever fast-forwarded from
`develop`, and deploys to **production**. A release is cut by pushing a SemVer tag.

```mermaid theme={null}
flowchart LR
    F["feature/* branch"] -->|"PR"| D["develop"]
    D -->|"push tag dev-vX.Y.Z"| DEV["Dev environment<br/>dev.haico.gr"]
    D -->|"git merge --ff-only + push"| M["main"]
    M -->|"push tag vX.Y.Z"| PROD["Production<br/>haico.gr"]
```

| Tag | Environment | Images published | Deploys to |
| - | - | - | - |
| `dev-vX.Y.Z` | dev | `ghcr.io/<owner>/haico-*:dev` + `:X.Y.Z` | dev (auto) |
| `vX.Y.Z` | prod | `ghcr.io/<owner>/haico-*:latest` + `:X.Y.Z` | production (gateable behind a required reviewer) |

Tags must be strict SemVer; anything else is rejected by the pipeline's `resolve-release` job. The
GitHub Environment name matches the `deployment/<env>/` directory name one-to-one.

> [!NOTE]
>
> Promotion to `main` is a **local** `git merge --ff-only` + push, not the GitHub merge button. On a
> free private repo "Require linear history" cannot be enforced, and a UI merge would add a merge
> commit, breaking the byte-for-byte equality between the `dev-vX.Y.Z` and `vX.Y.Z` images. See
> [CONTRIBUTING.md](https://github.com/petrosrapto/HAICO/blob/main/CONTRIBUTING.md) and the [Maintainers handbook](/docs/maintainers-handbook.md).

## The CI/CD pipeline

One workflow, [`.github/workflows/ci.yml`](https://github.com/petrosrapto/HAICO/blob/main/.github/workflows/ci.yml), drives everything. On a SemVer tag push it runs:

```mermaid theme={null}
flowchart TD
    A["lint + tests<br/>(frontend build/test, backend pytest)"] --> B["ci-complete gate"]
    B --> C["resolve-release<br/>classify tag into environment / version / channel"]
    C --> D["build-and-push<br/>backend + frontend images to GHCR<br/>(scoped to the dev/prod GitHub Environment)"]
    D --> E["deploy<br/>OpenVPN + SSH to the app host, render .env, compose pull + up, assert the running build matches the tag"]
    E --> G["release-summary"]
    E --> H["announce-release<br/>Discord post from frontend/src/data/whats-new.json (only when a webhook secret is set)"]
```

The **deploy** job connects to the target host over OpenVPN, SSHes in, renders the `root` / `backend`
/ `frontend` `.env` files by name-matching each key against the environment's GitHub secrets, copies
the `deployment/<env>/` tree over, does a `docker login ghcr.io`, and runs the host's `scripts/deploy.sh`
(which pulls the pinned images, runs `docker compose up -d`, and health-checks Postgres, the backend,
and the frontend). The frontend image bakes the per-environment `NEXT_PUBLIC_*` and `API_PROXY_TARGET`
build args, so it is built once per channel. Both images are also stamped with four build args computed
from the tag, `APP_VERSION`, `APP_CHANNEL`, `APP_COMMIT` and `APP_BUILT_AT`, which surface at `GET /api/version`,
in the site footer's version chip, and in the navbar's `DEV` chip on the dev channel.

## Runtime topology

A single reverse-proxy host runs nginx (on the host, not in a container) and fronts both
environments, terminating TLS and routing by domain to each app host's Compose stack:

```ascii theme={null}
                         ┌──────────────────────────────┐
        haico.gr ───────▶│  reverse proxy (nginx, TLS)  │
   dev.haico.gr ───────▶ │  Let's Encrypt, rate limits  │
                         └───────────────┬──────────────┘
                          ┌──────────────┴───────────────┐
                          ▼                              ▼
                 ┌──────────────────┐           ┌──────────────────┐
                 │  prod app host   │           │  dev app host    │
                 │  docker compose: │           │  docker compose: │
                 │  frontend :3000  │           │  frontend :3000  │
                 │  backend  :8000  │           │  backend  :8000  │
                 │  postgres :5432  │           │  postgres :5432  │
                 │  phoenix  :6006  │           │  phoenix  :6006  │
                 └──────────────────┘           └──────────────────┘
```

The proxy forwards `/` to the frontend, `/api/` and `/health` to the backend, and `/phoenix/` to
Phoenix. The committed nginx configs are [`deployment/prod/nginx/`](https://github.com/petrosrapto/HAICO/blob/main/deployment/prod/nginx) and [`deployment/dev/nginx/`](https://github.com/petrosrapto/HAICO/blob/main/deployment/dev/nginx); they depend on
host-resident snippets and rate-limit zones that live only on the proxy host.

## Required GitHub Environment secrets

Configure GitHub Environments **`dev`** and **`prod`** (Settings → Environments) with the same secret
**names**; add a required reviewer to `prod` to gate production auto-deploy.

| Group | Keys |
| - | - |
| Infra / deploy | `VPN_PRIVATE_KEY`, `OPENVPN_CONFIG`, `REMOTE_SERVER_IP`, `SSH_USER`, `SSH_PRIVATE_KEY_B64`, `GHCR_PAT` |
| App secrets | `JWT_SECRET`, `POSTGRES_PASSWORD`, `ADMIN_DEFAULT_PASSWORD`, `FRONTEND_URL`, the LLM keys (`OPENAI_API_KEY`, …), `SMTP_*`, `RECAPTCHA_SECRET_KEY`, `GOOGLE_OAUTH_CLIENT_ID`, `LANGCHAIN_*` (the LangSmith key and target, used only when the YAML enables LangSmith), and (optional) `GITHUB_TOKEN` for the admin "create issue from report" action (blank disables it; the repo and labels live in `config.*.yaml` under `github:`) |
| Phoenix | `PHOENIX_SECRET` (must be **≥ 32 chars** or Phoenix crash-loops), `PHOENIX_API_KEY`, `PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD` |
| Build-time vars (public) | `API_PROXY_TARGET`, `NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID`, `NEXT_PUBLIC_RECAPTCHA_SITE_KEY` |
| Build stamp (computed by CI, nothing to configure) | `APP_VERSION`, `APP_CHANNEL`, `APP_COMMIT`, `APP_BUILT_AT`: set by `build-and-push` from the tag and baked into both images |
| Repository secrets (optional) | `DISCORD_RELEASE_WEBHOOK_URL`, `DISCORD_DEV_WEBHOOK_URL`: Discord channel webhooks for the `announce-release` job (Settings → Secrets and variables → Actions, **repository** level, not per Environment); unset means no post |

The image refs (`BACKEND_IMAGE` / `FRONTEND_IMAGE`) are not secrets; the deploy job computes them and
pins the exact `X.Y.Z` for reproducibility.

> [!WARNING]
>
> **Phoenix tracing.** With auth on, the OTLP collector is protected too, so the backend needs a Phoenix
> System API key in `PHOENIX_API_KEY`, or trace exports fail with `UNAUTHENTICATED`. And `PHOENIX_SECRET`
> shorter than 32 characters crash-loops the container.

## Cutting a release

**1. Dev**

```bash theme={null}
git checkout develop && git pull
git tag dev-v0.3.1 && git push origin dev-v0.3.1
```

Or comment `/promote dev` on a PR: [`promote-dev.yml`](https://github.com/petrosrapto/HAICO/blob/main/.github/workflows/promote-dev.yml) auto-bumps and pushes the next
`dev-vX.Y.Z` onto the PR head (it uses a PAT so the tag triggers CI).

**2. Production**

```bash theme={null}
git checkout main && git pull
git merge --ff-only origin/develop && git push origin main
git tag v0.3.1 && git push origin v0.3.1
```

If the release changes anything users will notice, make sure
`frontend/src/data/whats-new.json` has an entry for it before you tag (see the
[Maintainers handbook](/docs/maintainers-handbook.md)); once the deploy succeeds, CI posts the
Discord announcement from that entry.

**Rollback:** tags are immutable, so re-tag a new SemVer at the previous good commit and let CI
redeploy, or SSH to the host and pin `BACKEND_IMAGE` / `FRONTEND_IMAGE` in `~/haico_deploy/.env` then
run `scripts/deploy.sh`. Migrations are forward-only, so snapshot the database before a migration
release.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.