It's time to write your first configuration. This episode covers downloading Envoy via Docker, assembling a minimal configuration for simple HTTP proxying, running it, and verifying traffic through Envoy.

Episode 3 is your "hello world" moment with Envoy. After understanding the architecture in episode 2, we now write a real configuration: download the Envoy image, assemble a minimal bootstrap to proxy HTTP, run it, and prove that traffic truly flows through Envoy.
You'll see the pattern repeated throughout the series: a listener on port 10000, an HTTP connection manager as the filter, one virtual host, and one backend cluster. This configuration is deliberately kept as simple as possible — additional variables and features will be added in episodes 4 through 8.
The most practical way is to use the official image from Docker Hub:
docker pull envoyproxy/envoy:v1.31.0There are a few variants you should know:
envoyproxy/envoy:latest — the latest release, convenient for labs but changes quickly.envoyproxy/envoy:v1.31.0 — a specific version, stable for repeated use.envoyproxy/envoy:distroless — a minimal variant without a shell, for production.For this series we consistently use envoyproxy/envoy:v1.31.0 so the output across all episodes is reproducible.
The default image contains a built-in config that runs the admin and a simple listener. To use your own config, mount the YAML file:
docker run -d --name envoy-hello \
-v ~/envoy-lab/configs:/etc/envoy \
-p 10000:10000 -p 9901:9901 \
envoyproxy/envoy:v1.31.0We'll fill the ~/envoy-lab/configs directory with a hello.yaml file. This docker run command mounts the config directory into the container and exposes ports 10000 and 9901.
Create the file configs/hello.yaml with the following content:
admin:
address:
socket_address:
address: 0.0.0.0
port_value: 9901
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
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains:
- "*"
routes:
- match:
prefix: "/"
route:
cluster: service_backend
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: service_backend
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: service_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 10.0.0.1
port_value: 8080Let's break down the important parts: filter_chains contains a single http_connection_manager filter that wraps route_config and the list of http_filters. The route maps all / prefixes to the cluster service_backend, which contains a single endpoint at 10.0.0.1:8080.
Since 10.0.0.1:8080 may not exist, start a dummy backend first so this hello world is complete:
python3 -m http.server 8080 &
curl -s http://localhost:8080/The python3 -m http.server 8080 command runs a static file server on port 8080 — good enough as a target to see whether Envoy forwards requests correctly.
Now point curl at Envoy's port, not directly at the backend:
curl -s -H "Host: localhost" http://localhost:10000/Notice the difference: port 10000 is Envoy's listener, while port 8080 is the backend. If Envoy works, you'll see the same directory listing as when calling the backend directly. The Host header is used to select the virtual host.
For stronger proof that traffic flows through Envoy, look at the counters in the admin interface:
curl -s localhost:9901/stats | grep "http.ingress_http"
curl -s localhost:9901/clustersThe stats output will show ingress_http requests increasing, and clusters displays endpoint status. The ingress_http statistics are Envoy's observability language, which you'll keep reading all the way to episode 21.
Before production, make a habit of validating your YAML first:
docker run --rm -v ~/envoy-lab/configs:/etc/envoy \
envoyproxy/envoy:v1.31.0 envoy --mode validate -c /etc/envoy/hello.yamlThe envoy --mode validate -c hello.yaml command checks the config without starting the proxy. An OK output means the YAML is valid and all resources are known to Envoy. This habit will pay off greatly in episode 20 when we integrate into CI/CD.
The most common error in this episode is a failed connection because the backend doesn't exist. Make sure python3 -m http.server 8080 is really running, then check:
curl -v -H "Host: localhost" http://localhost:10000/The curl -v part shows handshake details and the response code. If you see 502, inspect Envoy's logs with docker logs envoy-hello and make sure the backend address in the config is correct.
Wrong indentation produces an error at startup. Use a YAML linter and keep two spaces per level. If Envoy rejects your config, run envoy --mode validate to find the offending line.
Episode 3 took you from zero to a working proxy: pulling the image, writing a minimal bootstrap, running a test backend, and proving traffic flows through Envoy via the admin statistics.
Key takeaways:
envoyproxy/envoy:v1.31.0 image is enough for the whole series.http_connection_manager turns TCP into routable HTTP requests.envoy --mode validate before production.python3 -m http.server is a handy test backend for learning.In the next episode, episode 4, we'll discuss listener, filter chain, and route configuration — virtual hosts, path matching, header manipulation, redirect, and rewrite, so your routing is no longer just forwarding every prefix to one cluster.