Self-Hosting Tyk Gateway (Open Source) for My Homelab
One front door for all my self-hosted apps — and a clean way to A/B test two versions of the same app with zero downtime
Introduction
Over the last few months I've been slowly turning my old laptop into a proper homelab. I host a handful of small applications on it — my anonymous Ask Me Anything (AMA) app, a bill-splitting app called SplitBillz, and a sample real-estate landing page. Each one runs in its own container on its own port. That works, but it leaves me juggling raw IP:PORT combinations and there's no single place to manage TLS, rate limits, or routing.
So I put an API gateway in front of everything. I chose Tyk Gateway in its fully open-source mode: no dashboard, no license key, just the gateway and a Redis instance. Every app sits behind one front door, each reachable on its own subdomain.
The real motivation, though, was this: I'd just written a second version of my AMA app, and I wanted to run both versions side by side — pointing traffic at whichever one I choose, switching instantly, and keeping the site up the whole time. That's an A/B test and a blue-green deployment rolled into one, and a gateway is the natural place to do it. This article walks through how I set it up.
If you're not yet hosting anything at home, my earlier articles on setting up Docker on Ubuntu and self-hosting an application are good prerequisites.
Why an API Gateway in a Homelab?
A gateway is overkill for a single app. But the moment you have more than one service, a few problems show up that a gateway solves cleanly:
- One entry point: All traffic enters on ports 80/443. The gateway decides where it goes based on the hostname.
- Subdomain routing:
ask.irfansyafi.com,splitbillz.irfansyafi.com, andsample-page.irfansyafi.comall hit the same gateway and get routed to different containers. - Rate limiting & quotas: Protect small homelab services from being hammered, without baking that logic into each app.
- Painless version switching: Swap the upstream target of an app without touching DNS, the app, or the client — the foundation for A/B testing and blue-green deploys.
The Architecture
Here's the shape of what we're building. The gateway is the only thing exposed to the network; the apps stay private on the host.
┌─────────────────────────────┐
ask.irfansyafi.com ───▶ │ │ ──▶ AMA app (:2025)
splitbillz.irfansyafi ───▶ │ Tyk Gateway │ ──▶ SplitBillz (:2029)
sample-page.irfansyafi ──▶ │ (open source) + Redis │ ──▶ Estate page (:3000)
│ │
└─────────────────────────────┘
▲
one front door
(ports 80 / 443, host)
Tyk in OSS mode needs exactly two moving parts: the gateway itself and a Redis instance (Tyk uses Redis for rate-limit counters, quota tracking, and analytics buffering). There is no separate database in the open-source setup — API definitions are loaded from files or pushed over the gateway's REST API.
Step 1: Running Tyk Gateway with Docker Compose
I run the whole thing with Docker Compose so it comes back up after a reboot. Create a folder, say ~/tyk-gateway/, and start with the compose file:
# docker-compose.yml
version: "3.3"
services:
tyk-gateway:
image: tykio/tyk-gateway:v5.3
ports:
- "80:8080" # serve plain HTTP on the host's port 80
volumes:
- ./tyk.conf:/opt/tyk-gateway/tyk.conf
- ./apps:/opt/tyk-gateway/apps # our API definitions live here
environment:
- TYK_GW_SECRET=changeme-please # secret for the gateway REST API
depends_on:
- tyk-redis
extra_hosts:
- "host.docker.internal:host-gateway" # lets the gateway reach apps on the host
tyk-redis:
image: redis:7-alpine
ports:
- "6379:6379"
Two details matter here. First, extra_hosts maps host.docker.internal to the host machine — that's how the gateway, running inside a container, reaches my apps that are listening on the host's ports. You'll see that hostname again in every API definition. Second, the ./apps folder is where Tyk auto-loads API definitions on startup.
Step 2: The Gateway Config (tyk.conf)
Next to the compose file, drop a minimal tyk.conf. The key flag is use_db_app_configs: false, which tells Tyk to load APIs from the app_path folder instead of a dashboard or database — this is what "open source mode" really means in practice.
{
"listen_port": 8080,
"secret": "changeme-please",
"template_path": "/opt/tyk-gateway/templates",
"use_db_app_configs": false,
"app_path": "/opt/tyk-gateway/apps/",
"storage": {
"type": "redis",
"host": "tyk-redis",
"port": 6379,
"database": 0
},
"enable_jsvm": false,
"allow_insecure_configs": true,
"policies": {
"policy_source": "file"
},
"http_server_options": {
"enable_websockets": true
}
}
With use_db_app_configs set to false, every .json file in app_path becomes a live API. Now we just need to write those files.
Step 3: Defining an API — The AMA App
An API definition in Tyk is just JSON describing how to route one hostname to one upstream service. Here's the definition for my AMA app, saved as apps/python-ama-v1.json:
{
"name": "python-ama-v1",
"api_id": "python-ama-v1",
"org_id": "default",
"use_keyless": true,
"active": true,
"listen_port": 0,
"domain": "ask.irfansyafi.com",
"hostname": "ask.irfansyafi.com",
"proxy": {
"listen_path": "/",
"target_url": "http://host.docker.internal:2025",
"strip_listen_path": true
},
"version_data": {
"not_versioned": true,
"versions": {
"Default": { "name": "Default" }
}
},
"use_extended_paths": true,
"disable_quota": false,
"rate": 10,
"per": 60,
"disable_quota_exemption": false,
"quota_max": 10,
"quota_renews": 60,
"quota_remaining": 10
}
Let's unpack the fields that actually do the work:
use_keyless: true— no API key required. These are public web apps, so anyone should be able to reach them.domain/hostname— the gateway only routes a request to this API when the incomingHostheader isask.irfansyafi.com. This is what makes subdomain routing work.proxy.target_url— the upstream. My AMA app is listening on the host's port2025, reached viahost.docker.internal.proxy.listen_path: "/"+strip_listen_path: true— the API owns the whole root path, and the listen path is stripped before forwarding, so the app sees a clean/.rate: 10/per: 60— a 10-requests-per-60-seconds rate limit, applied at the gateway, so a flood never reaches the small app behind it.quota_max/quota_renews— a longer-window request quota on top of the burst rate limit.
Step 4: The Other Two Apps
The beauty of this pattern is that adding an app is just one more JSON file. My other two services are nearly identical — only the name, domain, and target port change. Here's SplitBillz, as apps/splitbillz-v1.json:
{
"name": "splitbillz-v1",
"api_id": "splitbillz-v1",
"org_id": "default",
"use_keyless": true,
"active": true,
"domain": "splitbillz.irfansyafi.com",
"hostname": "splitbillz.irfansyafi.com",
"proxy": {
"listen_path": "/",
"target_url": "http://host.docker.internal:2029",
"strip_listen_path": true
},
"version_data": {
"not_versioned": true,
"versions": { "Default": { "name": "Default" } }
},
"use_extended_paths": true,
"rate": 10,
"per": 60,
"quota_max": 10,
"quota_renews": 60,
"quota_remaining": 10
}
And the estate landing page, as apps/sample-estate-landing-page.json, pointing at port 3000:
{
"name": "estate-landing",
"api_id": "estate-landing",
"org_id": "default",
"use_keyless": true,
"active": true,
"domain": "sample-page.irfansyafi.com",
"hostname": "sample-page.irfansyafi.com",
"proxy": {
"listen_path": "/",
"target_url": "http://host.docker.internal:3000",
"strip_listen_path": true
},
"version_data": {
"not_versioned": true,
"versions": { "Default": { "name": "Default" } }
},
"use_extended_paths": true,
"rate": 10,
"per": 60,
"quota_max": 10,
"quota_renews": 60,
"quota_remaining": 10
}
Three files, three apps, one gateway. Drop them in apps/, run docker compose up -d, and point each subdomain's DNS at the homelab. Every request now flows through Tyk.
Step 5: Loading and Verifying APIs
Because we're in file mode, the gateway loads everything in apps/ at boot. After editing files you can trigger a hot reload via the gateway's REST API instead of restarting — this re-reads the definitions with no downtime:
# Hot-reload all API definitions (no restart, no dropped connections)
curl -H "x-tyk-authorization: changeme-please" \
http://localhost:80/tyk/reload/group
A quick sanity check — pass the Host header explicitly so the gateway knows which API you mean:
curl -H "Host: ask.irfansyafi.com" http://localhost:80/
# Watch the gateway log to confirm which upstream it picked
docker compose logs -f tyk-gateway
Step 6: The Payoff — A/B Testing Two Versions of the AMA App
Here's the whole reason I went down this road. I built a v2 of the AMA app and I want to run it next to v1, deciding exactly which version visitors hit — while the site never goes down.
The trick: the gateway already decouples the public hostname from the upstream port. v1 runs on :2025; I deploy v2 on a different port, say :2026. Both are alive at the same time. The only question is which one ask.irfansyafi.com points to, and that lives entirely in the gateway.
Option A: Instant cut-over (blue-green)
The simplest, most reliable switch: keep one API definition for the public hostname and just change its target_url. v1 is "blue", v2 is "green" — both running, one of them live.
// apps/python-ama.json — flip target_url, then hot-reload
"proxy": {
"listen_path": "/",
"target_url": "http://host.docker.internal:2026", // was :2025 (v1)
"strip_listen_path": true
}
curl -H "x-tyk-authorization: changeme-please" \
http://localhost:80/tyk/reload/group
The reload swaps the upstream between requests, so in-flight traffic isn't dropped. If v2 misbehaves, flipping the port back to :2025 is an instant rollback — no rebuild, no redeploy, the old version was running the whole time. That's the "constant uptime" property I was after.
Option B: Side-by-side preview hostnames
Before flipping production, I like to validate v2 on its own hostname while v1 keeps serving everyone. Two definitions, same pattern as before:
// apps/python-ama-v1.json → ask.irfansyafi.com → :2025 (live)
// apps/python-ama-v2.json → beta.ask.irfansyafi.com → :2026 (preview)
Now I can click through v2 at beta.ask.irfansyafi.com with real traffic conditions, and only promote it to the main hostname (Option A) once I'm happy. This is genuine A/B testing on my own terms — I decide who sees what by which link I share.
Option C: Weighted / true A/B split
For an actual statistical split, Tyk can load-balance across multiple upstreams. By giving the API several targets, a share of requests goes to each version:
"proxy": {
"listen_path": "/",
"strip_listen_path": true,
"enable_load_balancing": true,
"target_list": [
"http://host.docker.internal:2025", // v1
"http://host.docker.internal:2026" // v2
]
}
Tyk round-robins across target_list, so roughly half the visitors land on each version — a real A/B split. Pin a visitor to one version for their whole session by enabling session-stickiness on the upstream, so they don't bounce between v1 and v2 between clicks. Add or remove a target and hot-reload to shift the weighting or end the experiment.
Why This Beats Editing DNS or the App
- No DNS propagation waits: The hostname never changes; only the gateway's internal target does. Switches are instant, not "wait an hour for the TTL".
- Zero downtime: Both versions run simultaneously. Cut-over and rollback happen between requests via a hot reload.
- The app stays dumb: Neither v1 nor v2 needs to know it's part of an experiment. All the routing logic lives in one place.
- One control plane for everything: The same gateway also rate-limits SplitBillz and serves the estate page. One mental model for the whole homelab.
Conclusion
Tyk's open-source gateway turned my pile of containers-on-ports into something that feels intentional: one front door, per-subdomain routing, built-in rate limiting, and — the part I actually wanted — a clean way to run two versions of my AMA app at once and steer traffic between them with a single hot reload. Whether I want an instant blue-green cut-over, a private preview, or a real weighted A/B split, it's the same small JSON file and one curl.
Next on my list is putting TLS termination in front of the gateway and wiring the API reloads into my deploy pipeline so promoting a new version is fully automated. If you're running more than one thing at home, a gateway is one of those additions that pays for itself almost immediately. Give it a try!
Contact Me
If you have questions or would like to be in touch whether to improve the project (or want to collaborate), feel free to reach out. I'm also open to learn from others.