DEV Community

Alvaro Lopez
Alvaro Lopez

Posted on

How I provision a real Postgres database in 200ms using .NET 10 and Docker (No K8s allowed)

If you are building integration tests for your backend, you know the drill: you need a database. You don't want a fake in-memory mock, you want the real deal. But booting a Postgres container locally or inside a GitHub Actions runner takes several seconds (sometimes minutes if the image isn't cached).

I got so annoyed by my CI pipelines hanging that I decided to build TrashDB: an API that gives you a remote, ephemeral database on demand. My hard requirement was speed. I wanted a developer to hit my API and get a fully functional database connection string in under 300 milliseconds.

Here is a look under the hood at how I architected the backend to achieve that using .NET 10 and bare-metal Docker.

1. Skipping Kubernetes Entirely

The first temptation when building an infrastructure orchestration tool is to reach for Kubernetes. But K8s is built for high availability and self-healing, not for sub-second ephemeral container provisioning. The control plane overhead alone would kill my 200ms latency budget.

Instead, I went back to basics: a bare-metal VPS running a raw Docker daemon. My .NET 10 API acts as the sole orchestrator. It talks directly to the Docker Engine API via the Unix socket (or Docker HTTP API).

2. The Architecture of a 200ms Boot

To get a container running in milliseconds, you have to ruthlessly cut corners on things you don't need for an ephemeral test environment:

  • Pre-pulled Images: The biggest bottleneck in container startup is pulling layers. The host machine runs a CRON job that ensures the latest postgres:alpine, redis:alpine, and chromadb images are always downloaded and cached.
  • No Volumes: These are throwaway databases. They don't need persistence. By bypassing volume mounts entirely, the Docker daemon doesn't have to deal with filesystem mapping or disk I/O bottlenecks during boot. It creates the container entirely in memory space.
  • Host Networking Bypass: Instead of setting up complex internal Docker networks or reverse proxies (like Traefik or Nginx) which add routing latency, the .NET API simply asks Docker to map the container's internal port to a random open port on the host machine.

3. The .NET 10 Orchestrator

The core logic is surprisingly simple. When an HTTP POST /v1/containers request hits the API:

  1. Validate: The API checks the API key and requested engine.
  2. Port Allocation: It finds a free port on the host.
  3. Docker API Call: It sends a raw request to the local Docker socket to Create and Start a container using the pre-cached alpine image.
  4. TTL Tagging: It applies a label to the container with an expiration timestamp.
  5. Response: It immediately returns the host IP and the dynamically mapped port to the user.

A background BackgroundService (Worker Service) in .NET polls the Docker engine every few seconds, looking for containers whose TTL has expired, and ruthlessly sends a SIGKILL to clean them up.

Keep It Simple, Stupid (KISS)

Building TrashDB reminded me that we often over-engineer our infrastructure. By stripping away persistence, meshes, and clustering, you can push raw Docker to do incredible things incredibly fast.

If you are tired of local Testcontainers melting your CI/CD pipelines, I packaged this entire architecture into a free Alpha. You can grab an API key at trashdb.dev and get your test databases running in 200ms.

Let me know in the comments: what is the weirdest hack you've used to speed up your integration tests?

Top comments (0)