# Guide 26: VigilCareRecords CI/CD (Gitea Actions + Docker Compose) How VigilCareRecords is built, tested, and deployed on the same Gitea Actions + `act_runner` + container registry setup already used by VigilCareClinical. See [`docs/25-gitea-cicd-docker-deploy.md`](25-gitea-cicd-docker-deploy.md) for the general pattern this guide instantiates; this doc only covers what is specific to this repo. Related files: - [`.gitea/workflows/ci.yml`](../.gitea/workflows/ci.yml) / [`.gitea/workflows/cd.yml`](../.gitea/workflows/cd.yml) - [`docker-compose.yml`](../docker-compose.yml) (local + CI dependencies) - [`docker-compose.prod.yml`](../docker-compose.prod.yml) (production app stack) - [`.env.example`](../.env.example) - [`VigilCareRecordsAPI/Dockerfile`](../VigilCareRecordsAPI/Dockerfile) / [`vigilcare-records-web/Dockerfile`](../vigilcare-records-web/Dockerfile) --- ## 1. Topology VigilCareRecords ships two images: | Image | Dockerfile | Build context | |---|---|---| | `vigilcare-records-api` | `VigilCareRecordsAPI/Dockerfile` | repo root | | `vigilcare-records-dashboard` | `vigilcare-records-web/Dockerfile` | `vigilcare-records-web/` | Unlike VigilCareClinical, there is no gateway service, and the frontend does not bake in an absolute API URL at build time — `src/api/client.ts` and `src/api/fhirClient.ts` use relative base URLs (`/api/v1`, `/fhir`). The dashboard's `nginx.conf` reverse-proxies those paths to the `api` container, so the same image works behind any hostname without a build-arg. ## 2. Shared production infrastructure Per the "Integrated Database Deployment" decision documented in [`vigilcare-records-clinical-overview.md`](vigilcare-records-clinical-overview.md), VigilCareRecords does **not** run its own Postgres/Redis/MinIO/Seq in production. It joins the external `shared-services` Docker network already created by VigilCareClinical's own compose project and connects to those same containers, using: - Its own logical Postgres database (`vigilcare_records`, separate schema/tables from VigilCareClinical's `patients`/`encounters`/`observations` — see the promotion service for how the two connect at the application layer, not the infrastructure layer) - A dedicated Redis logical database (`defaultDatabase=2` in `.env.example`) so batch-assignment locks never collide with VigilCareClinical's keys - A dedicated MinIO bucket (`vigilcare-records-scans`) separate from any clinical document buckets **Before the first deploy**, confirm the VigilCareClinical infra stack (or whatever compose project owns `shared-services`) is already running on the deploy host — `docker network ls | grep shared-services` should show it. `docker compose -f docker-compose.prod.yml up` will fail to find the network otherwise. ## 3. One-time host setup ```bash ssh deploy@YOUR_HOST "mkdir -p /opt/vigilcare-records" scp .env deploy@YOUR_HOST:/opt/vigilcare-records/.env ssh deploy@YOUR_HOST "chmod 600 /opt/vigilcare-records/.env" ``` Fill in `.env` from [`.env.example`](../.env.example) first — generate `JWT_SECRET` with `openssl rand -base64 48`, and get real credentials for the shared Postgres/Redis/MinIO/Seq services from whoever manages that stack. CD never uploads or overwrites `.env`; only the `IMAGE_TAG` line is patched automatically on each release. Create the Records MinIO bucket once (the API also creates it on startup if missing): ```bash ssh deploy@YOUR_HOST bash -euo pipefail <<'EOF' cd /opt/vigilcare-records env_val() { sed -n "s/^${1}=//p" .env | tail -n1 | tr -d '\r'; } ACCESS="$(env_val MINIO_ACCESS_KEY)" SECRET="$(env_val MINIO_SECRET_KEY)" BUCKET="$(env_val MINIO_BUCKET_NAME)"; BUCKET="${BUCKET:-vigilcare-records-scans}" docker run --rm --network shared-services --entrypoint /bin/sh minio/mc \ -c "mc alias set local http://minio:9000 '${ACCESS}' '${SECRET}' && mc mb --ignore-existing local/${BUCKET}" EOF ``` ## 4. Gitea secrets and variables Repo → **Settings** → **Actions**. ### Secrets | Secret | Used by | |---|---| | `REGISTRY_USERNAME` | `docker login` | | `REGISTRY_TOKEN` | `docker login` (access token / PAT with package write) | | `PG_CONNECTION_DDL` | migrate job only — DDL-privileged connection (`Host=postgres` on `shared-services`), never given to the API container | | `DEPLOY_HOST` | SSH / SCP | | `DEPLOY_USER` | SSH / SCP | | `DEPLOY_SSH_KEY` | Private key PEM / OpenSSH private key body | ### Variables | Variable | Used by | |---|---| | `REGISTRY` | Optional override of the default `git.vectur45.com/trent/vigilcare-records` | ## 5. Release flow ```bash # CI green on master, then: git tag v1.0.0 git push origin v1.0.0 ``` This triggers `.gitea/workflows/cd.yml`: build & push both images → SSH the EF migrations bundle onto the deploy host and run it on `shared-services` with `PG_CONNECTION_DDL` → SSH deploy (`scp` the prod compose file, patch `IMAGE_TAG`, `pull` + `up -d`) → smoke test (`/health/ready`, dashboard `/`) → automatic rollback to the previous `IMAGE_TAG` on failure (schema changes are not reverted; see the expand/contract note in `cd.yml`). Manual redeploy of an existing tag: Gitea UI → Actions → CD → Run workflow, with `image_tag` input. ## 6. Pre-production checklist (application-level, not part of this CI/CD change) - **Demo data seeding runs unconditionally on startup.** `Program.cs` calls `DataSeeder.SeedAsync(db)` whenever `ASPNETCORE_ENVIRONMENT` is not `Testing` — including `Production`. Unlike the VigilCareClinical example (`Seeding__EnableDemoData=false`), this app has no seeding toggle. Before a real clinical deploy, either add a guard around `DataSeeder.SeedAsync` for `Production`, or confirm the twelve seeded demo accounts (all password `password`) are acceptable/rotated before go-live. - Confirm `Cors:AllowedOrigins` / `DASHBOARD_ORIGIN` matches the real public dashboard origin if the dashboard is ever served from a different origin than the API (not the default same-origin nginx-proxy setup described above). - Rotate `JWT_SECRET` from any value used during testing before the first real deploy.