Deployment
A first deploy to a plain VPS, no Docker required.
This assumes a standard Ubuntu/Debian VPS with SSH access. Every command here is meant to be run on that server, not on your own machine.
- Generate production secrets
Don't reuse local development values. Generate fresh ones:
openssl rand -hex 16Run this once for
ENVI_ENCRYPTION_KEY(needs to be exactly 32 characters — this produces exactly that) and, if you're also deploying the web dashboard, once more forENVI_SESSION_SECRET. - Install system dependencies
sudo apt update && sudo apt upgrade -y sudo apt install -y postgresql postgresql-contrib redis-server nodejs npm git ufwGo isn't in Ubuntu's apt repos at a current version — install it from the official tarball:
curl -fsSL https://go.dev/dl/go1.25.12.linux-amd64.tar.gz -o go.tar.gz sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go.tar.gz echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc && source ~/.bashrcSwap
amd64forarm64if this VPS is ARM (uname -mshowsaarch64). Then PM2, via npm:sudo npm install -g pm2 - Set up Postgres and Redis
Redis needs no config for localhost-only use — it's bound to
127.0.0.1by default. Postgres needs a role and database:sudo -u postgres psql -c "CREATE ROLE envi WITH LOGIN PASSWORD 'a-real-password';" sudo -u postgres psql -c "CREATE DATABASE envi OWNER envi;" - Clone and configure
git clone git@github.com:shellhaki/envi.git cd enviCreate
.envhere — it's gitignored, so it only ever exists on this server:DATABASE_URL=postgresql://envi:a-real-password@localhost:5432/envi REDIS_URL=redis://localhost:6379 ENVI_ENCRYPTION_KEY=<generated above> RESEND_API_KEY=<your production Resend key> RESEND_FROM="Envi <noreply@envisecrets.com>" ENVI_WEB_URL=https://app.envisecrets.com ENVI_API_PORT=8080 ENVI_SERVER_PORT=8081 ENVIRONMENT=productionENVI_API_PORTandENVI_SERVER_PORTare the ports the two binaries listen on locally. Neither is exposed directly to the internet; see the reverse-proxy step below. - Apply the schema and build
make db-init make build-api build-installdb-initdetects a fresh database and appliesmigrations/schema.sql. The twobuild-*targets producebin/envi-apiandbin/envi-install, matching whatecosystem.config.jsexpects. - Start under PM2, and survive reboots
pm2 start ecosystem.config.js pm2 save pm2 startuppm2 startupprints a command specific to this machine — copy and run exactly that line. That's what brings both processes back after a reboot. Confirm they're actually serving, not just "online":curl -s http://localhost:8080/health curl -s http://localhost:8081/health - Put a reverse proxy in front, for TLS
Don't expose
:8080/:8081directly. Pointapi.envisecrets.comat:8080andinstall.envisecrets.comat:8081, and terminate TLS at the proxy. The repo ships a ready-to-use Traefik config undertraefik/, but nginx or Caddy work equally well — the only requirement is that DNS for both names resolves to this VPS before the proxy tries to issue certificates. - Lock down the firewall
sudo ufw allow OpenSSH sudo ufw allow 80,443/tcp sudo ufw enableThis leaves Postgres, Redis, and the raw app ports reachable only from
localhost. That matters more here than in most apps, given what this one stores. - Verify for real
curl https://api.envisecrets.com/health curl https://install.envisecrets.com/The second should print the actual
install.shcontents — at that pointcurl -fsSL https://install.envisecrets.com | shworks for anyone.
Updating a deployment
cd envi && git pull
make build-api build-install
pm2 reload ecosystem.config.js
pm2 reload (not restart) does a zero-downtime reload where possible.

