Getting Started
Stand up Kong with Docker Compose, route to a backend, add API-key auth and rate limiting
Getting Started
Kong is a pragmatic first gateway — Lua + Nginx, declarative config, large plugin ecosystem. This page boots it locally in DB-less mode (config from YAML), routes traffic to a real backend, and turns on the two features you'll always end up using: auth and rate limits.
For Envoy / Traefik / cloud-managed equivalents, the concepts are the same; the syntax differs.
Stand It Up
# docker-compose.yml
services:
echo:
image: ealen/echo-server:latest # tiny server that echoes the request
environment:
PORT: "8080"
kong:
image: kong:3.6
environment:
KONG_DATABASE: "off" # DB-less mode
KONG_DECLARATIVE_CONFIG: /etc/kong/kong.yml
KONG_PROXY_ACCESS_LOG: /dev/stdout
KONG_ADMIN_ACCESS_LOG: /dev/stdout
KONG_PROXY_ERROR_LOG: /dev/stderr
KONG_ADMIN_ERROR_LOG: /dev/stderr
KONG_ADMIN_LISTEN: 0.0.0.0:8001
KONG_PLUGINS: "bundled"
ports:
- "8000:8000" # proxy
- "8001:8001" # admin API
volumes:
- ./kong.yml:/etc/kong/kong.yml:ro
depends_on: [echo]# kong.yml — declarative config (DB-less)
_format_version: "3.0"
services:
- name: echo-service
url: http://echo:8080
routes:
- name: echo-route
paths: ["/echo"]
strip_path: true # /echo/foo → /foo upstreamdocker compose up -d
curl -s http://localhost:8000/echo/hello | jqIf you see a JSON dump describing your request, the proxy works.
Add API-Key Auth
# kong.yml — append below the service block
plugins:
- name: key-auth
service: echo-service
config:
key_names: ["apikey"]
hide_credentials: true
consumers:
- username: alice
keyauth_credentials:
- key: alice-secret-keydocker compose restart kong
# No key → 401
curl -i http://localhost:8000/echo/hi
# HTTP/1.1 401 Unauthorized
# With key → 200
curl -i http://localhost:8000/echo/hi -H 'apikey: alice-secret-key'
# HTTP/1.1 200 OKThe hide_credentials: true option strips the header before forwarding upstream so backends never see it.
Add Rate Limiting
plugins:
- name: key-auth
service: echo-service
config:
key_names: ["apikey"]
hide_credentials: true
- name: rate-limiting
service: echo-service
config:
minute: 5
policy: local
hide_client_headers: falsedocker compose restart kong
for i in {1..7}; do
curl -s -o /dev/null -w "%{http_code}\n" \
http://localhost:8000/echo/hi -H 'apikey: alice-secret-key'
done
# 200 200 200 200 200 429 429The response includes informative headers:
RateLimit-Limit: 5
RateLimit-Remaining: 0
RateLimit-Reset: 47Route Multiple Backends
A gateway's main job:
services:
- name: users-service
url: http://users:8080
routes:
- { name: users-route, paths: ["/api/users"] }
- name: orders-service
url: http://orders:8080
routes:
- { name: orders-route, paths: ["/api/orders"] }
- name: legacy-service
url: http://legacy:8080
routes:
- name: legacy-route
paths: ["/api/legacy"]
methods: ["GET"] # only GETs from outside/api/users/* → users service; /api/orders/* → orders; /api/legacy/* GETs → legacy. One public URL, many backends.
Inspect via Admin API
curl -s http://localhost:8001/services | jq
curl -s http://localhost:8001/routes | jq
curl -s http://localhost:8001/plugins | jq
curl -s http://localhost:8001/consumers | jq
# Live cluster status
curl -s http://localhost:8001/status | jqIn production the admin API should never be exposed publicly. Bind it to an internal interface and put your own auth in front.
Quick Wins
A few one-line plugins worth turning on early:
plugins:
- name: cors
service: echo-service
config:
origins: ["https://app.example.com"]
credentials: true
- name: request-size-limiting
service: echo-service
config: { allowed_payload_size: 1 } # 1 MB max
- name: correlation-id
service: echo-service
config: { header_name: X-Request-Id, generator: uuid, echo_downstream: true }
- name: prometheus # /metrics on the admin APIcorrelation-id is underrated — every request gets a UUID at the edge that every backend log line should include. Tracing-without-tracing.
Kubernetes: Same Idea, K8s-Native
In a cluster, the same routing pattern is expressed as Gateway API resources (or Ingress, for older setups):
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: public
namespace: gateway
spec:
gatewayClassName: envoy
listeners:
- { name: https, port: 443, protocol: HTTPS, tls: { ... } }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api
spec:
parentRefs: [{ name: public }]
hostnames: ["api.example.com"]
rules:
- matches: [{ path: { value: "/users" } }]
backendRefs: [{ name: users, port: 80 }]
- matches: [{ path: { value: "/orders" } }]
backendRefs: [{ name: orders, port: 80 }]Pick a Gateway API controller (Envoy Gateway, Contour, Kong, Istio, NGINX) and the same shape applies.
Tear Down
docker compose down -vWhat's Next
You have a working gateway with auth and rate limits. Next:
- Patterns — JWT/OIDC, mTLS, request transforms, BFF, schema enforcement
- Best Practices — HA, versioning, observability, anti-patterns