Deploy to VPS
This guide deploys envGo to a Linux VPS (DigitalOcean, Vultr, Linode, Hetzner, AWS EC2, and so on). It covers both local mode and public mode, with a systemd service and a reverse proxy for TLS.
Prerequisites
- A Linux VPS (Ubuntu/Debian assumed below)
- SSH access
- A domain name, if you want HTTPS with a trusted certificate
- The envGo Linux binary:
dist/envgo-linux-amd64ordist/envgo-linux-arm64
Step 1 — Upload
# The binaryscp dist/envgo-linux-amd64 user@your-vps:/tmp/envgo
# Your project (HTML, .env, routes config)scp -r /path/to/your/project user@your-vps:/var/www/myappOr clone a repository and create .env on the server:
ssh user@your-vpssudo git clone https://github.com/yourusername/yourproject.git /var/www/myappStep 2 — Install the binary
# Detect the architecture firstuname -m # x86_64 → amd64, aarch64 → arm64
sudo cp /tmp/envgo /usr/local/bin/envgosudo chmod +x /usr/local/bin/envgo
# Verifyenvgo -venvgo -hStep 3 — Configure the environment
cd /var/www/myappsudo nano .envMY_SECRET=your-secret-valueOPENAI_API_KEY=sk-your-real-keyIf you plan to use envgo run on the server, you can also set:
HOST=127.0.0.1PORT=8080Lock down permissions — the file should not be world-readable:
sudo chmod 600 /var/www/myapp/.envStep 4 — Choose a mode
Option A — Local mode with --allow
The browser still chooses target URLs, but only allowlisted hosts are reachable. This is the simplest setup and is fine for private tools.
sudo -u www-data envgo \ --dir /var/www/myapp \ --env /var/www/myapp/.env \ --allow api.openai.com \ --host 127.0.0.1 \ --port 8080Option B — Public mode with a routes config (recommended)
The browser chooses a route name only; target URLs come from the config.
sudo -u www-data envgo \ --config /var/www/myapp/envgo.routes.json \ --env /var/www/myapp/.env \ --dir /var/www/myapp \ --host 127.0.0.1 \ --port 8080Create the routes config before you start. See Configuration for every field.
{ "trust_proxy": true, "default_rate_limit": "30/min", "scrub_response": true, "routes": [ { "name": "chat", "method": "POST", "target": "https://api.openai.com/v1/chat/completions", "vars": ["OPENAI_API_KEY"], "inject": { "headers": { "Authorization": "Bearer {OPENAI_API_KEY}" } }, "rate_limit": "20/min" } ]}Step 5 — Run as a systemd service
sudo nano /etc/systemd/system/envgo.servicePublic mode:
[Unit]Description=envGo ServerAfter=network.target
[Service]Type=simpleUser=www-dataGroup=www-dataWorkingDirectory=/var/www/myappExecStart=/usr/local/bin/envgo --config /var/www/myapp/envgo.routes.json --env /var/www/myapp/.env --dir /var/www/myapp --host 127.0.0.1 --port 8080Restart=alwaysRestartSec=5
# HardeningNoNewPrivileges=truePrivateTmp=trueProtectSystem=fullProtectHome=true
[Install]WantedBy=multi-user.targetFor local mode, replace ExecStart with:
ExecStart=/usr/local/bin/envgo --dir /var/www/myapp --env /var/www/myapp/.env --allow api.openai.com --host 127.0.0.1 --port 8080Enable and start:
sudo systemctl daemon-reloadsudo systemctl enable envgosudo systemctl start envgosudo systemctl status envgoOn shutdown, systemd sends SIGTERM, which envGo handles with a graceful drain
of up to 3 seconds.
Step 6 — Reverse proxy for TLS
Terminating TLS at Caddy or nginx keeps envGo simple and gives you automatic certificates.
Caddy
sudo apt install -y caddysudo nano /etc/caddy/Caddyfileyourdomain.com { reverse_proxy 127.0.0.1:8080}sudo systemctl enable caddysudo systemctl start caddyCaddy obtains and renews a Let’s Encrypt certificate automatically and redirects HTTP to HTTPS.
nginx
server { listen 443 ssl; server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}
server { listen 80; server_name yourdomain.com; return 301 https://$host$request_uri;}Setting X-Forwarded-For is what makes trust_proxy: true meaningful.
trust_proxy — read this before enabling it
{ "trust_proxy": true, "default_rate_limit": "30/min", "routes": [] }- Behind a proxy you control: set it to
true. Rate limiting then keys on the real client IP from the firstX-Forwarded-Forentry. - Directly exposed: leave it
false. If it istrueon a directly reachable instance, clients can forgeX-Forwarded-Forand give themselves a fresh rate limit bucket per request.
Step 7 — Firewall
Only expose the ports the reverse proxy needs:
sudo ufw allow 22/tcpsudo ufw allow 80/tcpsudo ufw allow 443/tcpsudo ufw enablesudo ufw statusPort 8080 stays bound to 127.0.0.1 and must not be opened.
Verification
# Static pagecurl -I https://yourdomain.com/
# A route with the bearer auth header, if the route uses authcurl -X POST https://yourdomain.com/api/chat \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4","messages":[{"role":"user","content":"Hello"}]}'
# Logssudo journalctl -u envgo -fsudo journalctl -u caddy -fA 200 with an upstream payload means the whole chain works. A 400 with
variable not available for this route means a vars entry does not match a key
in .env.
Troubleshooting
Address already in use
envGo automatically tries up to 20 consecutive ports, so it may already be running on the next port up — check the startup log. If something else holds the port:
sudo lsof -i :8080sudo systemctl stop envgoTo change the port of a systemd service, edit the --port value in
ExecStart, not PORT in .env — HOST/PORT from .env only apply to
envgo run.
sudo systemctl edit envgo # or edit the unit file directlysudo systemctl daemon-reloadsudo systemctl restart envgoPermission denied reading .env
sudo chown www-data:www-data /var/www/myapp/.envsudo chmod 600 /var/www/myapp/.envConnection refused from the reverse proxy
sudo systemctl status envgosudo journalctl -u envgo -n 50sudo ss -tlnp | grep 8080 # confirm envGo is bound where you expectCertificate warnings
If you enabled --tls, the warning is expected — envGo uses a self-signed
certificate. Use Caddy or nginx for a trusted certificate instead. See
TLS / HTTPS.
Routes return 401 / 403 unexpectedly
401— the route usesauth, or the session token is missing in local mode403— in local mode, the target host is not in--allow404— the route name does not match, or you are not actually in public mode (check for thePUBLIC MODEline in the startup log)
Security checklist
-
.envhas mode600and is owned by the service user -
.envis in.gitignore - envGo is bound to
127.0.0.1, not0.0.0.0 - Port 8080 is closed at the firewall
- Reverse proxy terminates TLS
-
default_rate_limitor per-routerate_limitis set -
trust_proxy: trueonly because a proxy is actually in front -
--dashboardis not enabled in production -
scrub_responseenabled if upstreams might echo credentials - No
.phpfiles unless a PHP interpreter is installed
Next steps
- TLS / HTTPS — certificate options compared
- Docker Deployment — container alternative
- Threat Model — known limitations