Skip to content

Step 1: Your First Envoy Proxy

Duration: ~30 minutes Goal: Run Envoy in Docker, understand the config structure, and proxy HTTP to a real backend.


What you will learn

  • The three-part structure of every Envoy config: listeners, clusters, routes
  • How to run Envoy in Docker with a mounted config file
  • How to use the admin API to inspect what Envoy is doing

Concepts

The request flow

Every request through Envoy follows this path:

client → listener → filter chain → route → cluster → upstream

These map to three top-level sections in every Envoy config:

Section Purpose
listeners Where Envoy accepts connections (IP + port)
clusters Named groups of upstream servers
routes Rules inside filter chains that map requests to clusters

The config file format

Envoy uses YAML (or JSON) and the xDS API. All type URLs start with type.googleapis.com/ — this is proto-based configuration and the @type field tells Envoy which protobuf message to deserialize into.

Why @type?

Envoy uses protobuf Any for extensibility. The @type field identifies the concrete message type, which is why you see long strings like type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager.

Virtual hosts and domain matching

Inside the HTTP Connection Manager's route_config, a virtual host groups routes that share a domain name pattern:

route_config:
  name: local_route
  virtual_hosts:
    - name: local_service
      domains: ["*"]          # matches any Host header
      routes:
        - match:
            prefix: "/"
          route:
            cluster: my_cluster
  • domains — a list of patterns matched against the incoming Host (or :authority in HTTP/2) header. Envoy picks the first virtual host whose domain list matches.
  • "*" matches everything (wildcard catch-all)
  • "api.example.com" matches an exact hostname
  • "*.example.com" matches any subdomain
  • routes — an ordered list of route rules. Envoy evaluates them top to bottom and uses the first match. Common matchers:
  • prefix: "/" — any path starting with / (catches everything)
  • path: "/healthz" — exact path match
  • safe_regex — regular expression match

Route timeout semantics

Every route can have a timeout field that applies to the entire request, from when Envoy sends the first byte upstream to when the last response byte is received:

route:
  cluster: my_cluster
  timeout: 15s    # total request deadline (default: 15s if omitted)

The default timeout is 15 seconds. Set timeout: 0s to disable it entirely (required for streaming RPCs — see Step 3).

Cluster discovery types

The type field on a cluster controls how Envoy resolves endpoints:

Type Behavior
STATIC Endpoints are hardcoded in the config — no DNS
LOGICAL_DNS Resolve DNS once, re-resolve periodically; use one IP
STRICT_DNS Resolve DNS continuously; use all returned IPs as endpoints
EDS Endpoints come from a dynamic discovery service (used in Step 5)

Setup

Create a working directory:

mkdir -p envoy-tutorial/step1 && cd envoy-tutorial/step1

Create envoy.yaml:

static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 10000
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager  # (1)
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http  # (2)
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]  # (3)
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: httpbin_cluster
                http_filters:
                  - name: envoy.filters.http.router  # (4)
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

  clusters:
    - name: httpbin_cluster
      connect_timeout: 5s  # (5)
      type: LOGICAL_DNS  # (6)
      dns_lookup_family: V4_ONLY
      load_assignment:
        cluster_name: httpbin_cluster
        endpoints:
          - lb_endpoints:  # (7)
              - endpoint:
                  address:
                    socket_address:
                      address: httpbin.org
                      port_value: 80

admin:
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 9901  # (8)
  1. http_connection_manager (HCM) is the main network filter. It parses HTTP/1.1 and HTTP/2, handles routing, and passes requests to its own inner HTTP filter chain.
  2. stat_prefix is a string prepended to all metrics from this HCM instance. With ingress_http, stats will look like http.ingress_http.downstream_rq_total. Choose a unique prefix per listener.
  3. domains: ["*"] matches any Host header. In production you would use specific domain names (e.g. api.example.com) to do virtual hosting — multiple virtual hosts can share one listener.
  4. The router filter must be last in the HTTP filter chain — it is the terminal filter that forwards the request to the upstream cluster.
  5. connect_timeout controls how long Envoy waits to establish a TCP connection to an upstream. This is separate from the request timeout (the timeout field on a route). If connection establishment takes longer than connect_timeout, Envoy fails the request with a connection error.
  6. LOGICAL_DNS resolves httpbin.org to one IP and re-resolves periodically. Good for external services with stable hostnames.
  7. The endpoint hierarchy endpoints → lb_endpoints → endpoint comes from Envoy's proto model. endpoints is a list of LocalityLbEndpoints (can represent a geographic zone). lb_endpoints is the list of individual endpoints within that locality. For static configs you usually have one locality containing all your endpoints.
  8. The admin API is unauthenticated. In production, bind it to 127.0.0.1 or restrict access at the network level.

Run it

docker run --rm -it \
  -p 10000:10000 \
  -p 9901:9901 \
  -v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml \
  envoyproxy/envoy:v1.31-latest \
  envoy -c /etc/envoy/envoy.yaml

You should see Envoy start and print its version and config source.


Exercises

1. Test the proxy

curl -v http://localhost:10000/get

You should receive httpbin's JSON response. Notice in Envoy's stdout you'll see an access log line for the request.

2. Explore the admin API

Open http://localhost:9901 in your browser, or use curl:

# See your cluster status
curl http://localhost:9901/clusters

# See all stats (there are thousands)
curl http://localhost:9901/stats | grep httpbin

# Dump the full running config as JSON
curl http://localhost:9901/config_dump | python3 -m json.tool | less

Stats naming convention

Envoy stats follow the pattern <scope>.<name>. For your cluster: cluster.httpbin_cluster.upstream_rq_total is the total requests sent to the cluster. cluster.httpbin_cluster.upstream_cx_connect_fail counts connection failures.

3. Watch an upstream failure

Stop the container and change httpbin.org to a non-existent hostname like does-not-exist.example.com. Start Envoy again and curl it:

curl -v http://localhost:10000/get

You should get a 503 Service Unavailable. Check the stats:

curl http://localhost:9901/stats | grep connect_fail

4. Add a second route

Try adding a second virtual host or route that sends /status requests to a different path. Modify the route_config section:

routes:
  - match:
      prefix: "/status"
    route:
      cluster: httpbin_cluster
      prefix_rewrite: "/status"  # keep the path
  - match:
      prefix: "/"
    route:
      cluster: httpbin_cluster

Routes are matched in order — the first match wins.


What you learned

  • The three-part Envoy config structure: listeners, clusters, routes
  • How @type works in Envoy's proto-based config
  • Cluster discovery types and when to use each
  • The admin API for real-time visibility
  • How stats are named and how to query them

Next step

In Step 2 you will run multiple backend instances and configure Envoy to load-balance between them.