wip
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
# build-push.ps1 — Build both ROLAC images and push them to the Gitea registry.
|
||||
#
|
||||
# Usage (from anywhere):
|
||||
# .\deploy\build-push.ps1 # tags :latest and :<git-sha>
|
||||
# .\deploy\build-push.ps1 -Tag v1.2.0 # also adds an extra :v1.2.0 tag
|
||||
# .\deploy\build-push.ps1 -NoPush # build only, don't push
|
||||
#
|
||||
# Prereqs:
|
||||
# - Docker Desktop running
|
||||
# - docker login git.golife.love (once, with a write:package access token)
|
||||
|
||||
param(
|
||||
[string]$Tag, # optional extra tag, e.g. a release version
|
||||
[switch]$NoPush # build only
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Repo root = parent of this script's folder
|
||||
$RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$Registry = 'git.golife.love/chrischen'
|
||||
$Api = "$Registry/rolac-api"
|
||||
$App = "$Registry/rolac-app"
|
||||
|
||||
# Short git sha for an immutable version tag
|
||||
$Sha = (git -C $RepoRoot rev-parse --short HEAD).Trim()
|
||||
Write-Host "Building from commit $Sha" -ForegroundColor Cyan
|
||||
|
||||
# Assemble the -t arguments for each image
|
||||
$apiTags = @('-t', "${Api}:latest", '-t', "${Api}:$Sha")
|
||||
$appTags = @('-t', "${App}:latest", '-t', "${App}:$Sha")
|
||||
if ($Tag) {
|
||||
$apiTags += @('-t', "${Api}:$Tag")
|
||||
$appTags += @('-t', "${App}:$Tag")
|
||||
}
|
||||
|
||||
Write-Host "==> Building API image" -ForegroundColor Green
|
||||
docker build @apiTags "$RepoRoot\API"
|
||||
if ($LASTEXITCODE -ne 0) { throw "API build failed" }
|
||||
|
||||
Write-Host "==> Building APP image" -ForegroundColor Green
|
||||
docker build @appTags "$RepoRoot\APP"
|
||||
if ($LASTEXITCODE -ne 0) { throw "APP build failed" }
|
||||
|
||||
if ($NoPush) {
|
||||
Write-Host "Build complete (push skipped)." -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "==> Pushing API image" -ForegroundColor Green
|
||||
docker push --all-tags $Api
|
||||
if ($LASTEXITCODE -ne 0) { throw "API push failed" }
|
||||
|
||||
Write-Host "==> Pushing APP image" -ForegroundColor Green
|
||||
docker push --all-tags $App
|
||||
if ($LASTEXITCODE -ne 0) { throw "APP push failed" }
|
||||
|
||||
Write-Host "Done. Pushed :latest and :$Sha$(if($Tag){" and :$Tag"})." -ForegroundColor Cyan
|
||||
@@ -0,0 +1,31 @@
|
||||
services:
|
||||
api:
|
||||
image: git.golife.love/chrischen/rolac-api:${TAG:-latest}
|
||||
env_file: .env
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
ConnectionStrings__DefaultConnection: ${DB_CONNECTION}
|
||||
Jwt__SecretKey: ${JWT_SECRET}
|
||||
Cors__AllowedOrigins__0: https://app.rolac.org
|
||||
volumes:
|
||||
- api-storage:/app/App_Data/storage
|
||||
restart: unless-stopped
|
||||
expose: ["8080"]
|
||||
|
||||
app:
|
||||
image: git.golife.love/chrischen/rolac-app:${TAG:-latest}
|
||||
restart: unless-stopped
|
||||
expose: ["80"]
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports: ["80:80", "443:443"]
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- /etc/letsencrypt:/etc/letsencrypt:ro
|
||||
- /var/www/certbot:/var/www/certbot:ro
|
||||
depends_on: [api, app]
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
api-storage:
|
||||
@@ -0,0 +1,96 @@
|
||||
# Deploy to Synology NAS (Container Manager) — LAN / HTTP
|
||||
|
||||
Target: run the ROLAC stack on a Synology NAS, reachable on the LAN at
|
||||
`http://<nas-ip>:8080`, with images built & pushed to the **local Gitea registry**
|
||||
(`git.golife.love`, same NAS) and auto-deployed by a **Gitea act_runner** on push to `main`.
|
||||
|
||||
```
|
||||
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)
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
## One-time NAS setup
|
||||
|
||||
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 the 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 against Gitea with the label **`nas`** (this is what `runs-on: nas` targets).
|
||||
|
||||
Get a registration token in Gitea: Site/Repo → Settings → Actions → Runners →
|
||||
"Create new runner". Example:
|
||||
```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=<token> \
|
||||
-e GITEA_RUNNER_LABELS=nas \
|
||||
gitea/act_runner:latest
|
||||
```
|
||||
|
||||
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 not already (Settings → Advanced → Actions).
|
||||
|
||||
---
|
||||
|
||||
## Day-to-day
|
||||
|
||||
`git push` to `main` → `.gitea/workflows/ci-cd-nas.yml` runs:
|
||||
**test → build both images → push to registry → sync compose/nginx → `docker compose up -d` → health check.**
|
||||
|
||||
Open `http://<nas-ip>:8080` and log in.
|
||||
|
||||
---
|
||||
|
||||
## Manual deploy (no runner yet)
|
||||
|
||||
From a machine with Docker + `docker login git.golife.love`:
|
||||
```powershell
|
||||
# repo root, build + push (uses deploy/build-push.ps1)
|
||||
.\deploy\build-push.ps1
|
||||
```
|
||||
Then on the NAS:
|
||||
```bash
|
||||
cd /volume1/docker/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). Make sure the DB user has DDL rights;
|
||||
back up before the first run.
|
||||
- **Bind-mount paths**: the runner deploys by running 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`.
|
||||
- To expose beyond the LAN later, put it behind DSM's reverse proxy (Application Portal)
|
||||
or switch to the Azure `deploy/` files with certbot.
|
||||
@@ -0,0 +1,29 @@
|
||||
services:
|
||||
api:
|
||||
image: git.golife.love/chrischen/rolac-api:${TAG:-latest}
|
||||
env_file: .env
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
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.
|
||||
Cors__AllowedOrigins__0: ${APP_ORIGIN:-http://localhost:8080}
|
||||
volumes:
|
||||
- ./data/api-storage:/app/App_Data/storage
|
||||
restart: unless-stopped
|
||||
expose: ["8080"]
|
||||
|
||||
app:
|
||||
image: git.golife.love/chrischen/rolac-app:${TAG:-latest}
|
||||
restart: unless-stopped
|
||||
expose: ["80"]
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
# DSM already uses 80/443, so the edge is published on 8080 (HTTP, LAN only).
|
||||
ports: ["8080:80"]
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
depends_on: [api, app]
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,18 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# API -> api container. The SPA calls same-origin /api/... (environment.prod.ts).
|
||||
location /api/ {
|
||||
proxy_pass http://api:8080/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Everything else -> the Angular static app (its own nginx does SPA fallback).
|
||||
location / {
|
||||
proxy_pass http://app:80;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
server { # HTTP -> HTTPS
|
||||
listen 80;
|
||||
server_name app.rolac.org api.rolac.org;
|
||||
location /.well-known/acme-challenge/ { root /var/www/certbot; }
|
||||
location / { return 301 https://$host$request_uri; }
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name app.rolac.org;
|
||||
ssl_certificate /etc/letsencrypt/live/app.rolac.org/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/app.rolac.org/privkey.pem;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://api:8080/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
location / {
|
||||
proxy_pass http://app:80;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user