Skip to content

Deployment ​

SimpleModule applications deploy as standard ASP.NET applications. This guide covers Docker deployment, the CI/CD pipeline, and production configuration.

Docker ​

Dockerfile ​

The project includes a multi-stage Dockerfile that produces a minimal runtime image. The snippet below is a simplified reference — see the repository's actual Dockerfile for the full script, which lists every module's .csproj and workspace package.json explicitly for optimal layer caching:

dockerfile
# Stage 1: Base — .NET SDK + Node.js 22
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS base
WORKDIR /src
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
    && apt-get install -y --no-install-recommends nodejs \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# Stage 2: .NET restore (cached unless .csproj / props files change)
FROM base AS restore
WORKDIR /src
COPY global.json Directory.Build.props Directory.Packages.props .editorconfig ./
COPY *.slnx ./
# Copy every framework/module/package .csproj individually for layer caching
# ... (see real Dockerfile for the full list) ...
RUN dotnet restore template/SimpleModule.Host/SimpleModule.Host.csproj

# Stage 3: npm restore (cached unless package.json / lockfile changes)
FROM restore AS npm-restore
WORKDIR /src
COPY package.json package-lock.json ./
# Copy every workspace package.json individually for layer caching
# ... (see real Dockerfile for the full list) ...
RUN npm ci

# Stage 4: Frontend build + .NET publish
FROM npm-restore AS build
WORKDIR /src
COPY . .
RUN npm run build \
    && npx @tailwindcss/cli \
       -i template/SimpleModule.Host/Styles/app.css \
       -o template/SimpleModule.Host/wwwroot/css/app.css \
       --minify
RUN dotnet publish template/SimpleModule.Host/SimpleModule.Host.csproj \
    -c Release -o /app/publish --no-restore \
    -p:ErrorOnDuplicatePublishOutputFiles=false \
    -p:JsBuildCommand=echo

