I built a self-hosted deployment platform that automatically deploys Docker apps, provisions nginx and SSL, and places overdue client projects behind a payment paywall that disappears automatically after payment.
Important context: This tool is designed for projects where the client has explicitly agreed to service suspension for overdue invoices in their contract. It is not a hostage mechanism—it is an automated enforcement of agreed payment terms, with instant restoration upon payment. Transparency with clients upfront is non-negotiable.
The problem: I hate chasing clients for money
If you freelance or run a small agency, you know the drill. You build a client's website or API, deploy it, and then the invoice goes out. The client loves the product. They use it every day. But when the payment date comes around, they go quiet.
I am not good at arguing with people about money. I do not enjoy the awkward back-and-forth of "hey, just checking in on that invoice" followed by three days of silence. I wanted a system that handled this for me, automatically, without me having to be the bad guy.
The second problem: deploying apps is a fight
Every time I needed to deploy a new client project, I had to:
- Pull the Docker image manually
- Create a container with the right ports, env vars, and volumes
- Write an nginx config by hand
- Get the SSL certificate set up
- Test the config, reload nginx, and pray nothing broke
- Repeat the whole process for the next client
It was tedious, error-prone, and every project had its own quirks.
| Manual deployment | Gatekeeperd |
|---|---|
| Docker CLI commands | Validated wizard flow |
| Hand-written nginx configs | Auto-generated + nginx -t safety check |
| Manual certbot runs | One API call with wildcard reuse |
| Chasing invoices via email | Automatic 402 paywall |
| Manual unblocking after payment | Paystack webhook auto-restores access |
What I built: Gatekeeperd
Gatekeeperd is a Ktor application written in Kotlin that runs on your VPS and gives you a JWT-protected API for the entire client project lifecycle.
Architecture
┌─────────────────┐
│ React Dashboard │
└────────┬────────┘
│ HTTPS + JWT
┌────────▼────────┐ ┌──────────┐
│ Gatekeeperd API │────▶│ Redis │ (gate status cache, 60s TTL)
│ (Kotlin/Ktor) │ └──────────┘
└───┬────┬────┬───┘ ┌──────────┐
│ │ │ │ PostgreSQL│ (projects, payments, audit log)
│ │ │ └──────────┘
│ │ └────────▶ Certbot (Let's Encrypt)
│ └─────────────▶ nginx (reverse proxy + auth_request gating)
└──────────────────▶ Docker Engine (containers, networks, images)
└────────▶ Paystack (payments + webhooks)
See it in action

① Client site is live and fully accessible. Traffic flows through nginx to the container.

② One click in the admin dashboard blocks the project. nginx immediately serves the 402 paywall instead of proxying to the container.

③ Client clicks Pay Now → Paystack checkout opens. On success, the webhook fires, Redis cache clears, and the site auto-unblocks. No emails sent. No manual intervention.
The deployment engine
Instead of three separate manual workflows, Gatekeeperd unifies Docker, nginx, and SSL into a single validated pipeline. Each step is a discrete API call, orchestrated through the dashboard's wizard flow.
Step 1: Validate before you mutate
Before creating anything, the wizard calls POST /api/docker/validate to catch problems early:
POST /api/docker/validate
Content-Type: application/json
{
"image": "ghcr.io/mikesplore/carwash-api:1.0.0",
"hostPort": 9921,
"containerPort": 8080,
"network": "gatekeeper-internal"
}
{
"imageExists": true,
"portAvailable": true,
"networkAvailable": true,
"valid": true
}
No surprises at create time. If the image doesn't exist or the port is taken, you know before you've spun up half a stack.
Step 2: Container lifecycle
The Docker integration covers the full lifecycle:
- Pull images from any registry (including private Docker Hub images via CLI)
- Create containers with port mappings, env vars, volume mounts, restart policies, and custom networks
- List, inspect, start, stop, restart, and delete containers
- Auto-create the internal
gatekeeper-internalnetwork on first run
Step 3: nginx config generation
This was the part I hated doing by hand. Now it is one API call per site.
Gatekeeperd generates a complete nginx site config via POST /api/nginx/sites that includes:
-
auth_requestgating that callsGET /api/gate/checkon every request - A paywall fallback location that serves the 402 page when the auth subrequest returns 403
- A bypass route for
/api/gate/so payment webhooks always reach the API - Proper proxy headers for WebSockets and real client IPs
Every config is validated with nginx -t before reloading via systemctl. If validation fails, the operation aborts and no changes are made. The service also auto-infers the upstream port from the container name (e.g., myapp:9921 → proxy to 9921).
Step 4: SSL automation
Certificates are handled through certbot via POST /api/ssl/certificates. The smart part is certificate resolution: the system checks for exact matches first, then reuses wildcard/parent domain certificates (e.g., example.com covers acw.example.com). Custom certificate paths and requireSsl enforcement are also supported.
The payment gate
This is the core of the platform. Every project has a status: ACTIVE or BLOCKED.
When a client is active, GET /api/gate/check returns 200 OK and nginx proxies traffic to the container. When they are overdue, it returns 403 Forbidden and nginx serves the paywall instead.
The paywall is self-service. The client sees their project name, the amount due, and a Pay Now button that redirects to Paystack. When payment succeeds, POST /api/payments/webhook fires, the project status flips to ACTIVE, and the Redis cache is invalidated instantly.
An auto-blocker runs on a schedule: if a project's due date passes and the grace period expires, it gets blocked automatically. If a payment is later reversed, the same webhook endpoint processes the chargeback event, re-blocks the project, and restores the due date. (Note: Reversal handling is implemented but not yet documented in the public API docs. Update pending.)
Lessons learned
Why nginx auth_request instead of app-level middleware?
Middleware would require modifying every client app. auth_request keeps the gating logic entirely in the reverse proxy layer. Client apps stay untouched—they don't even know Gatekeeperd exists. This means I can gate a Rails app, a Node API, or a static site with zero code changes on the client side.
Why Redis for gate status?
Every HTTP request to a gated site hits GET /api/gate/check. Without caching, that's a PostgreSQL query per request per client. Redis with a 60-second TTL absorbs the read load while keeping block/unblock propagation fast enough that clients see changes within a minute.
Why shell out to Docker CLI instead of using the Docker Engine API?
The Docker Engine API is comprehensive but verbose. For private registry pulls with credential helpers, the CLI handles auth flow, credential storage, and platform negotiation out of the box. Shelling out to docker pull was simpler and more reliable than reimplementing credential helper support in Kotlin. Trade-off: slightly slower, but deployment is not a hot path.
Why Ktor?
I needed a lightweight, coroutine-native HTTP framework that could handle long-running Docker operations without blocking threads. Ktor's routing DSL made the API surface readable, and its test infrastructure let me mock nginx/Docker interactions cleanly. Coming from Django, the explicitness of Ktor was refreshing—no magic, no implicit middleware chains.
Certbot automation gotchas
Certbot's --nginx plugin assumes it owns the nginx config. Since Gatekeeperd generates configs programmatically, I had to use standalone/webroot modes and manage certificate paths manually. Also, rate limits are real: staging certificates during development saved me from hitting Let's Encrypt's production limits repeatedly.
Webhook reliability
Paystack webhooks can arrive out of order or duplicate. Idempotency keys on payment records prevent double-unblocking. Signature verification on every webhook payload is non-negotiable—never trust the source IP alone.
Try it yourself
Gatekeeperd is completely open source. If you're interested in self-hosted deployment platforms, automated client hosting, or payment-gated SaaS infrastructure, I'd love feedback and contributions.
👉 GitHub: github.com/mikesplore/gatekeeperd
The stack: Kotlin + Ktor, PostgreSQL, Redis, Docker, nginx, certbot, Paystack, and a React admin dashboard. Deploy it on any VPS where you have root access and Docker installed.
Built by a freelancer who got tired of chasing invoices and writing nginx configs at 2 AM. May your deployments be boring and your payments automatic. 🛠️
Top comments (0)