Octopus
1.x
Docs/Octopus/Build Your First Gateway
Open

Reading10 min
Updated31 Jul 2026
Sourcev1/first-gateway.mdx

Build Your First Gateway

In this guide you'll build a gateway that routes traffic to three backend services, load-balances across instances, runs active health checks, authenticates requests with JWT, and exposes Prometheus metrics.

What you'll build01

A gateway that:

  • Routes to three backend services (users, orders, products).

  • Load-balances across multiple instances of a service.

  • Runs active health checks on upstreams.

  • Authenticates requests with a JWT auth provider, with public paths excluded.

  • Serves health probes (/livez, /readyz, /startupz) and Prometheus metrics.

Project setup02

1

Create a project directory

mkdir my-gateway
cd my-gateway

Start the gateway config

Create config.yaml. The gateway section is the only required one:

gateway:
  listen: "0.0.0.0:8080"
  workers: 0                 # 0 = one worker per CPU core
  request_timeout: 30s

observability:
  logging:
    level: info
    format: text
  metrics:
    enabled: true
    endpoint: /metrics
Note

Build up config.yaml section by section below. The complete file at the end of this page is validated and ready to copy.

Define upstreams03

An upstream is a named backend made up of one or more instances. Define three services. The order-service has two instances so it can be load-balanced:

upstreams:
  - name: user-service
    lb_policy: round_robin
    instances:
      - id: user-1
        host: 127.0.0.1
        port: 8081
    health_check:
      type: http
      path: /health
      interval: 10s
      timeout: 2s
      healthy_threshold: 2
      unhealthy_threshold: 3

  - name: order-service
    lb_policy: least_connections
    instances:
      - id: order-1
        host: 127.0.0.1
        port: 8082
      - id: order-2
        host: 127.0.0.1
        port: 8083
    health_check:
      type: http
      path: /health
      interval: 10s
      timeout: 2s

  - name: product-service
    lb_policy: round_robin
    instances:
      - id: product-1
        host: 127.0.0.1
        port: 8084
    health_check:
      type: http
      path: /health
      interval: 10s
      timeout: 2s
Warning

When upstreams come from a static config file, the gateway currently uses round-robin regardless of lb_policy, and health_check / circuit_breaker are parsed and validated but not yet wired into the static path. Both are honored on the Kubernetes/operator-driven path. See Upstreams, Load balancing, and Health checks.

Configure routes04

A route maps an incoming request to an upstream by name. Each path must start with /, and each upstream must exist in upstreams:

routes:
  - path: /api/users
    upstream: user-service
    methods: [GET, POST, PUT, DELETE]
    strip_prefix: /api
    timeout: 30s

  - path: /api/auth
    upstream: user-service
    methods: [POST]
    strip_prefix: /api
    skip_auth: true            # public — no authentication
    timeout: 10s

  - path: /api/orders
    upstream: order-service
    methods: [GET, POST, PUT, DELETE]
    strip_prefix: /api
    require_roles: [orders]    # authorization: caller must have this role

  - path: /api/products
    upstream: product-service
    methods: [GET, POST, PUT, DELETE]
    strip_prefix: /api
Note

strip_prefix removes the prefix before proxying, so /api/users/123 reaches the upstream as /users/123. Routes also support priority, add_prefix, per-route timeout, and per-route auth fields — see Routes.

Add authentication05

Authentication is configured with named auth providers and a global auth section, not a middleware list. Define a JWT provider, make it the default, and exclude public paths:

auth_providers:
  primary-jwt:
    type: jwt
    secret: "${JWT_SECRET}"     # HS256 by default
    algorithm: HS256
    issuer: my-gateway

auth:
  default_provider: primary-jwt
  global_enforce: true          # all routes require auth unless skip_auth / skip_paths
  skip_paths:
    - /api/auth
    - /livez
    - /readyz
    - /startupz

A route can override the provider with auth_provider:, opt out with skip_auth: true, or require roles/scopes with require_roles / require_scopes. See Authentication & authorization and the Security section for the request flow and other provider types (OIDC, API key, forward-auth, mTLS).

Compression06

Response compression runs in the request chain and is enabled by default. Tune it under gateway.compression:

gateway:
  listen: "0.0.0.0:8080"
  compression:
    enabled: true
    level: 6
    min_size: 1024
    algorithms: ["br", "zstd", "gzip"]

See Compression.

Warning

Compression and authentication are the middleware wired into the request chain today. CORS and rate limiting are present in the schema (global cors, per-route cors/rate_limit) but not yet enforced at request time. See Middleware for what is active.

Configure observability07

observability has three sub-sections: logging, metrics, and tracing.

