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
482 changes: 3 additions & 479 deletions .gitignore

Large diffs are not rendered by default.

90 changes: 64 additions & 26 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,35 +1,73 @@
# - Stage 1 --------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Stage 1: Builder
# This stage builds the application and its dependencies.
# ------------------------------------------------------------------------------
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS builder

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
WORKDIR /src

# Copy and restore dependencies
COPY src/Dotnet.Samples.AspNetCore.WebApi/*.csproj ./Dotnet.Samples.AspNetCore.WebApi/
RUN dotnet restore ./Dotnet.Samples.AspNetCore.WebApi
# Restore dependencies
COPY src/Dotnet.Samples.AspNetCore.WebApi/*.csproj ./Dotnet.Samples.AspNetCore.WebApi/
RUN dotnet restore ./Dotnet.Samples.AspNetCore.WebApi

# Copy source and publish
COPY src/Dotnet.Samples.AspNetCore.WebApi ./Dotnet.Samples.AspNetCore.WebApi
WORKDIR /src/Dotnet.Samples.AspNetCore.WebApi
RUN dotnet publish -c Release -o /app/publish
# Copy source code and pre-seeded SQLite database
COPY src/Dotnet.Samples.AspNetCore.WebApi ./Dotnet.Samples.AspNetCore.WebApi

# - Stage 2 --------------------------------------------------------------------
WORKDIR /src/Dotnet.Samples.AspNetCore.WebApi

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
# Build solution and publish release
RUN dotnet publish -c Release -o /app/publish

# Copy published output
# Note: This includes the SQLite database because it's marked as <Content> with
# <CopyToOutputDirectory> in the .csproj file. No need to copy it manually.
COPY --from=build /app/publish .
# ------------------------------------------------------------------------------
# Stage 2: Runtime
# This stage creates the final, minimal image to run the application.
# ------------------------------------------------------------------------------
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime

# Add non-root user (aspnetcore) for security hardening
RUN adduser --disabled-password --gecos '' aspnetcore \
&& chown -R aspnetcore:aspnetcore /app
USER aspnetcore
WORKDIR /app

# Set environment variables
ENV ASPNETCORE_URLS=http://+:9000
ENV ASPNETCORE_ENVIRONMENT=Production
# Install curl for health check
RUN apt-get update && apt-get install -y --no-install-recommends curl && \

Check warning on line 30 in Dockerfile

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Dockerfile#L30

Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
rm -rf /var/lib/apt/lists/*

# Default entrypoint
ENTRYPOINT ["dotnet", "Dotnet.Samples.AspNetCore.WebApi.dll"]
# Metadata labels for the image. These are useful for registries and inspection.
LABEL org.opencontainers.image.title="🧪 Web API made with .NET 8 (LTS) and ASP.NET Core"
LABEL org.opencontainers.image.description="Proof of Concept for a Web API made with .NET 8 (LTS) and ASP.NET Core"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.source="https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi"

# Set environment variables
ENV ASPNETCORE_URLS=http://+:9000
ENV ASPNETCORE_ENVIRONMENT=Production

# Copy published app from builder
COPY --from=builder /app/publish .

# Copy metadata docs for container registries (e.g.: GitHub Container Registry)
COPY --chmod=444 README.md ./
COPY --chmod=555 assets ./assets

# https://rules.sonarsource.com/docker/RSPEC-6504/

# Copy entrypoint and healthcheck scripts
COPY --chmod=555 scripts/entrypoint.sh ./entrypoint.sh
COPY --chmod=555 scripts/healthcheck.sh ./healthcheck.sh


# Copy pre-seeded SQLite database as init bundle
COPY --from=builder /src/Dotnet.Samples.AspNetCore.WebApi/storage/players-sqlite3.db ./docker-compose/players-sqlite3.db

# Create non-root user and make volume mount point writable
RUN adduser --disabled-password --gecos '' aspnetcore && \
mkdir -p /storage && \
chown -R aspnetcore:aspnetcore /storage

USER aspnetcore

HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD ["./healthcheck.sh"]

EXPOSE 9000

ENTRYPOINT ["./entrypoint.sh"]
CMD ["dotnet", "Dotnet.Samples.AspNetCore.WebApi.dll"]
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,39 @@ https://localhost:9000/swagger/index.html

## Container

This project includes a multi-stage `Dockerfile` for local development and production builds.
### Docker Compose

### Build the image
This setup uses [Docker Compose](https://docs.docker.com/compose/) to build and run the app and manage a persistent SQLite database stored in a Docker volume.

#### Build the image

```bash
docker compose build
```

#### Start the app

```bash
docker build -t aspnetcore-app .
docker compose up
```

### Run the container
> On first run, the container copies a pre-seeded SQLite database into a persistent volume
> On subsequent runs, that volume is reused and the data is preserved

#### Stop the app

```bash
docker run -p 9000:9000 aspnetcore-app
docker compose down
```

#### Optional: database reset

```bash
docker compose down -v
```

> This removes the volume and will reinitialize the database from the built-in seed file the next time you `up`.

## Credits

The solution has been coded using [Visual Studio Code](https://code.visualstudio.com/) with the [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) extension.
Expand Down
17 changes: 17 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
services:
api:
image: dotnet-samples-aspnetcore-webapi
container_name: aspnetcore-app
build:
context: .
dockerfile: Dockerfile
ports:
- "9000:9000"
volumes:
- storage:/storage/
environment:
- STORAGE_PATH=/storage/players-sqlite3.db
restart: unless-stopped

volumes:
storage:
25 changes: 25 additions & 0 deletions scripts/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/sh
set -e

IMAGE_STORAGE_PATH="/app/docker-compose/players-sqlite3.db"
VOLUME_STORAGE_PATH="/storage/players-sqlite3.db"

echo "✔ Starting container..."

if [ ! -f "$VOLUME_STORAGE_PATH" ]; then
echo "⚠️ No existing database file found in volume."
if [ -f "$IMAGE_STORAGE_PATH" ]; then
echo "🔄 Copying database file to writable volume..."
cp "$IMAGE_STORAGE_PATH" "$VOLUME_STORAGE_PATH"
echo "✔ Database initialized at $VOLUME_STORAGE_PATH"
else
echo "⚠️ Database file missing at $IMAGE_STORAGE_PATH"
exit 1
fi
else
echo "✔ Existing database file found. Skipping seed copy."
fi

echo "✔ Ready!"
echo "🚀 Launching app..."
exec "$@"
5 changes: 5 additions & 0 deletions scripts/healthcheck.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
set -e

# Simple health check using curl
curl --fail http://localhost:9000/health
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
</ItemGroup>

<ItemGroup>
<Content Include="Data/players-sqlite3.db" Condition="Exists('Data/players-sqlite3.db')">
<Content Include="storage/players-sqlite3.db">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
</Content>
Expand Down
8 changes: 7 additions & 1 deletion src/Dotnet.Samples.AspNetCore.WebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
/* Entity Framework Core ---------------------------------------------------- */
builder.Services.AddDbContextPool<PlayerDbContext>(options =>
{
var dataSource = Path.Combine(AppContext.BaseDirectory, "Data", "players-sqlite3.db");
var dataSource = Path.Combine(AppContext.BaseDirectory, "storage", "players-sqlite3.db");

options.UseSqlite($"Data Source={dataSource}");

if (builder.Environment.IsDevelopment())
{
options.EnableSensitiveDataLogging();
Expand All @@ -48,6 +50,7 @@
builder.Services.AddScoped<IPlayerRepository, PlayerRepository>();
builder.Services.AddScoped<IPlayerService, PlayerService>();
builder.Services.AddMemoryCache();
builder.Services.AddHealthChecks();

/* AutoMapper --------------------------------------------------------------- */
builder.Services.AddAutoMapper(typeof(PlayerMappingProfile));
Expand Down Expand Up @@ -91,4 +94,7 @@
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing#endpoints
app.MapControllers();

// https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks
app.MapHealthChecks("/health");

await app.RunAsync();
Loading