Steven's Knowledge

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 upstream
docker compose up -d
curl -s http://localhost:8000/echo/hello | jq

If 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-key
docker 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 OK

The 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: false
docker 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 429

The response includes informative headers:

RateLimit-Limit: 5
RateLimit-Remaining: 0
RateLimit-Reset: 47

Route 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 | jq

In 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 API

correlation-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 -v

What'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

On this page