- Deleted the deploy.cmd script as it was no longer needed. - Updated Dockerfile to include curl for health checks and added a non-root user for improved security. - Modified health check command to use curl for better reliability. - Adjusted docker-compose.test.yml to reflect changes in health check configuration. - Cleaned up appsettings.json and removed unused configuration properties. - Removed Resource entity and related requests from the codebase as part of the architectural shift. - Updated documentation to reflect the removal of hardware binding and related endpoints. Co-authored-by: Cursor <cursoragent@cursor.com>
11 KiB
Azaion Admin API — Containerization
Date: 2026-05-13 · Cycle: 1 · Status: planning artifact (no code changes; Dockerfile updates land in Step 7).
1. Container Inventory
The system has only one runtime container. The four library components are linked into the API at build time, not shipped separately.
| # | Container | Built from | Purpose | Lifetime |
|---|---|---|---|---|
| 1 | admin-api |
Dockerfile (root) |
Single ASP.NET Core 10 service exposing all 17 endpoints | Long-running |
| 2 | e2e-runner |
e2e/Dockerfile |
Black-box test consumer used by CI and local docker-compose.test.yml |
One-shot (run-and-exit) |
| 3 | test-db |
postgres:16-alpine (no custom Dockerfile) |
Isolated Postgres for tests | One-shot (per CI run) |
docker.test/Dockerfileis a leftover placeholder (FROM alpine:latest; CMD echo hello) and is unused. Drift G — recommend deletion in Step 7 (scripts) cleanup.
2. Component → Container Mapping
| Component | Ships in container? | Notes |
|---|---|---|
| 01 Data Layer | no | Class library Azaion.Common, linked into admin-api |
| 02 User Management | no | Class library Azaion.Services |
| 03 Auth & Security | no | Class library Azaion.Services |
| 04 Resource Management | no | Class library Azaion.Services |
| 05 Admin API | yes | Hosts the Minimal API process (Azaion.AdminApi) |
3. admin-api — Dockerfile Specification
| Property | Current value | Planned value (Step 7) | Rationale |
|---|---|---|---|
| Build base image | mcr.microsoft.com/dotnet/sdk:10.0 (--platform=$BUILDPLATFORM) |
unchanged | Matches restriction (.NET 10.0); cross-platform build supported |
| Runtime base image | mcr.microsoft.com/dotnet/aspnet:10.0 |
pin by digest in production (@sha256:…) |
Restrictions forbid moving off aspnet:10.0; digest pin protects against silent base-image churn |
| Stages | base → build → publish → final |
unchanged structure; non-root user added in final |
Existing layout already follows multi-stage best practice |
| Working dir | /app |
unchanged | Matches start-container.sh mounts |
| Exposed port | 8080 |
unchanged | Bound by Kestrel via ASPNETCORE_URLS=http://+:8080 |
| Container user | root (current) | USER app (UID 1654, GID 1654) |
Closes security audit F-6 / AZ-518 (Drift C). Non-existing UID; matches the convention in mcr.microsoft.com/dotnet/aspnet:8.0+ images |
| Mount points needing write | /app/Content, /app/logs |
chown app:app both directories in the final stage |
The new non-root user must own the dirs that are bind-mounted from the host |
| Build arg | CI_COMMIT_SHA=unknown |
unchanged; populated by Woodpecker | Already wired; surfaces as AZAION_REVISION env var inside the container |
| OCI labels | none on the Dockerfile (CI adds three: revision, created, source) |
move the three labels into the Dockerfile so local builds also carry them | Single source of truth; consistent labeling regardless of build origin |
| Health check | none | HEALTHCHECK CMD curl -fsS http://localhost:8080/health || exit 1 |
Wires into the /health endpoint planned in Step 5 (Observability). Until that endpoint exists, fall back to the TCP probe already used in docker-compose.test.yml. |
| Entrypoint | ["dotnet", "Azaion.AdminApi.dll"] |
unchanged | Smallest-possible entrypoint; PID 1 is the .NET process |
Sketch (planning artifact — actual edits land in Step 7)
FROM mcr.microsoft.com/dotnet/aspnet:10.0@sha256:<pinned> AS base
WORKDIR /app
EXPOSE 8080
RUN groupadd -g 1654 app && useradd -u 1654 -g 1654 -m -d /home/app -s /sbin/nologin app \
&& mkdir -p /app/Content /app/logs && chown -R app:app /app
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG TARGETARCH
WORKDIR /app
COPY . .
RUN dotnet restore
WORKDIR /app/Azaion.AdminApi
RUN dotnet build "Azaion.AdminApi.csproj" -c Release -o /app/build
FROM build AS publish
RUN arch=$([ "$TARGETARCH" = "amd64" ] && echo "x64" || echo "$TARGETARCH") && \
dotnet publish "Azaion.AdminApi.csproj" -c Release -o /app/publish /p:UseAppHost=false --os linux --arch $arch
FROM base AS final
ARG CI_COMMIT_SHA=unknown
ARG BUILD_DATE=unknown
ENV AZAION_REVISION=$CI_COMMIT_SHA
LABEL org.opencontainers.image.revision="$CI_COMMIT_SHA"
LABEL org.opencontainers.image.created="$BUILD_DATE"
LABEL org.opencontainers.image.source="https://git.azaion.com/azaion/admin"
COPY --from=publish --chown=app:app /app/publish /app/
USER app
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -fsS http://localhost:8080/health || exit 1
ENTRYPOINT ["dotnet", "Azaion.AdminApi.dll"]
4. e2e-runner — Dockerfile Specification
Existing e2e/Dockerfile is sufficient for cycle 1; no changes proposed.
| Property | Value | Notes |
|---|---|---|
| Base image | mcr.microsoft.com/dotnet/sdk:10.0 (build + run) |
SDK is required because the runner invokes dotnet test |
| Stages | build → run |
Multi-stage to discard sources from the final image |
| Working dir | /test |
Matches docker-compose.test.yml |
| Output dir | /test-results |
Bind-mounted to ./e2e/test-results on the host |
| User | root (acceptable — short-lived, no network exposure, no persistence beyond /test-results) |
Non-root not required for one-shot CI containers |
| Loggers | console, trx, xunit |
Last one feeds Woodpecker's parser |
| Entrypoint | dotnet test Azaion.E2E.dll … |
Already present |
5. Local Development — docker-compose.yml
Currently the project does not ship a local-dev compose file. Local devs run the API via
dotnet runagainst a host Postgres on port 4312. We adddocker-compose.ymlin Step 7 (scripts) so newcomers get a one-command bring-up.
# docker-compose.yml — planning artifact for Step 7
services:
api:
build:
context: .
dockerfile: Dockerfile
args:
CI_COMMIT_SHA: dev
image: azaion/admin:dev-local
env_file: .env
depends_on:
db:
condition: service_healthy
ports:
- "8080:8080"
volumes:
- ./.dev/content:/app/Content
- ./.dev/logs:/app/logs
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 5
start_period: 30s
networks: [azaion-net]
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
volumes:
- ./e2e/db-init/00_run_all.sh:/docker-entrypoint-initdb.d/00_run_all.sh:ro
- ./env/db:/docker-entrypoint-initdb.d/sql:ro
- dev-db:/var/lib/postgresql/data
ports:
- "4312:5432" # match local-dev convention; non-standard host port
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
networks: [azaion-net]
volumes:
dev-db:
networks:
azaion-net:
driver: bridge
Notes:
- The DB schema and roles are bootstrapped from the same SQL files that the test-compose uses (
env/db/*.sql), sodocker-compose.ymlanddocker-compose.test.ymlproduce DB images with identical structure. .dev/is added to.gitignoreand.dockerignorein Step 7.db.portsexposes4312:5432so a developer running the API outside Docker can still hit the same connection string defined in.env.
6. Blackbox Test — docker-compose.test.yml (existing)
The current file is already aligned with the Step 2 contract (docker compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from e2e-consumer). Only one drift to log:
| Drift | Description | Resolved In |
|---|---|---|
| Drift H | system-under-test.healthcheck uses a raw bash TCP probe (exec 3<>/dev/tcp/127.0.0.1/8080). Once /health exists (Step 5), switch to the curl-based probe to actually test the application layer. |
Step 5 + Step 7 |
No structural change in cycle 1 — the file already brings up Postgres + SUT + e2e-runner on a private network and tears down on test exit.
7. Image Tagging Strategy
| Context | Tag format | Example | Notes |
|---|---|---|---|
| CI build (per push) | $REGISTRY_HOST/$REGISTRY_IMAGE:${CI_COMMIT_BRANCH}-${TAG_SUFFIX} |
docker.azaion.com/azaion/admin:dev-arm |
Existing convention from .woodpecker/02-build-push.yml |
| CI build (per push) — additional immutable tag | $REGISTRY_HOST/$REGISTRY_IMAGE:${CI_COMMIT_SHA:0:12}-${TAG_SUFFIX} |
docker.azaion.com/azaion/admin:a1b2c3d4e5f6-arm |
NEW (Drift A resolution) — gives every CI build an immutable tag the host scripts can pin |
| Production deploy | the SHA tag from above; never latest |
docker.azaion.com/azaion/admin:a1b2c3d4e5f6-arm |
Eliminates the host-pulls-:latest / CI-never-pushes-:latest mismatch |
| Local dev | azaion/admin:dev-local |
— | Built by docker-compose.yml; never pushed |
| Multi-arch (future) | <image>:<branch>-amd and <image>:<branch>-arm (already matrix-prepared) |
— | The Woodpecker matrix is wired; uncomment the amd64 row when an amd agent is online |
Drift A resolution depends on a CI change (Step 3) and a script change (Step 7). The tag format itself is decided here.
8. .dockerignore
Existing .dockerignore is sufficient; no changes proposed in cycle 1. It already excludes bin/, obj/, .env, .git, IDE folders, Dockerfile*, and compose files. The only addition required by the new local-dev compose is .dev/ — added in Step 7.
.dev
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
9. Self-verification
- Every component has a Dockerfile specification (only Admin API ships; libraries explicitly excluded with rationale).
- Multi-stage builds specified for every production image.
- Non-root user planned for
admin-api(Drift C closed in spec; code change in Step 7). - Health check defined for every long-running service (real
/healthplanned in Step 5; TCP fallback documented for the interim). docker-compose.ymlcovers all components + Postgres dependency.docker-compose.test.ymlalready enables black-box testing; one observation logged (Drift H)..dockerignoredefined and reviewed (one addition planned:.dev/).
10. Drifts Logged Here (carried forward)
| ID | Severity | Description | Resolved In |
|---|---|---|---|
| C | Medium | Dockerfile final stage runs as root → add USER app (UID 1654) |
Step 7 |
| G | Low | Unused docker.test/Dockerfile placeholder |
Step 7 (delete) |
| H | Low | docker-compose.test.yml health check is TCP-only; upgrade to /health once available |
Step 5 + Step 7 |