Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions .github/workflows/build-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:
- "blazor/create-interactive-ui-csharp-12/**"
- "blazor/forms-validation-masterclass/**"
- "blazor/ssr-interactive-islands/**"
- "cloud-native/dockerize-aspnet-core-clean-images/**"
- ".github/workflows/build-samples.yml"

pull_request:
Expand All @@ -34,6 +35,7 @@ on:
- "blazor/create-interactive-ui-csharp-12/**"
- "blazor/forms-validation-masterclass/**"
- "blazor/ssr-interactive-islands/**"
- "cloud-native/dockerize-aspnet-core-clean-images/**"
- ".github/workflows/build-samples.yml"

workflow_dispatch:
Expand Down Expand Up @@ -455,3 +457,165 @@ jobs:
blazor/ssr-interactive-islands/BlazorCatalogIslands.slnx
--configuration Release
--no-build

test-dockerized-aspnet-core:
name: Test Dockerized ASP.NET Core sample
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Check out repository
uses: actions/checkout@v5

- name: Install .NET 10 SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"

- name: Restore
run: >
dotnet restore
cloud-native/dockerize-aspnet-core-clean-images/ContainerizedApiMinimal.slnx

- name: Build
run: >
dotnet build
cloud-native/dockerize-aspnet-core-clean-images/ContainerizedApiMinimal.slnx
--configuration Release
--no-restore

- name: Test
run: >
dotnet test
cloud-native/dockerize-aspnet-core-clean-images/ContainerizedApiMinimal.slnx
--configuration Release
--no-build

- name: Validate Compose configuration
run: >
docker compose
--file cloud-native/dockerize-aspnet-core-clean-images/compose.yaml
config
--quiet

- name: Build container image
run: >
docker build
--pull
--tag dotnet-guide/containerized-api-minimal:ci
cloud-native/dockerize-aspnet-core-clean-images

- name: Run hardened container
run: |
docker run \
--detach \
--name containerized-api-ci \
--publish 127.0.0.1:5152:8080 \
--env ASPNETCORE_ENVIRONMENT=Production \
--env Sample__Message="Configured by GitHub Actions" \
--read-only \
--tmpfs /tmp \
--cap-drop ALL \
--security-opt no-new-privileges:true \
dotnet-guide/containerized-api-minimal:ci

- name: Wait for Docker health
run: |
status=""

for attempt in $(seq 1 30); do
status="$(
docker inspect \
--format='{{.State.Health.Status}}' \
containerized-api-ci
)"

echo "Attempt ${attempt}: ${status}"

if [ "$status" = "healthy" ]; then
break
fi

if [ "$status" = "unhealthy" ]; then
docker logs containerized-api-ci
exit 1
fi

sleep 2
done

test "$status" = "healthy"

- name: Verify container HTTP and runtime properties
run: |
python3 - <<'PY'
import json
import urllib.request

base = "http://127.0.0.1:5152"

for path in (
"/health/live",
"/health/ready",
"/health",
):
with urllib.request.urlopen(base + path) as response:
assert response.status == 200, (path, response.status)

with urllib.request.urlopen(base + "/api/todos") as response:
todos = json.load(response)

assert len(todos) == 3
assert any(
item["title"] == "Run the container as non-root"
for item in todos
)

with urllib.request.urlopen(base + "/info") as response:
info = json.load(response)

assert info["environment"] == "Production"
assert info["message"] == "Configured by GitHub Actions"
assert info["runningInContainer"] is True
assert info["user"].lower() != "root"
PY

- name: Verify non-root image and runtime-only contents
run: |
image_user="$(
docker image inspect \
dotnet-guide/containerized-api-minimal:ci \
--format='{{.Config.User}}'
)"

echo "Image user: ${image_user}"

test -n "$image_user"
test "$image_user" != "0"
test "$image_user" != "root"

sdk_output="$(
docker exec \
containerized-api-ci \
dotnet --list-sdks
)"

test -z "$sdk_output"

- name: Show container diagnostics
if: always()
run: |
docker ps --all
docker inspect containerized-api-ci || true
docker logs containerized-api-ci || true