# Stage 5: Runtime (slim image, non-root user)
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && apt-get clean && rm -rf /var/lib/apt/lists/* \
    && groupadd --system --gid 1001 appgroup \
    && useradd --system --uid 1001 --gid appgroup --create-home appuser
COPY --from=build --chown=appuser:appgroup /app/publish .
RUN mkdir -p /app/data /app/storage && chown appuser:appgroup /app/data /app/storage

# DEPLOY_VERSION feeds Inertia's asset-version header; if empty, the framework
# falls back to the entry assembly's last-write timestamp.
ARG DEPLOY_VERSION=
ENV DEPLOYMENT_VERSION=${DEPLOY_VERSION}

USER appuser
EXPOSE 8080
ENTRYPOINT ["dotnet", "SimpleModule.Host.dll"]

Key points:

  • Five stages -- base (SDK + Node 22), restore (.NET restore), npm-restore (workspace npm ci), build (frontend + dotnet publish), runtime (slim aspnet image)
  • Non-root runtime -- creates appuser (UID 1001) and runs the container as that user; owns /app/data (SQLite) and /app/storage (local files)
  • Per-project COPY for layer caching -- every .csproj and workspace package.json is copied individually so dotnet restore and npm ci only re-run when those manifests change
  • DEPLOY_VERSION build arg -- feeds DEPLOYMENT_VERSION for Inertia cache-busting; override with --build-arg DEPLOY_VERSION=$(git rev-parse --short HEAD) for deterministic versions
  • The runtime container exposes port 8080

Docker Compose ​

For local testing or simple deployments, use docker-compose.yml. The real compose file defines three services — api, worker, and postgres — so background jobs run in a dedicated consumer process while the web tier stays in producer mode:

yaml
services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      ASPNETCORE_ENVIRONMENT: Development
      Database__DefaultConnection: "Host=postgres;Port=5432;Database=simplemodule;Username=simplemodule;Password=${POSTGRES_PASSWORD:-simplemodule}"
      Database__Provider: PostgreSQL
      OpenIddict__BaseUrl: ${APP_BASE_URL:-http://localhost:8080}
      # api enqueues jobs but never executes them — the worker does.
      BackgroundJobs__WorkerMode: Producer
    volumes:
      - storage_data:/app/storage
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -sf http://localhost:8080/health/live || exit 1"]
      interval: 30s
      timeout: 5s
      start_period: 15s
      retries: 3
    restart: unless-stopped

  worker:
    build:
      context: .
      dockerfile: Dockerfile.worker
    environment:
      DOTNET_ENVIRONMENT: Development
      Database__DefaultConnection: "Host=postgres;Port=5432;Database=simplemodule;Username=simplemodule;Password=${POSTGRES_PASSWORD:-simplemodule}"
      Database__Provider: PostgreSQL
      BackgroundJobs__WorkerMode: Consumer
    volumes:
      - storage_data:/app/storage
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: postgres:17
    environment:
      POSTGRES_USER: simplemodule
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-simplemodule}
      POSTGRES_DB: simplemodule
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U simplemodule"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  pgdata:
  storage_data:

Start with:

bash
docker compose up -d

The API service waits for PostgreSQL to pass its health check before starting. The storage_data volume is shared between api and worker so uploaded files are visible from both the web upload path and the background job path.

CI/CD Pipeline ​

The GitHub Actions workflow (.github/workflows/ci.yml) runs on every push to main and every pull request. It has four jobs:

1. Build ​

build → Restore → Build

Verifies the solution compiles without errors.

2. Test (SQLite) ​

test-sqlite → Run all tests with in-memory SQLite

Runs the full test suite using SQLite (the default provider). This is fast and catches most issues.

3. Test (PostgreSQL) ​

test-postgresql → Run all tests against PostgreSQL service container

Runs the same test suite against a real PostgreSQL 16 instance. This catches provider-specific issues like SQL dialect differences. The job also verifies that EF Core migrations are up to date:

bash
dotnet ef migrations has-pending-model-changes

The connection string is passed via environment variable:

Database__DefaultConnection=Host=localhost;Database=simplemodule_test;Username=test;Password=test

4. Publish ​

publish → dotnet publish -c Release (only on main branch)

Runs only when tests pass on the main branch. Produces the release artifacts.

Pipeline Diagram ​

build ──┬── test-sqlite ────┬── publish (main only)
        └── test-postgresql ─┘

The test jobs run in parallel after build succeeds. Publish runs after both test jobs pass.

Production Build ​

Build a release-optimized application with:

bash
dotnet publish template/SimpleModule.Host/SimpleModule.Host.csproj -c Release

This produces a self-contained deployment package in the default publish directory.

Production Database ​

PostgreSQL is the recommended database for production. Configure it via appsettings.json or environment variables:

Using appsettings.json ​

json
{
  "Database": {
    "DefaultConnection": "Host=your-server;Database=simplemodule;Username=app;Password=secret",
    "Provider": "PostgreSQL"
  }
}

Using Environment Variables ​

Environment variables override appsettings.json. Use the __ (double underscore) separator for nested keys:

bash
export Database__DefaultConnection="Host=your-server;Database=simplemodule;Username=app;Password=secret"
export Database__Provider="PostgreSQL"

WARNING

For production, always use environment variables or a secrets manager for connection strings. Never commit credentials to source control.

Environment Configuration ​

Key Environment Variables ​

VariableDescriptionExample
ASPNETCORE_ENVIRONMENTRuntime environmentProduction, Development
Database__DefaultConnectionDatabase connection stringHost=...;Database=...
Database__ProviderDatabase providerSqlite, PostgreSQL
OpenIddict__BaseUrlPublic base URL used to register OpenIddict redirect URIshttps://app.example.com
BackgroundJobs__WorkerModeRole in the background-job pipelineProducer (web tier) or Consumer (worker tier)

Docker Environment ​

When running in Docker, pass environment variables via docker-compose.yml or docker run:

bash
docker run -d \
  -p 8080:8080 \
  -e ASPNETCORE_ENVIRONMENT=Production \
  -e Database__DefaultConnection="Host=db;Database=simplemodule;Username=app;Password=secret" \
  simplemodule

Database Initialization ​

SimpleModule relies on EF Core migrations per module — there is no EnsureCreated() bootstrap in the framework. Generate and apply migrations for each module's DbContext:

bash
# Add a migration for a specific module's DbContext
dotnet ef migrations add InitialCreate \
  --project modules/Customers/src/Customers \
  --startup-project template/SimpleModule.Host

# Apply migrations
dotnet ef database update \
  --project modules/Customers/src/Customers \
  --startup-project template/SimpleModule.Host

TIP

Each module has its own DbContext and schema. In PostgreSQL, modules use separate schemas for isolation. In SQLite, modules use table prefixes.

Next Steps ​

Released under the MIT License.