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:
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 incomingHost(or:authorityin 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 subdomainroutes— 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 matchsafe_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:
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:
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)
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.stat_prefixis a string prepended to all metrics from this HCM instance. Withingress_http, stats will look likehttp.ingress_http.downstream_rq_total. Choose a unique prefix per listener.domains: ["*"]matches anyHostheader. In production you would use specific domain names (e.g.api.example.com) to do virtual hosting — multiple virtual hosts can share one listener.- The
routerfilter must be last in the HTTP filter chain — it is the terminal filter that forwards the request to the upstream cluster. connect_timeoutcontrols how long Envoy waits to establish a TCP connection to an upstream. This is separate from the request timeout (thetimeoutfield on a route). If connection establishment takes longer thanconnect_timeout, Envoy fails the request with a connection error.LOGICAL_DNSresolveshttpbin.orgto one IP and re-resolves periodically. Good for external services with stable hostnames.- The endpoint hierarchy
endpoints → lb_endpoints → endpointcomes from Envoy's proto model.endpointsis a list ofLocalityLbEndpoints(can represent a geographic zone).lb_endpointsis the list of individual endpoints within that locality. For static configs you usually have one locality containing all your endpoints. - The admin API is unauthenticated. In production, bind it to
127.0.0.1or 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¶
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:
You should get a 503 Service Unavailable. Check the stats:
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
@typeworks 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.