← back to the garden

evergreen

Self-Hosting Infisical for Local Development

planted · March 23, 2026
#devops#secrets-management#docker#developer-experience

TL;DR: We replaced .env files with a self-hosted Infisical instance behind a VPN. Developers run infisical run --env=local -- pnpm dev and secrets are injected at runtime. No .env files to manage, share, or accidentally commit.

The Problem

Every team eventually hits the same pain points with .env files:

  • New developer joins, spends an hour asking teammates for env vars
  • Someone updates a secret, nobody else knows until things break
  • .env files get committed by accident despite .gitignore
  • Different developers have different values. "Works on my machine"
  • Secrets scattered across Slack DMs, Notion pages, and sticky notes

We wanted a single source of truth for development secrets that:

  1. Doesn't require a SaaS subscription
  2. Works offline (within our network)
  3. Doesn't change how developers run code
  4. Has a fallback for when it's unavailable

Why Self-Hosted Infisical

We evaluated several options:

Option Pros Cons
Infisical Cloud Zero setup Monthly cost, secrets leave your network
HashiCorp Vault Battle-tested, enterprise features Complex setup, overkill for dev secrets
Doppler Great DX, CLI is solid SaaS-only, per-seat pricing
AWS Secrets Manager Already using AWS Awkward for local dev, IAM overhead
Self-hosted Infisical Free, simple, stays in-network You maintain it

Infisical's Community Edition is free, runs as a single Docker container, has a clean dashboard, and ships with a CLI that wraps any command with injected env vars. That last part is what sold us. Zero code changes required.

Architecture

flowchart TB
    subgraph dev["Developer Machine"]
        cmd["infisical run --env=local\n--path=/api -- pnpm dev"]
        cli["Infisical CLI"]
        inject["env vars injected"]
        app["pnpm dev"]
        cmd --> cli
        cli --> inject
        inject --> app
    end

    subgraph server["Devtools Server (VPN-only)"]
        infisical["Infisical (Docker)\n:8080"]
    end

    cli -- "Tailscale VPN" --> infisical

Infisical runs on a utility server that's only reachable via our Tailscale VPN mesh. No public endpoint, no exposed ports. If you're not on the VPN, you can't reach it.

Setting Up the Server

1. Deploy Infisical

On your devtools/utility instance:

# Create a directory for Infisical
mkdir -p /opt/infisical && cd /opt/infisical

# Download the docker-compose file
curl -o docker-compose.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml

# Generate encryption keys
openssl rand -hex 16  # ENCRYPTION_KEY
openssl rand -hex 32  # AUTH_SECRET

# Create .env with your keys
cat > .env <<EOF
ENCRYPTION_KEY=<generated-hex-16>
AUTH_SECRET=<generated-hex-32>
SITE_URL=http://<vpn-ip>:8080
EOF

# Start
docker compose up -d

2. Initial Configuration

  1. Open http://<vpn-ip>:8080 (via VPN)
  2. Create your organization
  3. Create your first project (one per repository)
  4. Set up environments: local, dev, staging, prod
  5. Create folders per service (for monorepos)

3. Lock Down Access

  • Prod environment: read-only for most developers, write access for leads only
  • No public endpoint: VPN is the only way in
  • Auth keys: create per-integration, rotate quarterly

Organizing Secrets

One Project Per Repo

Don't mix repositories in a single project. Access control, audit logs, and environment separation all work at the project level.

graph TD
    org["Organization"]
    org --> backend["Project: backend"]
    org --> frontend["Project: frontend"]
    org --> mobile["Project: mobile"]

Folders for Monorepos

If your repo has multiple services, use folders. No inheritance. Duplicate shared keys across folders. This sounds redundant, but it eliminates surprises. Each folder is a self-contained set of everything that service needs.

/api
/worker
/admin
/infra     # Internal IPs, endpoints

Four Environments

Environment Purpose
local Points to localhost (Docker Postgres, Redis)
dev Points to dev infrastructure
staging Points to staging infrastructure
prod Production values (restricted access)

Integrating With Your Repository

The Config File

Add .infisical.json to your repo root (this is safe to commit, no secrets):

{
  "workspaceId": "<project-id-from-dashboard>",
  "defaultEnvironment": "local",
  "gitBranchToEnvironmentMapping": null
}

