diff --git a/.gitea/workflows/ci-cd-nas.yml b/.gitea/workflows/ci-cd-nas.yml deleted file mode 100644 index 1f45d6a..0000000 --- a/.gitea/workflows/ci-cd-nas.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: ci-cd-nas -on: - push: - branches: [main] - -jobs: - # Runs on the DEV PC runner (label `builder`): Docker Desktop + .NET SDK. - # DS220+ (Celeron J4025 / 2GB RAM) cannot build these images, so all the heavy - # work (test, dotnet publish, ng build) happens here, then images are pushed - # to the Gitea registry on the NAS. - build-push: - # Label is registered on the dev PC as `windows:host`; runs-on matches the - # label NAME (before the colon). `:host` means it runs directly on the PC, - # using its installed Docker Desktop + .NET SDK (no container). - runs-on: windows - defaults: - run: - # Windows PowerShell (always present). NOTE: do NOT use `shell: bash` - # here — act_runner in Windows host mode mislocates the generated .sh - # script ("No such file or directory"). PowerShell avoids that bug. - shell: powershell - env: - REGISTRY: git.golife.love/chrischen - steps: - - uses: actions/checkout@v4 - - - name: Test API - run: dotnet test API/ROLAC.API.Tests/ROLAC.API.Tests.csproj -c Release - - - name: Registry login - run: '"${{ secrets.REGISTRY_TOKEN }}" | docker login git.golife.love -u "${{ secrets.REGISTRY_USER }}" --password-stdin' - - - name: Build images - run: | - docker build -t "$env:REGISTRY/rolac-api:latest" -t "$env:REGISTRY/rolac-api:${{ github.sha }}" ./API - if ($LASTEXITCODE -ne 0) { exit 1 } - docker build -t "$env:REGISTRY/rolac-app:latest" -t "$env:REGISTRY/rolac-app:${{ github.sha }}" ./APP - if ($LASTEXITCODE -ne 0) { exit 1 } - - - name: Push images - run: | - docker push --all-tags "$env:REGISTRY/rolac-api" - if ($LASTEXITCODE -ne 0) { exit 1 } - docker push --all-tags "$env:REGISTRY/rolac-app" - if ($LASTEXITCODE -ne 0) { exit 1 } - - # Runs on the NAS runner (label `nas`): host Docker socket mounted and - # /volume1/docker/rolac bind-mounted at the same path. Deploy ONLY — it just - # pulls the freshly-built images and (re)starts the stack. No building here. - deploy: - needs: build-push - runs-on: nas - defaults: - run: - shell: sh - env: - DEPLOY_DIR: /volume1/docker/rolac - steps: - - uses: actions/checkout@v4 - - - name: Registry login - run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.golife.love -u "${{ secrets.REGISTRY_USER }}" --password-stdin - - - name: Sync compose + nginx to deploy dir - run: | - mkdir -p "$DEPLOY_DIR/nginx/conf.d" "$DEPLOY_DIR/data/api-storage" - cp deploy/nas/docker-compose.yml "$DEPLOY_DIR/docker-compose.yml" - cp deploy/nas/nginx/conf.d/rolac.conf "$DEPLOY_DIR/nginx/conf.d/rolac.conf" - - - name: Deploy - run: | - cd "$DEPLOY_DIR" - export TAG=${{ github.sha }} - docker compose pull - docker compose up -d - sleep 5 - curl -fsS http://localhost:8080/api/health diff --git a/.gitea/workflows/ci-cd-vm.yml b/.gitea/workflows/ci-cd-vm.yml new file mode 100644 index 0000000..9513bfc --- /dev/null +++ b/.gitea/workflows/ci-cd-vm.yml @@ -0,0 +1,50 @@ +name: ci-cd-vm +on: + push: + branches: [main] + +# Everything lives on the same Ubuntu VM (Gitea, the registry, the build, and the +# runtime share one Docker daemon), so a single job on the `ubuntu` runner does +# test -> build -> push -> deploy. No cross-machine pull is needed; deploy reuses +# the images just built in the local Docker. +jobs: + ci-cd: + runs-on: ubuntu + defaults: + run: + shell: bash + env: + REGISTRY: git.golife.love/chrischen + DEPLOY_DIR: /opt/rolac + steps: + - uses: actions/checkout@v4 + + - name: Test API + run: dotnet test API/ROLAC.API.Tests/ROLAC.API.Tests.csproj -c Release + + - name: Registry login + run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.golife.love -u "${{ secrets.REGISTRY_USER }}" --password-stdin + + - name: Build images + run: | + docker build -t "$REGISTRY/rolac-api:latest" -t "$REGISTRY/rolac-api:${{ github.sha }}" ./API + docker build -t "$REGISTRY/rolac-app:latest" -t "$REGISTRY/rolac-app:${{ github.sha }}" ./APP + + - name: Push images + run: | + docker push --all-tags "$REGISTRY/rolac-api" + docker push --all-tags "$REGISTRY/rolac-app" + + - name: Sync compose + nginx to deploy dir + run: | + mkdir -p "$DEPLOY_DIR/nginx/conf.d" "$DEPLOY_DIR/data/api-storage" + cp deploy/vm/docker-compose.yml "$DEPLOY_DIR/docker-compose.yml" + cp deploy/vm/nginx/conf.d/rolac.conf "$DEPLOY_DIR/nginx/conf.d/rolac.conf" + + - name: Deploy + run: | + cd "$DEPLOY_DIR" + export TAG=${{ github.sha }} + docker compose up -d + sleep 5 + curl -fsS http://localhost:8080/api/health diff --git a/deploy/nas/README.md b/deploy/nas/README.md deleted file mode 100644 index b3da683..0000000 --- a/deploy/nas/README.md +++ /dev/null @@ -1,142 +0,0 @@ -# Deploy to Synology NAS (Container Manager) — LAN / HTTP - -Target: run the ROLAC stack on a Synology **DS220+** (Celeron J4025 / 2GB RAM), -reachable on the LAN at `http://:8080`. Images are **built on the dev PC** -(the NAS is too weak to compile — Angular's build alone can need >2GB RAM), pushed to -the **Gitea registry** on the NAS, and the NAS only **pulls + restarts** the containers. - -``` - push main - dev PC ───────────────► Gitea (NAS) - (runner: builder) │ triggers .gitea/workflows/ci-cd-nas.yml - test + build + push ─────────┤ - ▼ - NAS runner (label: nas) ── deploy only ──┐ - ▼ -browser (LAN) -> http://:8080 - │ nginx edge (container, 8080->80) - ├── / -> app container (Angular static) - └── /api/ -> api container (ASP.NET, :8080) -api ──> existing PostgreSQL @ 192.168.68.55:49154 (not containerized) -``` - -Why this split: DS220+ can comfortably **run** these lightweight containers -(nginx + precompiled .NET + static files) but cannot **build** them. So building -(test, `dotnet publish`, `ng build`) runs on the dev PC; the NAS just pulls. - -Differences vs the Azure plan: no TLS/certbot, edge on **8080** (DSM owns 80/443), -reuse the LAN database, deploy via the on-NAS runner (no SSH). - ---- - -## Two runners, two jobs - -| Job | `runs-on` | Where | Does | -|-----|-----------|-------|------| -| `build-push` | `windows` | **dev PC** | test → build both images → push to registry | -| `deploy` | `nas` | **NAS** | pull images → `docker compose up -d` → health check | - -> The dev-PC runner is registered with the label `windows:host` — `runs-on` matches -> the label NAME (`windows`); `:host` is the run mode (executes directly on the PC, -> not in a container, so it uses Docker Desktop + the installed .NET SDK). - -`deploy` has `needs: build-push`, so it only runs after the build succeeds. - ---- - -## One-time setup — DEV PC (the `windows` runner) ✅ already done - -The dev PC runs act_runner natively with the label `windows:host`, using its -installed Docker Desktop + .NET 8 SDK. The workflow's `build-push` job targets -`runs-on: windows`. Requirements (for reference): - -- Docker Desktop running, and `docker` on PATH. -- .NET 8 SDK on PATH (`dotnet test` runs on this machine). -- The `build-push` job uses `shell: powershell`. Do **not** switch it to - `shell: bash`: act_runner in Windows host mode mislocates the generated `.sh` - script (`/bin/bash: .../1.sh: No such file or directory`). PowerShell avoids it. - -## One-time setup — NAS (the `nas` runner) - -1. **Deploy dir + secrets** (via SSH or File Station): - ```bash - mkdir -p /volume1/docker/rolac/nginx/conf.d /volume1/docker/rolac/data/api-storage - cp /path/to/repo/deploy/nas/.env.example /volume1/docker/rolac/.env - # edit /volume1/docker/rolac/.env -> real DB user/password + JWT_SECRET + APP_ORIGIN - ``` - -2. **Registry token** — in Gitea: Settings → Applications → new token with - `read:package` + `write:package`. Log the NAS Docker in once: - ```bash - docker login git.golife.love -u ChrisChen # paste the token - ``` - -3. **Install act_runner on the NAS** (Container Manager → Registry → `gitea/act_runner`, - or `docker run`). It must: - - mount the host Docker socket: `-v /var/run/docker.sock:/var/run/docker.sock` - - mount the deploy dir at the same path: `-v /volume1/docker/rolac:/volume1/docker/rolac` - - register with the label **`nas`** (this is what `runs-on: nas` targets). - ```bash - docker run -d --restart unless-stopped --name rolac-runner \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v /volume1/docker/rolac:/volume1/docker/rolac \ - -e GITEA_INSTANCE_URL=https://git.golife.love \ - -e GITEA_RUNNER_REGISTRATION_TOKEN= \ - -e GITEA_RUNNER_LABELS=nas \ - gitea/act_runner:latest - ``` - -## One-time setup — Gitea repo - -1. **Secrets** (Settings → Actions → Secrets): - - `REGISTRY_USER` = `ChrisChen` - - `REGISTRY_TOKEN` = the package token (with `write:package`) -2. **Enable Actions** for the repo if not already (Settings → Advanced → Actions). - ---- - -## Day-to-day - -`git push` to `main` → `.gitea/workflows/ci-cd-nas.yml`: - -1. **dev PC** (`builder`): `dotnet test` → build `rolac-api` + `rolac-app` - (tags `:latest` and `:`) → push to `git.golife.love/chrischen/*`. -2. **NAS** (`nas`): sync compose/nginx → `TAG= docker compose pull` → - `docker compose up -d` → `curl /api/health`. - -Open `http://:8080` and log in. - -Deploy pins `TAG=` (not `latest`), so the NAS always runs exactly the image -this commit produced and `compose pull` forces a fresh fetch. - ---- - -## Manual fallback (no runners yet) - -From the dev PC (Docker Desktop + `docker login git.golife.love`): -```powershell -# repo root — build + push both images (tags :latest and :) -.\deploy\build-push.ps1 -``` -Then on the NAS: -```bash -cd /volume1/docker/rolac -docker compose pull -docker compose up -d -curl -fsS http://localhost:8080/api/health -``` - ---- - -## Notes - -- **First boot runs DB migrations** against `192.168.68.55` automatically - (`Program.cs` calls `MigrateAsync()` + seed). Make sure the DB user has DDL rights; - back up before the first run. -- **Bind-mount paths**: the NAS runner runs compose at `/volume1/docker/rolac` on the - host (socket-mounted), so `./nginx/conf.d` and `./data` resolve to real NAS paths — - that's why the runner mounts that dir at the *same* path. -- **Uploaded files** persist under `/volume1/docker/rolac/data/api-storage`. -- **DS220+ runs, never builds.** Keep all compilation on the dev PC / a beefier runner. -- To expose beyond the LAN later, put it behind DSM's reverse proxy (Application Portal) - or switch to the Azure `deploy/` files with certbot. diff --git a/deploy/vm/README.md b/deploy/vm/README.md new file mode 100644 index 0000000..0a1bebc --- /dev/null +++ b/deploy/vm/README.md @@ -0,0 +1,114 @@ +# Deploy to the Ubuntu VM (all-in-one) — LAN / HTTP + +Everything runs on **one Ubuntu VM, one Docker daemon**: Gitea, the container +registry, the build, and the ROLAC runtime. So a single Gitea Actions job (the +`ubuntu` runner) does the whole pipeline — no cross-machine pull, no Windows-runner +quirks. + +``` +git push main + │ + ▼ +Gitea (on the VM) ── triggers .gitea/workflows/ci-cd-vm.yml + │ + ▼ +ubuntu runner (on the VM, same Docker daemon) + dotnet test + docker build ./API + ./APP -> :latest + : + docker push -> git.golife.love/chrischen/rolac-{api,app} + docker compose up -d (TAG=) + curl /api/health + │ + ▼ +browser -> http://:8080 + │ nginx edge (container, 8080->80) + ├── / -> app container (Angular static) + └── /api/ -> api container (ASP.NET, :8080) +api ──> existing PostgreSQL @ 192.168.68.55:49154 (not containerized) +``` + +No TLS yet — plain HTTP on port **8080**. Add Let's Encrypt later (see the Azure +`deploy/` files) or front it with an existing reverse proxy. + +--- + +## One-time setup — on the VM + +1. **Deploy dir + secrets:** + ```bash + sudo mkdir -p /opt/rolac/nginx/conf.d /opt/rolac/data/api-storage + sudo cp /path/to/repo/deploy/vm/.env.example /opt/rolac/.env + sudo nano /opt/rolac/.env # real DB user/password + JWT_SECRET + APP_ORIGIN + ``` + Make sure the user the runner executes as can read/write `/opt/rolac`. + +2. **Registry token** — in Gitea: Settings → Applications → new token with + `read:package` + `write:package`. Log Docker in once on the VM: + ```bash + docker login git.golife.love -u ChrisChen # paste the token + ``` + +3. **Install act_runner on the VM** with the label **`ubuntu`** and access to the + host Docker. The runner must be able to run `dotnet`, `docker`, and + `docker compose`, and reach `/opt/rolac`: + ```bash + docker run -d --restart unless-stopped --name rolac-runner \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /opt/rolac:/opt/rolac \ + -e GITEA_INSTANCE_URL=https://git.golife.love \ + -e GITEA_RUNNER_REGISTRATION_TOKEN= \ + -e GITEA_RUNNER_LABELS=ubuntu \ + gitea/act_runner:latest + ``` + > The job calls `dotnet test` and `docker build` directly. If act_runner runs in + > a container, that image needs the .NET 8 SDK + docker CLI on PATH. Simplest: + > install act_runner as a **native binary** on the VM (it then uses the host's + > Docker + an installed .NET SDK). Either way the label must be `ubuntu`. + +4. **Gitea repo secrets** (Settings → Actions → Secrets): + - `REGISTRY_USER` = `ChrisChen` + - `REGISTRY_TOKEN` = the package token from step 2 + +5. **Enable Actions** for the repo if needed (Settings → Advanced → Actions). + +--- + +## Day-to-day + +`git push` to `main` → `.gitea/workflows/ci-cd-vm.yml`: +**test → build both images → push to registry → sync compose/nginx → `compose up -d` → health check.** + +Open `http://:8080` and log in. + +Deploy pins `TAG=`, so the running containers match exactly the commit that +was built (the images already exist in the local Docker, so this is instant). + +--- + +## Manual deploy (no runner yet) + +On the VM, from a checkout of the repo: +```bash +docker login git.golife.love -u ChrisChen +docker build -t git.golife.love/chrischen/rolac-api:latest ./API +docker build -t git.golife.love/chrischen/rolac-app:latest ./APP +mkdir -p /opt/rolac/nginx/conf.d /opt/rolac/data/api-storage +cp deploy/vm/docker-compose.yml /opt/rolac/docker-compose.yml +cp deploy/vm/nginx/conf.d/rolac.conf /opt/rolac/nginx/conf.d/rolac.conf +cd /opt/rolac && docker compose up -d +curl -fsS http://localhost:8080/api/health +``` + +--- + +## Notes + +- **First boot runs DB migrations** against `192.168.68.55` automatically + (`Program.cs` calls `MigrateAsync()` + seed). The VM must reach that host and the + DB user needs DDL rights; back up before the first run. +- **Uploaded files** persist under `/opt/rolac/data/api-storage`. +- **Same Docker daemon for build + run** means `docker compose up` finds the freshly + built `:` images locally; `docker compose pull` is unnecessary here (but + harmless if you add it). +- To go HTTPS later: switch the edge to ports 80/443 and mount Let's Encrypt certs, + or use the Azure `deploy/` files which already include certbot. diff --git a/deploy/nas/docker-compose.yml b/deploy/vm/docker-compose.yml similarity index 83% rename from deploy/nas/docker-compose.yml rename to deploy/vm/docker-compose.yml index 80d2f4f..2fda9f3 100644 --- a/deploy/nas/docker-compose.yml +++ b/deploy/vm/docker-compose.yml @@ -7,7 +7,7 @@ services: ConnectionStrings__DefaultConnection: ${DB_CONNECTION} Jwt__SecretKey: ${JWT_SECRET} # Same-origin /api means CORS is not triggered by the browser; this is only - # a safety net for direct cross-origin calls / tools. Set to your NAS URL. + # a safety net for direct cross-origin calls / tools. Set to your VM URL. Cors__AllowedOrigins__0: ${APP_ORIGIN:-http://localhost:8080} volumes: - ./data/api-storage:/app/App_Data/storage @@ -21,7 +21,8 @@ services: nginx: image: nginx:alpine - # DSM already uses 80/443, so the edge is published on 8080 (HTTP, LAN only). + # HTTP only on a high port for now (TLS to be added later). Reach the app at + # http://:8080 on the LAN / wherever the VM is reachable. ports: ["8080:80"] volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro diff --git a/deploy/nas/nginx/conf.d/rolac.conf b/deploy/vm/nginx/conf.d/rolac.conf similarity index 100% rename from deploy/nas/nginx/conf.d/rolac.conf rename to deploy/vm/nginx/conf.d/rolac.conf diff --git a/deploy/vm/runner/Dockerfile b/deploy/vm/runner/Dockerfile new file mode 100644 index 0000000..2bea055 --- /dev/null +++ b/deploy/vm/runner/Dockerfile @@ -0,0 +1,34 @@ +# Custom Gitea act_runner image for the ROLAC pipeline. +# +# The workflow needs BOTH the .NET SDK (dotnet test) and the Docker CLI +# (docker build / push / compose) in the same execution environment. The stock +# gitea/act_runner image has neither, so we bake them on top of the .NET 8 SDK +# image and copy the act_runner binary in. Registered as label `ubuntu:host`, +# every step runs inside THIS container, which talks to the host Docker daemon +# via the mounted socket. +FROM mcr.microsoft.com/dotnet/sdk:8.0 + +# Docker CLI + compose plugin, Node.js (JS-based actions like checkout need it), +# git, curl, bash. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl gnupg git bash \ + && install -m 0755 -d /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \ + && chmod a+r /etc/apt/keyrings/docker.asc \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" \ + > /etc/apt/sources.list.d/docker.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends docker-ce-cli docker-compose-plugin \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +# act_runner binary from the official image. +COPY --from=gitea/act_runner:latest /usr/local/bin/act_runner /usr/local/bin/act_runner + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# .runner registration state persists here (mount a volume). +WORKDIR /data +ENTRYPOINT ["/entrypoint.sh"] diff --git a/deploy/vm/runner/README.md b/deploy/vm/runner/README.md new file mode 100644 index 0000000..c019c6f --- /dev/null +++ b/deploy/vm/runner/README.md @@ -0,0 +1,45 @@ +# Gitea act_runner on the VM (Docker Compose) + +Runs the CI/CD runner as a container, but with a **custom image** that bundles the +.NET 8 SDK + Docker CLI + Node, because the ROLAC workflow does both `dotnet test` +and `docker build`/`compose`. The stock `gitea/act_runner` image has neither. + +It registers with the label **`ubuntu:host`**: +- `ubuntu` = the label name the workflow targets (`runs-on: ubuntu`). +- `:host` = run each step **inside this runner container** (which has the tools), + instead of spawning a separate job container that wouldn't have dotnet/docker. + +The container mounts the **host Docker socket** (so build/push/compose act on the +host daemon) and **`/opt/rolac`** at the same path (so compose's relative volumes +resolve), and uses **host networking** (so the deploy step's +`curl http://localhost:8080/api/health` works). + +## Setup + +1. **Get a runner registration token** in Gitea: + Settings → Actions → Runners → **Create new runner** → copy the token. + (This is the *registration* token — different from the `REGISTRY_TOKEN` repo + secret used for `docker login`.) + +2. **Configure + start** (on the VM, from this directory): + ```bash + cd deploy/vm/runner + cp .env.example .env + nano .env # paste GITEA_RUNNER_REGISTRATION_TOKEN + docker compose up -d --build + ``` + +3. **Verify** it shows up online in Gitea → Settings → Actions → Runners, with the + `ubuntu` label. + +## Notes + +- Registration state is stored in `./runner-data/.runner` (a bind mount), so the + runner does **not** re-register on restart. To re-register from scratch, stop the + container and delete `runner-data/`. +- `docker login git.golife.love` for the registry is done by the **workflow** using + the repo secrets `REGISTRY_USER` / `REGISTRY_TOKEN` — you do not need to log in + inside the runner manually. +- Logs: `docker compose logs -f runner`. +- The runner can build/run containers on the host because it shares the host Docker + socket. Treat this runner as privileged — only run trusted workflows on it. diff --git a/deploy/vm/runner/docker-compose.yml b/deploy/vm/runner/docker-compose.yml new file mode 100644 index 0000000..94ef7fc --- /dev/null +++ b/deploy/vm/runner/docker-compose.yml @@ -0,0 +1,17 @@ +services: + runner: + build: . + image: rolac-act-runner:latest + restart: unless-stopped + # host networking so the deploy step's `curl http://localhost:8080/api/health` + # reaches the published edge port on the host. + network_mode: host + env_file: .env + volumes: + # talk to the host Docker daemon (build/push/compose all run on the host) + - /var/run/docker.sock:/var/run/docker.sock + # deploy target — must be the SAME path so compose's relative ./data and + # ./nginx volumes resolve to real host paths + - /opt/rolac:/opt/rolac + # persist runner registration so it doesn't re-register on restart + - ./runner-data:/data diff --git a/deploy/vm/runner/entrypoint.sh b/deploy/vm/runner/entrypoint.sh new file mode 100644 index 0000000..0784404 --- /dev/null +++ b/deploy/vm/runner/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -e + +# Register once (state stored in /data/.runner, which is a mounted volume so it +# survives restarts). On later starts it just runs the daemon. +if [ ! -f /data/.runner ]; then + echo "Registering runner with ${GITEA_INSTANCE_URL} ..." + act_runner register --no-interactive \ + --instance "${GITEA_INSTANCE_URL}" \ + --token "${GITEA_RUNNER_REGISTRATION_TOKEN}" \ + --name "${GITEA_RUNNER_NAME:-vm-runner}" \ + --labels "${GITEA_RUNNER_LABELS:-ubuntu:host}" +fi + +exec act_runner daemon