Envoy is a high performance edge and service proxy originally built at Lyft and now hosted by the CNCF. This guide covers running Envoy as a standalone edge proxy driven by a static bootstrap configuration, terminating TLS and routing to one or more backend applications. It does not use Envoy Gateway, a control plane, or any Kubernetes sidecar model. This is the classic "single binary reverse proxy in front of your app" pattern.
Everything here stays within RamNode's acceptable use policy. Envoy does not run or relay mail, and the only email address used anywhere is the one Let's Encrypt keeps on file for expiry notices, which involves no outbound SMTP from your VPS.
Prerequisites
- A RamNode VPS running Ubuntu 24.04 LTS (Debian 12 works with minor path differences)
- Root or a sudo-enabled user
- A domain or subdomain with an A record (and AAAA if you use IPv6) pointing at your VPS
- A backend service to proxy, for example an app listening on
127.0.0.1:8080
Throughout the guide, replace app.example.com with your real hostname and 1.38.3 with the current stable release from the Envoy releases page.
1. Prepare the system
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl ca-certificatesCreate a dedicated unprivileged system user for the proxy so Envoy never runs as root:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin envoy2. Install the Envoy binary
Envoy publishes statically linked Linux binaries with each release. Pin the version so upgrades are deliberate rather than accidental:
ENVOY_VERSION=1.38.3
sudo curl -fL \
"https://github.com/envoyproxy/envoy/releases/download/v${ENVOY_VERSION}/envoy-${ENVOY_VERSION}-linux-x86_64" \
-o /usr/local/bin/envoy
sudo chmod +x /usr/local/bin/envoy
envoy --versionIf your VPS is ARM based, substitute linux-aarch64 for linux-x86_64.
3. Create the directory layout
sudo mkdir -p /etc/envoy/certs /var/log/envoy /var/www/acme
sudo chown -R envoy:envoy /var/log/envoy /etc/envoy/certs4. Issue the TLS certificate
Because Envoy will own ports 80 and 443, issue the first certificate before Envoy is running, using Certbot in standalone mode.
sudo apt install -y certbot
sudo certbot certonly --standalone \
-d app.example.com \
--agree-tos --no-eff-email \
-m you@example.comEnvoy runs as an unprivileged user and cannot read files under /etc/letsencrypt/archive, so copy the active certificate into a location it owns. This deploy hook runs on every successful issuance and renewal:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/envoy-certs.sh >/dev/null <<'EOF'
#!/bin/bash
DOMAIN=app.example.com
DEST=/etc/envoy/certs
install -o envoy -g envoy -m 644 \
/etc/letsencrypt/live/${DOMAIN}/fullchain.pem ${DEST}/fullchain.pem
install -o envoy -g envoy -m 600 \
/etc/letsencrypt/live/${DOMAIN}/privkey.pem ${DEST}/privkey.pem
systemctl restart envoy
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/envoy-certs.sh
sudo /etc/letsencrypt/renewal-hooks/deploy/envoy-certs.shRenewals need port 80 free for a moment. Configure Certbot to stand Envoy down and back up around the standalone renewal:
sudo tee /etc/letsencrypt/cli.ini >/dev/null <<'EOF'
pre-hook = systemctl stop envoy
post-hook = systemctl start envoy
EOFThe renewal window is a few seconds roughly once a month. If you want zero downtime instead, use a DNS-01 challenge with a provider plugin (for example certbot-dns-cloudflare), which never touches ports 80 or 443. In that case drop the cli.ini hooks above and keep only the deploy hook.
5. Write the Envoy configuration
Create /etc/envoy/envoy.yaml. The admin interface binds to localhost only, port 80 redirects everything to HTTPS, and port 443 terminates TLS and forwards to the backend cluster.
admin:
address:
socket_address:
address: 127.0.0.1
port_value: 9901
static_resources:
listeners:
- name: http_listener
address:
socket_address: { address: 0.0.0.0, port_value: 80 }
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: redirect_route
virtual_hosts:
- name: redirect_all
domains: ["*"]
routes:
- match: { prefix: "/" }
redirect: { https_redirect: true, port_redirect: 443 }
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
- name: https_listener
address:
socket_address: { address: 0.0.0.0, port_value: 443 }
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_https
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: app
domains: ["app.example.com"]
routes:
- match: { prefix: "/" }
route: { cluster: backend_app }
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_params:
tls_minimum_protocol_version: TLSv1_2
tls_certificates:
- certificate_chain: { filename: /etc/envoy/certs/fullchain.pem }
private_key: { filename: /etc/envoy/certs/privkey.pem }
clusters:
- name: backend_app
type: STRICT_DNS
connect_timeout: 5s
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: backend_app
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 127.0.0.1, port_value: 8080 }To add more backends, define additional clusters and add matching virtual_hosts on the HTTPS listener. To load balance, list multiple lb_endpoints inside one cluster.
Validate the configuration before you ever start the service:
sudo -u envoy /usr/local/bin/envoy --mode validate -c /etc/envoy/envoy.yaml6. Create the systemd service
Envoy runs unprivileged but still needs to bind the low ports 80 and 443, so grant it the single capability required rather than running as root:
sudo tee /etc/systemd/system/envoy.service >/dev/null <<'EOF'
[Unit]
Description=Envoy Proxy
After=network-online.target
Wants=network-online.target
[Service]
User=envoy
Group=envoy
AmbientCapabilities=CAP_NET_BIND_SERVICE
ExecStart=/usr/local/bin/envoy -c /etc/envoy/envoy.yaml --log-path /var/log/envoy/envoy.log
Restart=on-failure
RestartSec=3
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now envoy
sudo systemctl status envoy --no-pager7. Configure the firewall
RamNode ships a clean VPS, so the firewall is yours to define. Allow SSH and the web ports, and keep the admin interface off the public internet (it already binds to localhost, and UFW gives you a second layer):
sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbosePort 9901 is never opened. If you need to reach the admin panel, tunnel it over SSH:
ssh -L 9901:127.0.0.1:9901 youruser@your-vps-ipThen open http://127.0.0.1:9901 in your local browser.
8. Verify
curl -I http://app.example.com # expect a 301 redirect to https
curl -I https://app.example.com # expect your backend response
curl -s 127.0.0.1:9901/server_info | headLive statistics are available at http://127.0.0.1:9901/stats and cluster health at http://127.0.0.1:9901/clusters (reachable through the SSH tunnel above).
9. Backups
The entire state you care about lives in /etc/envoy. Certbot state lives in /etc/letsencrypt. Back both up:
sudo tee /usr/local/bin/backup-envoy.sh >/dev/null <<'EOF'
#!/bin/bash
STAMP=$(date +%F)
DEST=/var/backups/envoy
mkdir -p "$DEST"
tar czf "$DEST/envoy-${STAMP}.tar.gz" /etc/envoy /etc/letsencrypt
find "$DEST" -name 'envoy-*.tar.gz' -mtime +14 -delete
EOF
sudo chmod +x /usr/local/bin/backup-envoy.sh
echo "0 3 * * * root /usr/local/bin/backup-envoy.sh" | sudo tee /etc/cron.d/backup-envoyFor offsite retention, push /var/backups/envoy to RamNode S3 compatible storage or another remote target with a tool such as rclone or restic.
10. Updating Envoy
Envoy issues security releases regularly, and several CVEs in the proxy stack land each quarter, so treat updates as routine:
ENVOY_VERSION=1.38.3 # set to the new release
sudo systemctl stop envoy
sudo curl -fL \
"https://github.com/envoyproxy/envoy/releases/download/v${ENVOY_VERSION}/envoy-${ENVOY_VERSION}-linux-x86_64" \
-o /usr/local/bin/envoy
sudo chmod +x /usr/local/bin/envoy
sudo -u envoy /usr/local/bin/envoy --mode validate -c /etc/envoy/envoy.yaml
sudo systemctl start envoy
envoy --versionAlways validate before restarting so a bad config never takes the listener down.
Security checklist
- Admin interface bound to
127.0.0.1:9901and never exposed through UFW - Envoy runs as an unprivileged system user with only
CAP_NET_BIND_SERVICE - Private key stored
600and owned by theenvoyuser tls_minimum_protocol_versionset to TLS 1.2- Version pinned and updated deliberately, tracking the releases page for CVE fixes
- Config validated with
--mode validatebefore every restart - No mail services involved, consistent with RamNode's AUP
Where to go next
Common additions to a standalone Envoy edge include rate limiting via the local rate limit filter, request and response header manipulation, gRPC and HTTP/2 upstream routing, access logging to a structured format, and health checks on your backend clusters. Each is a filter or cluster field added to the same static config, and every change should pass --mode validate before you restart.
