"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.
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
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.
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 }
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:
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