Skip to content

Commit 91551e5

Browse files
committed
Replace nginx with ASP.NET Core for static file serving
- Add server/ directory with .NET 10 Aspire solution: - Docs.Web: ASP.NET Core app serving static files with caching and redirects - Docs.AppHost: .NET Aspire orchestrator for local development - Docs.ServiceDefaults: Shared OpenTelemetry and health check configuration - Update Dockerfile to multi-stage build (Node -> .NET SDK -> .NET runtime) - Replicate nginx behavior: trailing slash redirects, cache headers, 404 handling - Update .gitignore with .NET patterns - Update README with Aspire and Docker development instructions Uses Aspire.AppHost.Sdk 13.1.0 and .NET 10.0
1 parent 25e87bc commit 91551e5

16 files changed

Lines changed: 525 additions & 22 deletions

.gitignore

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,41 @@ pnpm-debug.log*
2828
opencode.json
2929
.idea/**/copilot.data.migration.*.xml
3030

31-
# ides
31+
# .NET
32+
*.user
33+
*.suo
34+
*.userosscache
35+
*.sln.docstates
36+
[Bb]in/
37+
[Oo]bj/
38+
[Ll]og/
39+
[Ll]ogs/
3240
.vs/
41+
*.nupkg
42+
*.snupkg
43+
project.lock.json
44+
project.fragment.lock.json
45+
artifacts/
46+
47+
# Rider
48+
.idea/**/workspace.xml
49+
.idea/**/tasks.xml
50+
.idea/**/usage.statistics.xml
51+
.idea/**/dictionaries
52+
.idea/**/shelf
53+
54+
# User-specific files
55+
*.rsuser
56+
57+
# JetBrains Rider
58+
*.sln.iml
59+
60+
# ASP.NET Scaffolding
61+
ScaffoldingReadMe.txt
62+
63+
# Windows
64+
Thumbs.db
65+
ehthumbs.db
66+
67+
# Azure Functions
68+
local.settings.json

Dockerfile

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
1-
FROM node:22-alpine AS builder
2-
3-
EXPOSE 8080
1+
# Stage 1: Build Astro static site
2+
FROM node:22-alpine AS astro-builder
43

54
WORKDIR /app
65
COPY package.json package-lock.json ./
7-
RUN npm install --no-cache
6+
RUN npm ci
87
COPY . .
9-
RUN chmod +x build.sh
8+
RUN chmod +x build.sh && ./build.sh
9+
10+
# Stage 2: Build ASP.NET Core server
11+
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS dotnet-builder
1012

11-
RUN ./build.sh
13+
WORKDIR /src
14+
COPY server/ ./server/
15+
WORKDIR /src/server
16+
RUN dotnet restore Docs.slnx
17+
RUN dotnet publish Docs.Web/Docs.Web.csproj -c Release -o /app/publish --no-restore
1218

13-
FROM nginx:stable-alpine AS final
19+
# Stage 3: Final runtime image
20+
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
1421

15-
ARG APPLICATION_VERSION
16-
COPY deployment/nginx.conf /etc/nginx/nginx.conf
17-
COPY deployment/default.conf /etc/nginx/conf.d/default.conf
18-
RUN sed -i "s|\$APPLICATION_VERSION|${APPLICATION_VERSION}|g" /etc/nginx/conf.d/default.conf
19-
COPY --from=builder --chown=nginx:nginx /app/root/redirect.conf /etc/nginx/extra/redirect.conf
22+
EXPOSE 8080
23+
24+
WORKDIR /app
2025

