> ## Documentation Index
> Fetch the complete documentation index at: https://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)).

## Release model: two branches, tag-driven

The flow (recorded in [ADR-0001](/docs/adr/0001-two-branch-model)) 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).
</Note>

## 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"]
    E --> G["release-summary"]
```

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.

## 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_*`, 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`                                                                                                                                                                                                                                                                 |

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.
</Warning>

## Cutting a release

<Steps>
  <Step title="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).
  </Step>

  <Step title="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
    ```
  </Step>
</Steps>

**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.
