Files
admin/_docs/04_deploy/containerization.md
T
Oleksandr Bezdieniezhnykh c7b297de83
ci/woodpecker/push/01-test Pipeline failed
ci/woodpecker/push/02-build-push unknown status
refactor: remove deploy.cmd and update Dockerfile for health checks
- 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>
2026-05-13 08:47:21 +03:00

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/Dockerfile is 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 basebuildpublishfinal 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 run against a host Postgres on port 4312. We add docker-compose.yml in 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), so docker-compose.yml and docker-compose.test.yml produce DB images with identical structure.
  • .dev/ is added to .gitignore and .dockerignore in Step 7.
  • db.ports exposes 4312:5432 so 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 /health planned in Step 5; TCP fallback documented for the interim).
  • docker-compose.yml covers all components + Postgres dependency.
  • docker-compose.test.yml already enables black-box testing; one observation logged (Drift H).
  • .dockerignore defined 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