Docker Docker Compose .NET 10 PostgreSQL DevOps

From Zero to Docker: Containerizing a .NET Payment Platform

AL
Azim Litanga
· April 15, 2025 · ⏱ 9 min read
The PayCore Series — Article 3 of 4 · logicdynamics.net

"Works on my machine" is not a deployment strategy.

PayCore had a beautiful Blazor dashboard, a working payment engine, an AI-powered MCP server, and a PostgreSQL database full of real payment data. All of it ran perfectly on my Windows development machine. None of it could be deployed to a server without a manual, error-prone setup process.

Docker Compose changed that. This is the story of how PayCore went from a development environment to a one-command deployment.

# PayCore's full stack — one command:
docker compose up --build

# What starts:
# paycore-postgres    ← PostgreSQL 16
# paycore-api         ← PayCore.Api (ASP.NET Core)
# paycore-console     ← PayCore.Console (Blazor WASM)
# paycore-mcp         ← PayCore.Mcp (MCP Server)

What Docker actually does

Docker packages your application and everything it needs — the runtime, the dependencies, the configuration — into a container. A container runs the same way on any machine that has Docker installed.

Docker Compose takes this further: it lets you define multiple containers and their relationships in a single YAML file, then start them all with one command.

Multi-stage Dockerfiles: build small, ship fast

A .NET application needs the full SDK to compile but only the runtime to run. Multi-stage builds use the SDK image for compilation and a smaller runtime image for the final container:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY PayCore.Core/PayCore.Core.csproj           PayCore.Core/
COPY PayCore.Infrastructure/PayCore.Infrastructure.csproj PayCore.Infrastructure/
COPY PayCore.Console/Shared/PayCore.Console.Shared.csproj PayCore.Console/Shared/
COPY PayCore.Api/PayCore.Api.csproj             PayCore.Api/
RUN dotnet restore PayCore.Api/PayCore.Api.csproj
COPY . .
RUN dotnet publish PayCore.Api/PayCore.Api.csproj -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "PayCore.Api.dll"]

The final image is ~200MB instead of ~800MB because the SDK stays in the build stage — only the runtime ships.

The three bugs Docker revealed

Containerizing PayCore exposed three issues that only exist in Docker — not in local development.

🐛 Bug #1: Console.Shared missing from API Dockerfile

PayCore.Api references PayCore.Console.Shared for endpoint models. The Dockerfile didn't COPY it into the build context.

Error: "The type or namespace Console does not exist in namespace PayCore"

Fix: Add COPY PayCore.Console/Shared/PayCore.Console.Shared.csproj to Dockerfile.api

🐛 Bug #2: The localhost networking problem

Inside Docker, "localhost" means the container itself — not the host machine. PayCore.Console was calling PayCore.Api at https://localhost:7060. Inside Docker, that address doesn't exist.

Fix: Environment variable override in docker-compose.yml: PayCoreApi__BaseUrl: http://paycore-api:7060. Docker Compose creates a shared network where services reach each other by name.

🐛 Bug #3: Fresh container, empty database

The Docker PostgreSQL container starts with no tables. PayCore.Api was failing with "42P01: relation PaymentIntents does not exist".

Fix: Auto-migration on startup: db.Database.Migrate() — idempotent and safe to call every time.

The docker-compose.yml: annotated

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB:       paycore
      POSTGRES_PASSWORD: LDServer
    ports: ["5433:5432"]   # 5433 avoids clash with local Postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 10

  paycore-api:
    build: { context: ., dockerfile: Dockerfile.api }
    environment:
      ASPNETCORE_URLS: http://+:7060
      ConnectionStrings__PayCoreDb: Host=postgres;Port=5432;...
    depends_on:
      postgres: { condition: service_healthy }  # waits for DB

  paycore-console:
    build: { context: ., dockerfile: Dockerfile.console }
    environment:
      PayCoreApi__BaseUrl: http://paycore-api:7060  # service name!
    depends_on: [paycore-api]

  paycore-mcp:
    build: { context: ., dockerfile: Dockerfile.mcp }
    environment:
      ConnectionStrings__PayCoreDb: Host=postgres;Port=5432;...
    depends_on:
      postgres: { condition: service_healthy }
💡 depends_on with service_healthy Without the health check condition, the API container starts before PostgreSQL is ready to accept connections and crashes immediately. Always use service_healthy when a service depends on a database.

The moment it worked

After fixing the three bugs and running docker compose up --build, the PayCore dashboard appeared at http://localhost:7239:

✅ TOTAL PROCESSED: $21,319 USD ✅ TOTAL PAYMENTS: 120 ✅ ACTIVE PROVIDERS: 4 (Stripe, PayPal, M-PESA, Airtel) ✅ LIVE ● LIVE ● LIVE (all three SignalR connections established) ✅ Provider Health: Stripe 99.98% · PayPal 99.91% · M-PESA 99.87% · Airtel 99.92% ✅ Latency Trends rendering for all four providers

Four containers. One command. The entire PayCore stack — API, dashboard, MCP server, and PostgreSQL — running in perfect orchestration.

"Docker didn't just solve the deployment problem. It revealed three real bugs — a missing project reference, a networking assumption, and an empty database. Every one of them would have caused a production incident without Docker forcing them into the open."

The Docker cheat sheet

# Start everything (build images first):
docker compose up --build

# Start in background:
docker compose up -d

# Stop everything (keep database volume):
docker compose down

# Stop and wipe database (fresh start):
docker compose down -v

# See running containers:
docker ps

# See logs for one service:
docker compose logs paycore-api
docker compose logs paycore-console
← Previous
Teaching Claude to Query My Payment Database
Next →
Why African Mobile Money Made Me a Better Payment Engineer
AL
Azim Litanga

Founder of LogicDynamics — .NET engineer, builder, and creator. Passionate about Blazor, Web APIs, AI, and shipping real software. Based in Oklahoma City.