Setting defaultEnvironment to local means running infisical run without --env defaults to local. This prevents anyone from accidentally pulling prod secrets.

Package Scripts

infisical run does the heavy lifting. It fetches secrets, injects them as env vars, and runs your command. No SDK, no code changes, no dotenv import.

Single-service repo:

{
  "scripts": {
    "dev": "infisical run --env=local -- next dev",
    "dev:staging": "infisical run --env=staging -- next dev",
    "dev:local": "next dev"
  }
}

Monorepo:

{
  "scripts": {
    "dev:api": "infisical run --env=local --path=/api -- nest start --watch",
    "dev:api:dev": "infisical run --env=dev --path=/api -- nest start --watch",
    "dev:api:local": "nest start --watch"
  }
}

The :local variant is the escape hatch. It runs without Infisical using whatever .env files are present. Always provide this.

Env Example Files

Keep .env.*.example files committed. They document what each service needs without containing values:

# .env.api.example
DATABASE_URL=
REDIS_URL=
JWT_SECRET=

These serve as the reference when onboarding, and as the fallback template when Infisical isn't available.

Pre-commit Hook

Block .env files from ever being committed:

#!/bin/sh
if git diff --cached --name-only | grep -E '(^|/)\.env($|\.)' | grep -qv '\.example$'; then
  echo ".env files cannot be committed. (.env.*.example files are allowed)"
  exit 1
fi

Developer Workflow

Onboarding (One-Time)

# Install the CLI
brew install infisical/get-cli/infisical

# Connect to VPN
# (follow your team's VPN setup guide)

# Login to Infisical
infisical login --domain=http://<vpn-ip>:8080

# Clone and run
git clone <repo>
pnpm install
pnpm dev:api

No .env files to hunt down. No "ask Rahul for the JWT secret".

Day-to-Day

# Start with local env (default)
pnpm dev:api

# Point to dev infrastructure
pnpm dev:api:dev

# Check what secrets are set
infisical secrets --env=local --path=/api

Adding a New Secret

  1. Add to Infisical dashboard, all 4 environments and the relevant folder
  2. Add to .env.*.example with an empty value
  3. Reference in application config
  4. Commit the .example file change

When Infisical Is Unavailable

VPN down? Infisical server restarting? Use the fallback:

# One-time: export from a teammate who has access
infisical export --env=local --path=/api --format=dotenv > .env.api.local

# Run without Infisical
pnpm dev:api:local

The .env.api.local file is gitignored. It drifts over time, so treat it as a temporary escape hatch, not a primary workflow.

What About CI/CD and Production?

Infisical is for local development only. We don't use it in CI/CD or production. Deployed environments use cloud-native secret managers:

  • AWS Secrets Manager for database credentials
  • SSM Parameter Store for non-secret config
  • EC2 instance roles / IAM for service-level access

This keeps the blast radius small. If the Infisical instance goes down, production is unaffected. Developers just fall back to .env files until it's back.

Gotchas

Session expiry. Infisical CLI sessions expire. When you see invalid signature errors, re-login:

infisical login --domain=http://<vpn-ip>:8080

Export creates .env files. The export command is useful for debugging, but it reintroduces the very files you're trying to eliminate. Add a comment in your docs:

# Debugging only, don't commit or leave .env files around
infisical export --env=local --format=dotenv > .env.local

No inheritance. If /api and /worker both need DATABASE_URL, add it to both folders. Inheritance seems convenient but creates hidden dependencies and makes it hard to reason about what a service actually needs.

VPN is a hard dependency. If your team works across unreliable networks, consider running Infisical on a cloud instance with IP whitelisting instead of VPN-only access.

Was It Worth It?

For a team of 5-15 developers working on a monorepo with multiple services and environments, yes. The ROI showed up in:

  • Onboarding: went from "ask 3 people for env vars" to infisical login && pnpm dev
  • Secret rotation: update once in the dashboard, everyone gets it on next run
  • No more .env drift: the source of truth is the dashboard, not someone's laptop
  • Zero code changes: infisical run wraps any command, no SDK imports needed

The setup took about 2 hours (Docker deploy + VPN route + CLI setup). Ongoing maintenance is near-zero.

Secrets management for local dev doesn't need to be enterprise-grade. It needs to be convenient enough that developers actually use it instead of falling back to Slack DMs and .env files.