diff --git a/.gitignore b/.gitignore index e7833fa..af900e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ artifacts/ +.release/ bin/ obj/ *.user diff --git a/.woodpecker/linux-release.yml b/.woodpecker/linux-release.yml new file mode 100644 index 0000000..08424d6 --- /dev/null +++ b/.woodpecker/linux-release.yml @@ -0,0 +1,56 @@ +labels: + ci: domain-linux-native + platform: linux/amd64 + +when: + - event: tag + ref: refs/tags/v* + +depends_on: + - verify + +concurrency: 1 + +steps: + - name: linux-amd64 + image: bash + environment: + NUGET_PACKAGES: /home/justme/.nuget/packages + GITEA_BASE: https://git.oeo.one + GITEA_TOKEN: + from_secret: gitea_token + ZOT_REGISTRY: 192.168.2.162:5000 + ZOT_USERNAME: + from_secret: zot_username + ZOT_PASSWORD: + from_secret: zot_password + commands: + - bash ci/release-linux.sh amd64 + depends_on: [] + + - name: linux-arm64 + image: bash + environment: + NUGET_PACKAGES: /home/justme/.nuget/packages + GITEA_BASE: https://git.oeo.one + GITEA_TOKEN: + from_secret: gitea_token + ZOT_REGISTRY: 192.168.2.162:5000 + ZOT_USERNAME: + from_secret: zot_username + ZOT_PASSWORD: + from_secret: zot_password + commands: + - bash ci/release-linux.sh arm64 + depends_on: [] + + - name: backend-multiarch-index + image: bash + environment: + ZOT_REGISTRY: 192.168.2.162:5000 + ZOT_USERNAME: + from_secret: zot_username + ZOT_PASSWORD: + from_secret: zot_password + commands: + - bash ci/index-backend-image.sh diff --git a/.woodpecker/verify.yml b/.woodpecker/verify.yml new file mode 100644 index 0000000..39b217d --- /dev/null +++ b/.woodpecker/verify.yml @@ -0,0 +1,25 @@ +labels: + ci: domain-linux-native + platform: linux/amd64 + +when: + - event: push + branch: main + - event: pull_request + - event: tag + ref: refs/tags/v* + +steps: + - name: backend-tests + image: bash + environment: + NUGET_PACKAGES: /home/justme/.nuget/packages + commands: + - cd backend && dotnet test --solution Monitor.slnx -c Release + + - name: web-build + image: bash + commands: + - npm ci --prefix web + - npm run build --prefix web + - npm run lint:css --prefix web diff --git a/.woodpecker/windows-release.yml b/.woodpecker/windows-release.yml new file mode 100644 index 0000000..e56916c --- /dev/null +++ b/.woodpecker/windows-release.yml @@ -0,0 +1,35 @@ +labels: + ci: domain-windows + platform: windows/amd64 + +when: + - event: tag + ref: refs/tags/v* + +depends_on: + - verify + +concurrency: 1 + +steps: + - name: windows-amd64 + image: powershell + environment: + GITEA_BASE: https://git.oeo.one + GITEA_TOKEN: + from_secret: gitea_token + commands: + - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force + - ./ci/release-windows.ps1 -Architecture amd64 + depends_on: [] + + - name: windows-arm64 + image: powershell + environment: + GITEA_BASE: https://git.oeo.one + GITEA_TOKEN: + from_secret: gitea_token + commands: + - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force + - ./ci/release-windows.ps1 -Architecture arm64 + depends_on: [] diff --git a/README.md b/README.md index 8ef7dba..c844a83 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ Probe <──测量会话──> Agent ──长连接与数据上报──> Bac ## 快速开始 ### 环境要求 -[README.md](backend/README.md) - .NET SDK 10;项目使用 `backend/global.json` 指定 SDK 版本和滚动策略。 - Node.js 与 npm,用于构建 Web 前端。 @@ -45,6 +44,8 @@ npm run build --prefix web 前端产物位于 `web/display/dist/` 和 `web/admin/dist/`。NativeAOT 发布还需要对应平台的 C/C++ 工具链;Linux 发布脚本和 RID 说明见 [`docs/deployment.md`](docs/deployment.md)。 +Backend 以 OCI 镜像发布,支持 Linux/amd64 与 Linux/arm64;Agent 和 Probe 以 Windows/Linux 的 amd64/arm64 二进制包发布。容器部署配置见 [`compose.yaml`](compose.yaml),发布说明见 [`docs/deployment.md`](docs/deployment.md)。 + ### 本地运行 1. **启动 Backend。** 在仓库根目录运行: diff --git a/backend/README.md b/backend/README.md index 7d3f43f..aae510d 100644 --- a/backend/README.md +++ b/backend/README.md @@ -36,7 +36,9 @@ dotnet test --solution Monitor.slnx dotnet publish src/Monitor.Backend -c Release -r -p:PublishAot=true ``` -例如 `` 可为 `win-x64`、`linux-x64` 或 `linux-musl-x64`。NativeAOT 发布需要对应平台的 C/C++ 工具链。仓库根目录的 `scripts/publish.sh` 也提供发布和容器构建流程,详见 [`../docs/deployment.md`](../docs/deployment.md)。 +例如 `` 可为 `win-x64`、`linux-x64` 或 `linux-arm64`。NativeAOT 发布需要对应平台的 C/C++ 工具链。仓库根目录的 `scripts/publish.sh backend ` 可构建单个平台的 Backend 可执行文件;容器发布由 Woodpecker tag 流水线完成,详见 [`../docs/deployment.md`](../docs/deployment.md)。 + +正式发布通过 Woodpecker tag 流水线生成 Linux/amd64 与 Linux/arm64 OCI 镜像,容器内监听 `8080`,数据写入挂载的 `/data` 卷。Agent 与 Probe 的多平台二进制包单独发布,不包含在 Backend 镜像中。 ## 本地运行 diff --git a/ci/index-backend-image.sh b/ci/index-backend-image.sh new file mode 100644 index 0000000..c2d1042 --- /dev/null +++ b/ci/index-backend-image.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Publish a multi-architecture OCI index for the two architecture-specific +# Backend images already pushed by release-linux.sh. +set -euo pipefail + +version="${CI_COMMIT_TAG#v}" +[[ "${CI_COMMIT_TAG:-}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || { + echo "release must run from a vMAJOR.MINOR.PATCH tag" >&2 + exit 2 +} + +registry="${ZOT_REGISTRY:-192.168.2.162:5000}" +scheme="${ZOT_SCHEME:-http}" +image="monitor/monitor-backend" +auth_file="$(mktemp)" +chmod 600 "$auth_file" +trap 'rm -f "$auth_file"' EXIT +printf 'machine %s login %s password %s\n' "${registry%%:*}" "$ZOT_USERNAME" "$ZOT_PASSWORD" > "$auth_file" + +accept='application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json' + +manifest_head() { + local tag="$1" + curl --silent --show-error --fail --head --netrc-file "$auth_file" \ + -H "Accept: $accept" "$scheme://$registry/v2/$image/manifests/$tag" \ + | tr -d '\r' | awk ' + tolower($1)=="docker-content-digest:" { digest=$2 } + tolower($1)=="content-length:" { size=$2 } + tolower($1)=="content-type:" { media=$2 } + END { if (digest && size && media) print digest, size, media; else exit 1 }' +} + +amd="$(manifest_head "$version-amd64")" +arm="$(manifest_head "$version-arm64")" +read -r amd_digest amd_size amd_media <<< "$amd" +read -r arm_digest arm_size arm_media <<< "$arm" +index_media='application/vnd.oci.image.index.v1+json' +body="$(printf '{"schemaVersion":2,"mediaType":"%s","manifests":[{"mediaType":"%s","digest":"%s","size":%s,"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"%s","digest":"%s","size":%s,"platform":{"architecture":"arm64","os":"linux"}}]}' \ + "$index_media" "$amd_media" "$amd_digest" "$amd_size" "$arm_media" "$arm_digest" "$arm_size")" + +put_tag() { + local tag="$1" code + code="$(curl --silent --show-error --netrc-file "$auth_file" -o /dev/null -w '%{http_code}' \ + -H "Content-Type: $index_media" -X PUT --data-binary "$body" \ + "$scheme://$registry/v2/$image/manifests/$tag")" + case "$code" in + 200|201) echo "published $image:$tag" ;; + *) echo "failed to publish $image:$tag (HTTP $code)" >&2; return 1 ;; + esac +} + +put_tag "$version" +if [[ "$version" != *-* ]]; then + put_tag latest +fi diff --git a/ci/release-linux.sh b/ci/release-linux.sh new file mode 100644 index 0000000..e4d6d11 --- /dev/null +++ b/ci/release-linux.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# Build and publish one Linux architecture: Agent/Probe binary archives to +# Gitea Generic Packages and the matching Backend OCI image to Zot. +set -euo pipefail + +arch="${1:?usage: release-linux.sh }" +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +version="${CI_COMMIT_TAG#v}" +[[ "${CI_COMMIT_TAG:-}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || { + echo "release must run from a vMAJOR.MINOR.PATCH tag" >&2 + exit 2 +} + +[[ -n "${GITEA_TOKEN:-}" ]] || { echo "GITEA_TOKEN is required" >&2; exit 2; } +[[ -n "${ZOT_USERNAME:-}" && -n "${ZOT_PASSWORD:-}" ]] || { echo "Zot credentials are required" >&2; exit 2; } + +gitea_base="${GITEA_BASE:-https://git.oeo.one}" +gitea_host="${gitea_base#https://}" +gitea_host="${gitea_host#http://}" +gitea_host="${gitea_host%%/*}" +gitea_host="${gitea_host%%:*}" +registry="${ZOT_REGISTRY:-192.168.2.162:5000}" +image="${registry}/monitor/monitor-backend" +output="$root/.release/linux-$arch" +build_root="$root/.release/build/linux-$arch" +stage_root="$root/.release/stage/linux-$arch" +rid="linux-x64" +objcopy="llvm-objcopy" +if [[ "$arch" == arm64 ]]; then + rid="linux-arm64" + objcopy="aarch64-linux-gnu-objcopy" +elif [[ "$arch" != amd64 ]]; then + echo "unsupported architecture: $arch" >&2 + exit 2 +fi + +project_version="$(sed -n 's#.*\(.*\).*#\1#p' "$root/backend/Directory.Build.props" | head -1)" +if [[ "$version" != "$project_version" ]]; then + echo "tag version $version does not match project version $project_version" >&2 + exit 1 +fi + +mkdir -p "$output" "$build_root" "$stage_root" + +publish_aot() { + local name="$1" project="$2" destination="$3" + (cd "$root/backend" && dotnet publish "$project" \ + -c Release -r "$rid" --self-contained true \ + -p:PublishAot=true -p:StripSymbols=true -p:DebugType=none \ + -p:ObjCopyName="$objcopy" \ + --artifacts-path "$build_root/$name" \ + -o "$destination") +} + +publish_aot agent agent/src/Monitor.Agent "$output/agent" +publish_aot probe probe/src/Monitor.Probe "$output/probe" +publish_aot fleet-enroll fleet/tools/fleet-enroll "$output/fleet-enroll" +publish_aot backend src/Monitor.Backend "$output/backend" + +package_node() { + local role="$1" stage="$stage_root/$1" + mkdir -p "$stage/systemd" + cp "$output/$role/monitor-$role" "$stage/monitor-$role" + cp "$root/packaging/install.sh" "$stage/install.sh" + cp "$root/LICENSE" "$stage/LICENSE" + cp "$root/backend/$role/README.md" "$stage/README.md" + cp "$root/packaging/systemd/monitor-$role.service" "$stage/systemd/monitor-$role.service" + if [[ "$role" == agent ]]; then + cp "$output/fleet-enroll/fleet-enroll" "$stage/fleet-enroll" + else + mkdir -p "$stage/openwrt" + cp "$root/packaging/openwrt/monitor-probe.init" "$stage/openwrt/" + cp "$root/packaging/openwrt/monitor.keep" "$stage/openwrt/" + fi + + local archive="$output/monitor-$role-v$version-linux-$arch.tar.gz" + tar -C "$stage" -czf "$archive" . + (cd "$output" && sha256sum "$(basename "$archive")" > "$(basename "$archive").sha256") + upload_package "$archive" + upload_package "$archive.sha256" +} + +netrc="$(mktemp)" +chmod 600 "$netrc" +printf 'machine %s login justme password %s\n' "$gitea_host" "$GITEA_TOKEN" > "$netrc" +cleanup() { rm -f "$netrc"; } +trap cleanup EXIT + +upload_package() { + local file="$1" name + name="$(basename "$file")" + curl --fail-with-body --retry 3 --netrc-file "$netrc" \ + --upload-file "$file" \ + "$gitea_base/api/packages/justme/generic/monitor-node/$version/$name" >/dev/null + echo "uploaded Gitea package $name" +} + +package_node agent +package_node probe + +# The .NET runtime-deps image is a multi-arch OCI base. The Containerfile does +# not execute target-platform commands, so Buildah can assemble either image. +container_context="$root/.release/container/linux-$arch" +mkdir -p "$container_context/app" "$container_context/data" +cp -a "$output/backend/." "$container_context/app/" +cp "$root/packaging/container/data/.keep" "$container_context/data/.keep" + +export REGISTRY_AUTH_FILE="$root/.release/auth/zot-linux-$arch.json" +mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")" +buildah login --tls-verify=false --username "$ZOT_USERNAME" --password-stdin "$registry" <<< "$ZOT_PASSWORD" >/dev/null +local_image="localhost/monitor-backend:$version-$arch" +buildah bud --tls-verify=false --arch "$arch" \ + --build-arg "APP_VERSION=$version" \ + -f "$root/packaging/container/Backend.Containerfile" \ + -t "$local_image" "$container_context" >/dev/null +buildah push --tls-verify=false "$local_image" "docker://$image:$version-$arch" >/dev/null +echo "pushed Zot image $image:$version-$arch" diff --git a/ci/release-windows.ps1 b/ci/release-windows.ps1 new file mode 100644 index 0000000..432b541 --- /dev/null +++ b/ci/release-windows.ps1 @@ -0,0 +1,118 @@ +param( + [Parameter(Mandatory)] + [ValidateSet('amd64', 'arm64')] + [string]$Architecture +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +if (-not $env:CI_COMMIT_TAG -or $env:CI_COMMIT_TAG -notmatch '^v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$') { + throw 'Release must run from a vMAJOR.MINOR.PATCH tag.' +} +$version = $Matches[1] +$root = Split-Path -Parent $PSScriptRoot +$backend = Join-Path $root 'backend' +$projectVersion = [regex]::Match((Get-Content (Join-Path $backend 'Directory.Build.props') -Raw), '([^<]+)').Groups[1].Value +if ($version -ne $projectVersion) { + throw "Tag version $version does not match project version $projectVersion." +} +if (-not $env:GITEA_TOKEN) { + throw 'GITEA_TOKEN is required.' +} + +$rid = if ($Architecture -eq 'amd64') { 'win-x64' } else { 'win-arm64' } +$vcArchitecture = if ($Architecture -eq 'amd64') { 'x64' } else { 'arm64' } +$vsWhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' +$vsPath = & $vsWhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath +if (-not $vsPath) { + throw 'Visual Studio C++ Build Tools were not found.' +} + +$devCmd = Join-Path $vsPath 'Common7\Tools\VsDevCmd.bat' +$environmentLines = & cmd.exe /d /s /c "call `"$devCmd`" -no_logo -arch=$vcArchitecture -host_arch=x64 && set" +foreach ($line in $environmentLines) { + $separator = $line.IndexOf('=') + if ($separator -gt 0) { + [Environment]::SetEnvironmentVariable($line.Substring(0, $separator), $line.Substring($separator + 1), 'Process') + } +} + +$buildRoot = Join-Path $root ".release\build\windows-$Architecture-$PID" +$publishRoot = Join-Path $root ".release\windows-$Architecture-$PID" +$packageRoot = Join-Path $root '.release\packages' +$null = New-Item -ItemType Directory -Force -Path $publishRoot, $packageRoot + +function Publish-Aot([string]$Project, [string]$Output) { + $arguments = @( + 'publish', $Project, + '-c', 'Release', + '-r', $rid, + '--self-contained', 'true', + '-p:PublishAot=true', + '-p:StripSymbols=true', + '-p:DebugType=none', + '--artifacts-path', $buildRoot, + '-o', $Output + ) + Push-Location $backend + try { + & dotnet @arguments + if ($LASTEXITCODE -ne 0) { + throw "NativeAOT publish failed for $Project ($rid)." + } + } + finally { + Pop-Location + } +} + +$agentOut = Join-Path $publishRoot 'agent' +$probeOut = Join-Path $publishRoot 'probe' +$enrollOut = Join-Path $publishRoot 'fleet-enroll' +$null = New-Item -ItemType Directory -Force -Path $agentOut, $probeOut, $enrollOut +Publish-Aot 'agent/src/Monitor.Agent' $agentOut +Publish-Aot 'probe/src/Monitor.Probe' $probeOut +Publish-Aot 'fleet/tools/fleet-enroll' $enrollOut + +$giteaBase = if ($env:GITEA_BASE) { $env:GITEA_BASE.TrimEnd('/') } else { 'https://git.oeo.one' } +$netrc = Join-Path $env:TEMP ("monitor-gitea-$([guid]::NewGuid().ToString('N')).netrc") +$giteaHost = ([Uri]$giteaBase).Host +Set-Content -LiteralPath $netrc -Value "machine $giteaHost login justme password $($env:GITEA_TOKEN)" -Encoding ascii -NoNewline + +function Publish-Package([string]$Role, [string]$BinaryDirectory) { + $stage = Join-Path $publishRoot "stage-$Role" + $null = New-Item -ItemType Directory -Force -Path $stage + Copy-Item -LiteralPath (Join-Path $BinaryDirectory "monitor-$Role.exe") -Destination $stage + Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination $stage + Copy-Item -LiteralPath (Join-Path $root "backend\$Role\README.md") -Destination $stage + if ($Role -eq 'agent') { + Copy-Item -LiteralPath (Join-Path $enrollOut 'fleet-enroll.exe') -Destination $stage + } + + $archiveName = "monitor-$Role-v$version-windows-$Architecture.zip" + $archive = Join-Path $packageRoot $archiveName + Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $archive -CompressionLevel Optimal -Force + $hash = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() + $checksum = "$hash $archiveName`n" + $checksumPath = "$archive.sha256" + [IO.File]::WriteAllText($checksumPath, $checksum, [Text.UTF8Encoding]::new($false)) + + foreach ($file in @($archive, $checksumPath)) { + $name = Split-Path -Leaf $file + $url = "$giteaBase/api/packages/justme/generic/monitor-node/$version/$name" + & curl.exe --fail-with-body --retry 3 --netrc-file $netrc --upload-file $file $url + if ($LASTEXITCODE -ne 0) { + throw "Gitea package upload failed for $name." + } + Write-Output "uploaded Gitea package $name" + } +} + +try { + Publish-Package 'agent' $agentOut + Publish-Package 'probe' $probeOut +} +finally { + Remove-Item -LiteralPath $netrc -Force -ErrorAction SilentlyContinue +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..93b92cc --- /dev/null +++ b/compose.yaml @@ -0,0 +1,26 @@ +services: + backend: + image: registry.oeo.one/monitor/monitor-backend:${MONITOR_VERSION:-0.1.0} + container_name: monitor-backend + restart: unless-stopped + init: true + ports: + - "127.0.0.1:8080:8080" + environment: + ASPNETCORE_HTTP_PORTS: "8080" + Monitor__DataDirectory: /data + Monitor__InitialAdminUser: ${MONITOR_ADMIN_USER:-admin} + Monitor__InitialAdminPassword: "${MONITOR_ADMIN_PASSWORD:?Set MONITOR_ADMIN_PASSWORD in the environment}" + volumes: + - monitor-data:/data + read_only: true + tmpfs: + - /tmp:rw,noexec,nosuid,size=64m,mode=1777 + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + stop_grace_period: 30s + +volumes: + monitor-data: diff --git a/docs/deployment.md b/docs/deployment.md index 69ec9d1..7749245 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -18,6 +18,26 @@ scripts/package.sh → backend/art NativeAOT 不能交叉编译到别的操作系统;MIPS 不支持(见 `project-layout.md`)。 +仓库内的 `scripts/publish.sh` 面向本机目标平台;tag 发布流水线另使用 Linux 交叉工具链生成 arm64 产物,并在 Windows runner 上构建 Windows x64/arm64 产物。 + +## Backend 容器 + +Backend 使用 OCI 标准镜像,通过 `registry.oeo.one/monitor/monitor-backend:` 分发,支持 `linux/amd64` 和 `linux/arm64`。容器基于 .NET 10 `runtime-deps` noble-chiseled-extra 镜像,以非 root 用户运行,在 `8080/tcp` 提供 HTTP 服务;数据库和原始数据统一写入 `/data`。`extra` 变体带 IANA 时区数据,供按时区查询使用。将 `/data` 挂载为持久卷,并在反向代理处配置 HTTPS。 + +在部署目录保存此仓库的 `compose.yaml`,先通过部署平台的环境变量或密钥管理设置 `MONITOR_ADMIN_PASSWORD`(可选设置 `MONITOR_ADMIN_USER`、`MONITOR_VERSION`),然后启动: + +```sh +docker compose up -d +``` + +Compose 将容器端口只绑定到宿主机 `127.0.0.1:8080`,适合由同机 HTTPS 反向代理转发;数据保存在 Docker 命名卷 `monitor-data`。首次启动成功创建管理员后,改密应在管理端完成。 + +健康检查端点为 `GET /healthz`。容器定义见 [`../packaging/container/Backend.Containerfile`](../packaging/container/Backend.Containerfile)。 + +## 节点二进制发布 + +Woodpecker 在 `vMAJOR.MINOR.PATCH` tag 上构建 Agent 和 Probe 的 Windows/Linux amd64/arm64 NativeAOT 程序,打包为 `.zip` 或 `.tar.gz` 并上传到 Gitea Generic Packages。Agent 包还包含一次性注册工具 `fleet-enroll`。Linux 包带有安装脚本和 systemd/OpenWrt 单元;Windows 包包含可执行文件及对应 README。安装说明见 [`../backend/agent/README.md`](../backend/agent/README.md) 与 [`../backend/probe/README.md`](../backend/probe/README.md)。 + 实测体积(`StackTraceSupport=false`、`IlcFoldIdenticalMethodBodies=true`、`OptimizationPreference=Size`): | 程序 | linux-musl-x64 | linux-x64 | win-x64 | diff --git a/packaging/container/Backend.Containerfile b/packaging/container/Backend.Containerfile new file mode 100644 index 0000000..cc36afb --- /dev/null +++ b/packaging/container/Backend.Containerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extra + +ARG APP_VERSION=unknown +LABEL org.opencontainers.image.title="Monitor Backend" \ + org.opencontainers.image.description="Self-hosted network path quality monitoring backend" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.version="$APP_VERSION" + +WORKDIR /app +COPY --chown=1654:1654 app/ /app/ +COPY --chown=1654:1654 data/.keep /data/.keep + +ENV ASPNETCORE_HTTP_PORTS=8080 \ + Monitor__DataDirectory=/data \ + DOTNET_EnableDiagnostics=0 + +USER 1654:1654 +VOLUME ["/data"] +EXPOSE 8080 +ENTRYPOINT ["/app/monitor-backend"]