21-
RUN rm -rf /usr/share/nginx/html/*
22-
COPY --from=builder --chown=nginx:nginx /app/root/ /usr/share/nginx/html/
23-
RUN rm -rf /usr/share/nginx/html/redirect.conf
26+
# Copy ASP.NET Core app
27+
COPY --from=dotnet-builder /app/publish .
2428

25-
RUN mkdir -p /var/cache/nginx
29+
# Copy Astro static files to wwwroot
30+
COPY --from=astro-builder /app/root/ ./wwwroot/
2631

27-
RUN chown -R nginx:nginx /var/cache/nginx
28-
RUN touch /var/run/nginx.pid && \
29-
chown -R nginx:nginx /var/run/nginx.pid
32+
# Set environment variables
33+
ENV ASPNETCORE_URLS=http://+:8080
34+
ENV ASPNETCORE_ENVIRONMENT=Production
3035

31-
USER nginx
36+
USER $APP_UID
3237

33-
WORKDIR /usr/share/nginx/html
38+
ENTRYPOINT ["dotnet", "Docs.Web.dll"]

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,49 @@ You will need Node 22+ installed on your operating system and available in the P
1111

1212
Alternatively in VS Code, GitHub Codespaces, or WebStorm, you can use the devcontainer to get a development environment set up.
1313

14+
## Development with .NET Aspire
15+
16+
For local development with the full stack (Astro dev server + ASP.NET Core), you can use .NET Aspire:
17+
18+
### Prerequisites
19+
20+
* [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
21+
* Node.js 22+
22+
23+
### Running with Aspire
24+
25+
```bash
26+
cd server
27+
dotnet run --project Docs.AppHost
28+
```
29+
30+
This starts:
31+
- **Astro dev server** at http://localhost:4321 (with hot reload)
32+
- **ASP.NET Core server** (for production-like static file serving)
33+
- **Aspire Dashboard** at https://localhost:17001 (for traces, logs, metrics)
34+
35+
## Docker
36+
37+
### Building the Docker image
38+
39+
```bash
40+
docker build -t docs-duende .
41+
```
42+
43+
### Running the Docker container
44+
45+
```bash
46+
docker run -p 8080:8080 docs-duende
47+
```
48+
49+
The site will be available at http://localhost:8080.
50+
51+
### Building with version tag
52+
53+
```bash
54+
docker build --build-arg APPLICATION_VERSION=1.0.0 -t docs-duende .
55+
```
56+
1457
## Project Structure
1558

1659
This project uses Astro + Starlight. You'll see the following folders and files:
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Aspire.AppHost.Sdk/13.1.0">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<IsAspireHost>true</IsAspireHost>
9+
<UserSecretsId>b4f58b37-4c66-48e3-8e8a-9c4f7f1e8c5f</UserSecretsId>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
14+
<PackageReference Include="Aspire.Hosting.JavaScript" Version="13.1.0" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<ProjectReference Include="..\Docs.Web\Docs.Web.csproj" />
19+
</ItemGroup>
20+
21+
</Project>

server/Docs.AppHost/Program.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
var builder = DistributedApplication.CreateBuilder(args);
2+
3+
// Astro dev server (for local development only)
4+
var astro = builder.AddJavaScriptApp("astro", "../", "dev")
5+
.WithHttpEndpoint(port: 4321, env: "PORT")
6+
.WithExternalHttpEndpoints();
7+
8+
// ASP.NET Core static file server
9+
var web = builder.AddProject<Projects.Docs_Web>("web");
10+
11+
builder.Build().Run();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"https": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": true,
8+
"applicationUrl": "https://localhost:17001;http://localhost:17000",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development",
11+
"DOTNET_ENVIRONMENT": "Development",
12+
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21001",
13+
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22001"
14+
}
15+
},
16+
"http": {
17+
"commandName": "Project",
18+
"dotnetRunMessages": true,
19+
"launchBrowser": true,
20+
"applicationUrl": "http://localhost:17000",
21+
"environmentVariables": {
22+
"ASPNETCORE_ENVIRONMENT": "Development",
23+
"DOTNET_ENVIRONMENT": "Development",
24+
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19001",
25+
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20001",
26+
"ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
27+
}
28+
}
29+
}
30+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning",
6+
"Aspire.Hosting.Dcp": "Warning"
7+
}
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning",
6+
"Aspire.Hosting.Dcp": "Warning"
7+
}
8+
}
9+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsAspireSharedProject>true</IsAspireSharedProject>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
12+
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.6.0" />
13+
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.2.0" />
14+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
15+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
16+
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
17+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
18+
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
19+
</ItemGroup>
20+
21+
</Project>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using Microsoft.AspNetCore.Builder;
2+
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Diagnostics.HealthChecks;
5+
using Microsoft.Extensions.Logging;
6+
using OpenTelemetry;
7+
using OpenTelemetry.Metrics;
8+
using OpenTelemetry.Trace;
9+
10+
namespace Microsoft.Extensions.Hosting;
11+
12+
// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
13+
// This project should be referenced by each service project in your solution.
14+
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
15+
public static class Extensions
16+
{
17+
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
18+
{
19+
builder.ConfigureOpenTelemetry();
20+
21+
builder.AddDefaultHealthChecks();
22+
23+
builder.Services.AddServiceDiscovery();
24+
25+
builder.Services.ConfigureHttpClientDefaults(http =>
26+
{
27+
// Turn on resilience by default
28+
http.AddStandardResilienceHandler();
29+
30+
// Turn on service discovery by default
31+
http.AddServiceDiscovery();
32+
});
33+
34+
return builder;
35+
}
36+
37+
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
38+
{
39+
builder.Logging.AddOpenTelemetry(logging =>
40+
{
41+
logging.IncludeFormattedMessage = true;
42+
logging.IncludeScopes = true;
43+
});
44+
45+
builder.Services.AddOpenTelemetry()
46+
.WithMetrics(metrics =>
47+
{
48+
metrics.AddAspNetCoreInstrumentation()
49+
.AddHttpClientInstrumentation()
50+
.AddRuntimeInstrumentation();
51+
})
52+
.WithTracing(tracing =>
53+
{
54+
tracing.AddSource(builder.Environment.ApplicationName)
55+
.AddAspNetCoreInstrumentation()
56+
.AddHttpClientInstrumentation();
57+
});
58+
59+
builder.AddOpenTelemetryExporters();
60+
61+
return builder;
62+
}
63+
64+
private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
65+
{
66+
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
67+
68+
if (useOtlpExporter)
69+
{
70+
builder.Services.AddOpenTelemetry().UseOtlpExporter();
71+
}
72+
73+
return builder;
74+
}
75+
76+
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
77+
{
78+
builder.Services.AddHealthChecks()
79+
// Add a default liveness check to ensure app is responsive
80+
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
81+
82+
return builder;
83+
}
84+
85+
public static WebApplication MapDefaultEndpoints(this WebApplication app)
86+
{
87+
// Adding health checks endpoints to applications in non-development environments has security implications.
88+
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
89+
if (app.Environment.IsDevelopment())
90+
{
91+
// All health checks must pass for app to be considered ready to accept traffic after starting
92+
app.MapHealthChecks("/health");
93+
94+
// Only health checks tagged with the "live" tag must pass for app to be considered alive
95+
app.MapHealthChecks("/alive", new HealthCheckOptions
96+
{
97+
Predicate = r => r.Tags.Contains("live")
98+
});
99+
}
100+
101+
return app;
102+
}
103+
}

0 commit comments

Comments
 (0)