diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6db9090 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +**/%* +**/bin/ +**/obj/ +.git/ +.vs/ +.vscode/ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6efef2..c8e0bc7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,18 +2,17 @@ name: Build app on: pull_request: + branches: [ main ] jobs: build: runs-on: ubuntu-latest permissions: contents: read - packages: write + packages: read # Changed 'write' to 'read' for security as this doesn't push steps: - name: Checkout uses: actions/checkout@v4 - with: - ref: ${{ github.ref }} - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -27,4 +26,4 @@ jobs: run: dotnet build -c Release --no-restore - name: Test - run: dotnet test -c Release --no-build + run: dotnet test -c Release --no-build --verbosity normal \ No newline at end of file diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..aa7cba5 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,29 @@ +name: Docker Build Check + +on: [push, pull_request, workflow_dispatch] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build Docker image + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64,linux/arm64,linux/arm/v7 + file: ./Dockerfile + push: false # We only want to test the build, not push it + tags: patchpanda:test + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 29e6caa..981c9b0 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -1,8 +1,8 @@ -name: Build and Publish Docker image to GHCR +name: Build and publish Docker image on: release: - types: [published] + types: [released] jobs: build-and-push: @@ -10,39 +10,40 @@ jobs: permissions: contents: read packages: write + id-token: write # Required for security attestations steps: - name: Checkout code uses: actions/checkout@v4 + - name: Lowercase repo name + run: echo "REPO_LC=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - platforms: linux/amd64,linux/arm64,linux/arm/v7 + uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io - username: ${{ secrets.GHCR_USERNAME }} - password: ${{ secrets.GHCR_TOKEN }} - - - name: Extract release version - id: vars - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - - - name: Set lowercase repository name - id: repo-name - run: echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . - platforms: linux/amd64,linux/arm64,linux/arm + platforms: linux/amd64,linux/arm64,linux/arm/v7 file: ./Dockerfile push: true build-args: | - RELEASE_VERSION=${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ github.event.release.tag_name }} tags: | - ghcr.io/${{ env.REPO_LOWER }}:${{ env.RELEASE_VERSION }} - ghcr.io/${{ env.REPO_LOWER }}:latest + ghcr.io/${{ env.REPO_LC }}:latest + ghcr.io/${{ env.REPO_LC }}:${{ github.event.release.tag_name }} + provenance: true + sbom: true + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/.github/workflows/docker-unstable.yml b/.github/workflows/docker-unstable.yml index c6ceaf9..c404981 100644 --- a/.github/workflows/docker-unstable.yml +++ b/.github/workflows/docker-unstable.yml @@ -21,73 +21,69 @@ jobs: permissions: contents: read packages: write + id-token: write # Required for security attestations (v7) steps: - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch || github.ref }} + - name: Lowercase repo name + run: echo "REPO_LC=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - platforms: linux/amd64,linux/arm64,linux/arm + uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io - username: ${{ secrets.GHCR_USERNAME }} - password: ${{ secrets.GHCR_TOKEN }} - - - name: Set lowercase repository name - id: repo-name - run: echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . - platforms: linux/amd64,linux/arm64,linux/arm + platforms: linux/amd64,linux/arm64,linux/arm/v7 file: ./Dockerfile push: true build-args: | RELEASE_VERSION=unstable-${{ github.sha }} - tags: ghcr.io/${{ env.REPO_LOWER }}:unstable + tags: ghcr.io/${{ env.REPO_LC }}:unstable + provenance: true + sbom: true test-multi-arch: needs: build-and-push runs-on: ubuntu-latest - permissions: - contents: read - packages: read strategy: + fail-fast: false # Run all architectures even if one fails matrix: - arch: [amd64, arm64, arm] + arch: [amd64, arm64, arm/v7] steps: + - name: Lowercase repo name + run: echo "REPO_LC=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - with: - platforms: linux/${{ matrix.arch }} - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io - username: ${{ secrets.GHCR_USERNAME }} - password: ${{ secrets.GHCR_TOKEN }} - - - name: Set lowercase repository name - id: repo-name - run: echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - - - name: Pull Docker image for ${{ matrix.arch }} - run: | - docker pull --platform linux/${{ matrix.arch }} ghcr.io/${{ env.REPO_LOWER }}:unstable + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Run test container for ${{ matrix.arch }} + - name: Test image on ${{ matrix.arch }} run: | - docker run -p 8080:8080 -d --name patchpanda-test --platform linux/${{ matrix.arch }} ghcr.io/${{ env.REPO_LOWER }}:unstable - sleep 50 - docker logs patchpanda-test - docker stop patchpanda-test - docker rm patchpanda-test + set -e + echo "Starting smoke test for ${{ matrix.arch }}..." + # We use a 3-minute timeout to allow slow arm/v7 emulation to boot fully. + # Exit code 124 means the timeout hit, which confirms the app stayed up. + timeout 180s docker run --rm --platform linux/${{ matrix.arch }} \ + ghcr.io/${{ env.REPO_LC }}:unstable || [ $? -eq 124 ] + echo "Successfully verified image execution for ${{ matrix.arch }}" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6995de6..5f3391a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,69 +1,99 @@ -# Declare ARGs for build platform and target architecture for clarity +# PatchPanda.Web - Multi-Stage Docker Build (2026 Standards) +# Support: amd64, arm64, arm/v7 +# Features: Non-root user, Security Patches, Docker-in-Docker CLI, OIDC Ready, Health Checks + ARG BUILDPLATFORM +# ============================================================================ +# STAGE 1: Base Runtime (Hardened) +# ============================================================================ FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base + WORKDIR /app EXPOSE 8080 +# Patch OS vulnerabilities and install basic dependencies +USER root +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends curl ca-certificates gnupg && \ + rm -rf /var/lib/apt/lists/* +USER app + +# ============================================================================ +# STAGE 2: Build (Cross-Platform) +# ============================================================================ FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build + ARG TARGETARCH +ARG TARGETVARIANT WORKDIR /src +# Copy project file and restore specifically for the target architecture COPY ["PatchPanda.Web/PatchPanda.Web.csproj", "PatchPanda.Web/"] -# Map Docker's TARGETARCH to the arch used in .NET RIDs and restore dependencies RUN export DOTNET_ARCH=$(case ${TARGETARCH} in \ - "amd64") echo "x64" ;; \ - "arm64") echo "arm64" ;; \ - "arm") echo "arm" ;; \ - *) echo "Unsupported architecture: ${TARGETARCH}"; exit 1 ;; \ - esac) && \ - dotnet restore "PatchPanda.Web/PatchPanda.Web.csproj" -r "linux-${DOTNET_ARCH}" + "amd64") echo "x64" ;; \ + "arm64") echo "arm64" ;; \ + "arm") [ "${TARGETVARIANT}" = "v7" ] && echo "arm" || { echo "ERROR: Unsupported ARM variant '${TARGETVARIANT}'"; exit 1; } ;; \ + *) echo "ERROR: Unsupported architecture '${TARGETARCH}'"; exit 1 ;; \ + esac) && \ + dotnet restore "PatchPanda.Web/PatchPanda.Web.csproj" \ + --runtime "linux-${DOTNET_ARCH}" +# Copy remaining source code COPY . . -# Build and publish the application for the target runtime +# Publish the Release binary with strict ARMv7 validation RUN export DOTNET_ARCH=$(case ${TARGETARCH} in \ - "amd64") echo "x64" ;; \ - "arm64") echo "arm64" ;; \ - "arm") echo "arm" ;; \ - esac) && \ - dotnet publish "PatchPanda.Web/PatchPanda.Web.csproj" \ - -c Release \ - -o /app/publish \ - -r "linux-${DOTNET_ARCH}" \ - --no-restore \ - --self-contained false + "amd64") echo "x64" ;; \ + "arm64") echo "arm64" ;; \ + "arm") [ "${TARGETVARIANT}" = "v7" ] && echo "arm" || { echo "ERROR: Unsupported ARM variant '${TARGETVARIANT}'"; exit 1; } ;; \ + *) echo "ERROR: Unsupported architecture '${TARGETARCH}'"; exit 1 ;; \ + esac) && \ + dotnet publish "PatchPanda.Web/PatchPanda.Web.csproj" \ + --configuration Release \ + --output /app/publish \ + --runtime "linux-${DOTNET_ARCH}" \ + --no-restore \ + --self-contained false +# ============================================================================ +# STAGE 3: Final Production Image (Ubuntu 26.04 LTS "Resolute") +# ============================================================================ FROM base AS final -# Install utilities needed for installing the Docker CLI -RUN apt-get update && \ - apt-get install -y \ - ca-certificates \ - curl \ - gnupg \ - lsb-release && \ - \ - # Add Docker's official GPG key - mkdir -m 0755 -p /etc/apt/keyrings && \ - curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \ - \ - # Set up the stable repository, explicitly using 'bookworm' (Debian 12) - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \ - bookworm stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \ - \ - # Install the Docker CLI only - apt-get update && \ - apt-get install -y docker-ce-cli && \ - \ - # Clean up - rm -rf /var/lib/apt/lists/* -WORKDIR /app -COPY --from=build /app/publish . +# Set up environment and diagnostics ARG RELEASE_VERSION +ARG ENABLE_DIAGNOSTICS=0 ENV APP_VERSION=$RELEASE_VERSION -LABEL version=$RELEASE_VERSION +ENV DOTNET_EnableDiagnostics=${ENABLE_DIAGNOSTICS} + +# Install Docker CLI using Ubuntu 'resolute' repo +USER root +RUN mkdir -m 0755 -p /etc/apt/keyrings && \ + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \ + chmod a+r /etc/apt/keyrings/docker.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu resolute stable" > /etc/apt/sources.list.d/docker.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends docker-ce-cli && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app +# Copy artifacts first (owned by root for security) +COPY --from=build /app/publish . + +# Create data directory and fix permissions AFTER artifacts are copied +# Narrowed chown ensures binaries stay read-only while data is writable +RUN mkdir -p /app/data && \ + chown -R app:app /app/data + +HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:8080/ || exit 1 + +LABEL version=$RELEASE_VERSION \ + description="PatchPanda Web Application" \ + maintainer="dkorecko" +USER app ENTRYPOINT ["dotnet", "PatchPanda.Web.dll"] \ No newline at end of file