- name: Remove test container
if: always()
run: |
docker rm \
--force \
containerized-api-ci \
2>/dev/null \
|| true
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Each sample folder contains a focused implementation of one tutorial topic. The
| [`blazor/create-interactive-ui-csharp-12`](blazor/create-interactive-ui-csharp-12/) | Interactive .NET 10 Todo Dashboard demonstrating Razor components, form binding, DataAnnotations validation, scoped state, EventCallback communication, filtering, and bUnit component tests | [Blazor Web Development: Create Interactive UIs with C# 12](https://www.dotnet-guide.com/tutorials/blazor/create-interactive-ui-csharp-12/) |
| [`blazor/forms-validation-masterclass`](blazor/forms-validation-masterclass/) | Focused .NET 10 Profile Settings form demonstrating manual EditContext management, DataAnnotations, FluentValidation, backend field-error mapping, accessible inputs, dirty state, and bUnit testing | [Blazor .NET 8 Forms & Validation: EditForm, FluentValidation & Server Error Handling](https://www.dotnet-guide.com/tutorials/blazor/forms-validation-masterclass/) |
| [`blazor/ssr-interactive-islands`](blazor/ssr-interactive-islands/) | Focused .NET 10 catalog demonstrating static SSR, streaming review updates, a serializable render-mode boundary, an Interactive Server cart island, and component/integration testing | [Blazor SSR & Interactive Islands: Streaming Rendering, Auto Render Mode & Progressive Enhancement](https://www.dotnet-guide.com/tutorials/blazor/ssr-interactive-islands/) |
| [`cloud-native/dockerize-aspnet-core-clean-images`](cloud-native/dockerize-aspnet-core-clean-images/) | Focused .NET 10 container sample demonstrating a multi-stage Dockerfile, locked restore, runtime-only image, non-root execution, port 8080, runtime configuration, health checks, hardened Compose settings, and CI smoke testing | [Dockerizing ASP.NET Core: Multi-Stage Builds, Clean Images & a Production-Ready Ship Workflow](https://www.dotnet-guide.com/tutorials/cloud-native/dockerize-aspnet-core-clean-images/) |

## Companion articles
- [Common Microsoft.Extensions.AI mistakes](https://www.dotnet-guide.com/articles/dotnet-ai/microsoft-extensions-ai-common-mistakes/)
Expand Down Expand Up @@ -201,6 +202,27 @@ tutorials/
| `-- BlazorCatalogIslands.Tests/
| |-- BlazorCatalogIslands.Tests.csproj
| `-- CatalogIslandTests.cs
|-- cloud-native/
| `-- dockerize-aspnet-core-clean-images/
| |-- ContainerizedApiMinimal.slnx
| |-- Dockerfile
| |-- .dockerignore
| |-- compose.yaml
| |-- README.md
| |-- src/
| | |-- ContainerizedApiMinimal/
| | | |-- ContainerizedApiMinimal.csproj
| | | |-- Program.cs
| | | |-- appsettings.json
| | | `-- packages.lock.json
| | `-- ContainerHealthProbe/
| | |-- ContainerHealthProbe.csproj
| | |-- Program.cs
| | `-- packages.lock.json
| `-- tests/
| `-- ContainerizedApiMinimal.Tests/
| |-- ContainerizedApiMinimal.Tests.csproj
| `-- ContainerizedApiTests.cs
|-- aspnet-core/
| |-- api-security-in-practice/
| | |-- ApiSecurityMinimal.slnx
Expand Down
35 changes: 35 additions & 0 deletions cloud-native/dockerize-aspnet-core-clean-images/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Git and repository metadata
.git/
.github/
.gitattributes
.gitignore

# .NET build output
**/bin/
**/obj/
**/TestResults/

# IDE and user files
.vs/
.vscode/
*.user
*.suo

# Tests are validated before image build and aren't copied into the runtime image
tests/

# Local configuration and secrets
.env
.env.*
**/secrets.json
**/appsettings.Local.json
**/*.pfx
**/*.key

# Repository documentation and local orchestration
README.md
compose.yaml

# Miscellaneous local artifacts
*.log
*.tmp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/ContainerHealthProbe/ContainerHealthProbe.csproj" />
<Project Path="src/ContainerizedApiMinimal/ContainerizedApiMinimal.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/ContainerizedApiMinimal.Tests/ContainerizedApiMinimal.Tests.csproj" />
</Folder>
</Solution>
63 changes: 63 additions & 0 deletions cloud-native/dockerize-aspnet-core-clean-images/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# syntax=docker/dockerfile:1

FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS restore
WORKDIR /src

COPY ["src/ContainerizedApiMinimal/ContainerizedApiMinimal.csproj", "src/ContainerizedApiMinimal/"]
COPY ["src/ContainerizedApiMinimal/packages.lock.json", "src/ContainerizedApiMinimal/"]

COPY ["src/ContainerHealthProbe/ContainerHealthProbe.csproj", "src/ContainerHealthProbe/"]
COPY ["src/ContainerHealthProbe/packages.lock.json", "src/ContainerHealthProbe/"]

RUN dotnet restore \
"src/ContainerizedApiMinimal/ContainerizedApiMinimal.csproj" \
--locked-mode \
&& dotnet restore \
"src/ContainerHealthProbe/ContainerHealthProbe.csproj" \
--locked-mode

FROM restore AS publish

COPY ["src/ContainerizedApiMinimal/", "src/ContainerizedApiMinimal/"]
COPY ["src/ContainerHealthProbe/", "src/ContainerHealthProbe/"]

RUN dotnet publish \
"src/ContainerizedApiMinimal/ContainerizedApiMinimal.csproj" \
--configuration Release \
--no-restore \
--output /out/app \
/p:UseAppHost=false \
&& dotnet publish \
"src/ContainerHealthProbe/ContainerHealthProbe.csproj" \
--configuration Release \
--no-restore \
--output /out/probe \
/p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble AS final
WORKDIR /app

ENV ASPNETCORE_HTTP_PORTS=8080

COPY --from=publish \
--chown=app:app \
/out/app/ \
./

COPY --from=publish \
--chown=app:app \
/out/probe/ \
./

USER $APP_UID

EXPOSE 8080

HEALTHCHECK \
--interval=30s \
--timeout=5s \
--start-period=10s \
--retries=3 \
CMD ["dotnet", "ContainerHealthProbe.dll"]

ENTRYPOINT ["dotnet", "ContainerizedApiMinimal.dll"]
Loading
Loading