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://<nas-ip>: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://<nas-ip>: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-onmatches the label NAME (windows);:hostis 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
dockeron PATH. - .NET 8 SDK on PATH (
dotnet testruns on this machine). - Git for Windows installed — the job uses
shell: bash(Git Bash) for the multi-linedocker build/pushsteps.
One-time setup — NAS (the nas runner)
-
Deploy dir + secrets (via SSH or File Station):
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 -
Registry token — in Gitea: Settings → Applications → new token with
read:package+write:package. Log the NAS Docker in once:docker login git.golife.love -u ChrisChen # paste the token -
Install act_runner on the NAS (Container Manager → Registry →
gitea/act_runner, ordocker 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 whatruns-on: nastargets).
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=<token> \ -e GITEA_RUNNER_LABELS=nas \ gitea/act_runner:latest - mount the host Docker socket:
One-time setup — Gitea repo
- Secrets (Settings → Actions → Secrets):
REGISTRY_USER=ChrisChenREGISTRY_TOKEN= the package token (withwrite:package)
- Enable Actions for the repo if not already (Settings → Advanced → Actions).
Day-to-day
git push to main → .gitea/workflows/ci-cd-nas.yml:
- dev PC (
builder):dotnet test→ buildrolac-api+rolac-app(tags:latestand:<git-sha>) → push togit.golife.love/chrischen/*. - NAS (
nas): sync compose/nginx →TAG=<git-sha> docker compose pull→docker compose up -d→curl /api/health.
Open http://<nas-ip>:8080 and log in.
Deploy pins TAG=<git-sha> (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):
# repo root — build + push both images (tags :latest and :<git-sha>)
.\deploy\build-push.ps1
Then on the NAS:
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.55automatically (Program.cscallsMigrateAsync()+ 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/rolacon the host (socket-mounted), so./nginx/conf.dand./dataresolve 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.