observability:
  logging:
    level: info
    format: text          # or "json"
  metrics:
    enabled: true
    endpoint: /metrics
  tracing:
    enabled: false
    jaeger_endpoint: http://jaeger:4317
Note

Metrics are live on the /metrics endpoint. The JSON log format and distributed tracing are parsed but not yet applied at runtime — see Observability.

Complete configuration08

Here's the complete, validated config.yaml:

gateway:
  listen: "0.0.0.0:8080"
  workers: 0
  request_timeout: 30s
  shutdown_timeout: 30s
  compression:
    enabled: true
    level: 6
    min_size: 1024

upstreams:
  - name: user-service
    lb_policy: round_robin
    instances:
      - id: user-1
        host: 127.0.0.1
        port: 8081
    health_check:
      type: http
      path: /health
      interval: 10s
      timeout: 2s
      healthy_threshold: 2
      unhealthy_threshold: 3

  - name: order-service
    lb_policy: least_connections
    instances:
      - id: order-1
        host: 127.0.0.1
        port: 8082
      - id: order-2
        host: 127.0.0.1
        port: 8083
    health_check:
      type: http
      path: /health
      interval: 10s
      timeout: 2s

  - name: product-service
    lb_policy: round_robin
    instances:
      - id: product-1
        host: 127.0.0.1
        port: 8084
    health_check:
      type: http
      path: /health
      interval: 10s
      timeout: 2s

routes:
  - path: /api/users
    upstream: user-service
    methods: [GET, POST, PUT, DELETE]
    strip_prefix: /api
    timeout: 30s

  - path: /api/auth
    upstream: user-service
    methods: [POST]
    strip_prefix: /api
    skip_auth: true
    timeout: 10s

  - path: /api/orders
    upstream: order-service
    methods: [GET, POST, PUT, DELETE]
    strip_prefix: /api
    require_roles: [orders]

  - path: /api/products
    upstream: product-service
    methods: [GET, POST, PUT, DELETE]
    strip_prefix: /api

auth_providers:
  primary-jwt:
    type: jwt
    secret: "${JWT_SECRET}"
    algorithm: HS256
    issuer: my-gateway

auth:
  default_provider: primary-jwt
  global_enforce: true
  skip_paths:
    - /api/auth
    - /livez
    - /readyz
    - /startupz

observability:
  logging:
    level: info
    format: text
  metrics:
    enabled: true
    endpoint: /metrics
  tracing:
    enabled: false

Running the gateway09

1

Set environment variables

export JWT_SECRET=your-super-secret-key-change-this

Start backend services

For this example, use any HTTP servers on the configured ports (these won't answer /health, but they let you see routing work):

python3 -m http.server 8081   # user-service
python3 -m http.server 8082   # order-service (instance 1)
python3 -m http.server 8083   # order-service (instance 2)
python3 -m http.server 8084   # product-service

Validate and start Octopus

octopus validate -c config.yaml
octopus serve -c config.yaml

Test the gateway

# Health probes (served on the listen port)
curl http://localhost:8080/livez
curl http://localhost:8080/readyz

# Public route (skip_auth) — reaches the upstream as /auth/login
curl -X POST http://localhost:8080/api/auth/login

# Authenticated route — requires a JWT signed with $JWT_SECRET
export TOKEN="eyJhbGciOi..."
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/orders/456

# Prometheus metrics
curl http://localhost:9090/metrics

Monitoring10

The /metrics endpoint exposes Prometheus metrics. See Metrics for the exact metric names Octopus emits and how to scrape them, and Health probes for the probe semantics.

Production checklist11

Before deploying to production:

  • Terminate TLS (static gateway.tls or the Kubernetes path).

  • Use a strong JWT_SECRET supplied via the environment, never committed.

  • Scrape /metrics with Prometheus and wire alerting.

  • Wire Kubernetes liveness/readiness/startup probes to /livez, /readyz, /startupz.

  • Size terminationGracePeriodSeconds against gateway.shutdown_timeout and pre_stop_delay.

  • Run octopus validate -c config.yaml in CI.

See the full production checklist.

Next steps12

Common issues13

Careful

Upstream connection refused — confirm the backend is running and the instance host/port are correct; for FARP-discovered services, check the discovery backend.

JWT authentication failing — verify JWT_SECRET is set, the token is sent as Authorization: Bearer <token>, the algorithm matches the provider (HS256), and the token has not expired.

Config rejected on startup — run octopus validate -c config.yaml; it names the exact field that failed (for example a route referencing an upstream that doesn't exist).

For runtime debugging via logs and metrics, see Observability. For scaling guidance, see Scaling.