Docs

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.

  1. Generate production secrets

    Don't reuse local development values. Generate fresh ones:

    openssl rand -hex 16
    

    Run 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 for ENVI_SESSION_SECRET.

  2. Install system dependencies
    sudo apt update && sudo apt upgrade -y
    sudo apt install -y postgresql postgresql-contrib redis-server nodejs npm git ufw
    

    Go 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 ~/.bashrc
    

    Swap amd64 for arm64 if this VPS is ARM (uname -m shows aarch64). Then PM2, via npm:

    sudo npm install -g pm2
    
  3. Set up Postgres and Redis

    Redis needs no config for localhost-only use — it's bound to 127.0.0.1 by 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;"
    
  4. Clone and configure
    git clone git@github.com:shellhaki/envi.git
    cd envi
    

    Create .env here — 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=production
    

    ENVI_API_PORT and ENVI_SERVER_PORT are the ports the two binaries listen on locally. Neither is exposed directly to the internet; see the reverse-proxy step below.

  5. Apply the schema and build
    make db-init
    make build-api build-install
    

    db-init detects a fresh database and applies migrations/schema.sql. The two build-* targets produce bin/envi-api and bin/envi-install, matching what ecosystem.config.js expects.

  6. Start under PM2, and survive reboots
    pm2 start ecosystem.config.js
    pm2 save
    pm2 startup
    

    pm2 startup prints 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
    
  7. Put a reverse proxy in front, for TLS

    Don't expose :8080/:8081 directly. Point api.envisecrets.com at :8080 and install.envisecrets.com at :8081, and terminate TLS at the proxy. The repo ships a ready-to-use Traefik config under traefik/, 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.

  8. Lock down the firewall
    sudo ufw allow OpenSSH
    sudo ufw allow 80,443/tcp
    sudo ufw enable
    

    This 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.

  9. Verify for real
    curl https://api.envisecrets.com/health
    curl https://install.envisecrets.com/
    

    The second should print the actual install.sh contents — at that point curl -fsSL https://install.envisecrets.com | sh works 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.