infisical
secrets
devops
self-hosting
engineering

One Source of Truth for Secrets: Self-Hosting Infisical on a Raspberry Pi

Why I moved every API key out of scattered .env files into a single self-hosted Infisical instance running on a Raspberry Pi — how it beats the cloud vaults, and how one copy of each key feeds both local dev and Vercel.

June 8, 202611 min readviews

I have a handful of hobby apps. None of them are unicorns, but together they share a surprising amount of plumbing: the same Gemini key, the same Groq key, a GitHub token, an Expo token, an Apple App Store Connect key. For a long time every one of those values lived in a .env.local file — copied between repos, re-pasted whenever I moved machines, and quietly drifting out of sync.

That setup has three problems. Secrets end up in more places than you can track, rotating a shared key means editing N files, and the moment one .env lands in a commit you have a bad day. So I did the thing I'd been putting off: I stood up a single secrets manager that every project reads from, in both development and production. I picked Infisical, and I self-host it on a Raspberry Pi.

This post is why I chose it over the cloud vaults, and exactly how the setup works.

The problem with .env files

A .env file is fine for one app on one machine. It stops being fine the moment you have:

  • Shared keys. My GOOGLE_GENERATIVE_AI_API_KEY is the same in five repos. Five copies means five things to rotate and five things to leak.
  • Two environments per app. The dev key and the prod key need to live somewhere that isn't a file you might git add by accident.
  • More than one machine. PC, laptop, a VM. Every new checkout is a scavenger hunt for "which .env had the right values".

What I wanted was one place where each secret exists once, that both my local machine and my deploy platform read from, that I fully control, and that costs me nothing per secret.

What Infisical is

Infisical is an open-source (MIT-licensed) secrets platform — think "the API-key manager you'd build if you cared about DX". It does the obvious things (store secrets, organize them into projects and environments) and a lot of non-obvious ones: secret versioning and point-in-time recovery, automatic rotation, dynamic secrets, a Kubernetes operator, native syncs to platforms like Vercel, and built-in PKI/SSH management.

The part that matters for me: it can be fully self-hosted for free. The whole thing runs on a boring, well-understood stack — PostgreSQL for storage and Redis for cache — packaged as Docker images. That's exactly the kind of thing a Raspberry Pi is good at.

Why not Azure Key Vault, AWS, or HashiCorp Vault?

I priced out the usual suspects. For a solo developer with a pile of side projects, the economics and ergonomics are lopsided.

ToolPricing modelWhat it costs meSelf-host
AWS Secrets Manager~$0.40 per secret / month + $0.05 / 10k API callsPer-secret billing punishes "lots of small keys across lots of apps"No
Azure Key Vault~$0.03 / 10k operations (Standard); Premium HSM ~$1 / key / monthCheap, but Azure-shaped — best when you're already all-in on AzureNo
HashiCorp VaultHCP from ~$22/mo (dev) up to ~$1,000/mo (prod), plus per-client feesPowerful, operationally heavy, and the per-client pricing balloons fastOSS yes, but a chore to run
InfisicalCloud ~$22 / user / month — or $0 self-hostedOne Pi, unlimited secrets, unlimited projects, no per-secret taxYes, first-class

The takeaways:

  • AWS bills per secret. That's the exact wrong shape for my usage — I have many small keys spread across many tiny apps. The bill scales with the thing I have most of.
  • Azure Key Vault is genuinely cheap (no per-secret fee, just per-operation), and if I lived in Azure I'd think hard about it. But its mental model is Azure's, the DX is enterprise-IT-flavored, and it's another cloud account to babysit for projects that don't otherwise touch Azure.
  • HashiCorp Vault is the most capable of the bunch and the gold standard at scale — but it's also the heaviest to operate, and HCP's per-client pricing is built for companies, not hobby projects.
  • Infisical self-hosted is free, unlimited, and mine. No per-secret line item, no per-seat fee, no vendor holding my keys. The trade is that I run it — which on a Pi is a one-time afternoon, not an ongoing cost.

For a learning/side-project portfolio, "free, unlimited, self-owned, great DX" wins easily. The cloud vaults make sense when someone else is paying the AWS bill and compliance demands a managed HSM.

Why self-host (beyond the price)

The $0 is nice, but it isn't the main reason.

  • I own the data. My API keys never leave hardware I physically control. There's no third party that can be breached, subpoenaed, or have an outage that locks me out of my own secrets.
  • No artificial limits. Unlimited secrets, projects, and environments. I'm not rationing keys to stay under a free-tier cap.
  • It's a learning project in itself. Running Postgres + Redis + a reverse proxy + zero-trust access on a Pi is a genuinely useful thing to know how to do.
  • It composes with the rest of my homelab. The same Pi, the same Cloudflare account, the same backup routine.

My setup: a Pi behind Cloudflare Access

The instance runs on a Raspberry Pi via Docker (the Infisical app container plus Postgres and Redis). The interesting part is how it's exposed to the internet — because a secrets manager is the last thing you want sitting on an open port.

Instead of forwarding a port on my router, I put it behind a Cloudflare Tunnel and gate it with Cloudflare Access. So there are two independent layers of auth before anything reaches a secret:

  1. Cloudflare Access — the zero-trust layer in front of the whole instance. Nothing reaches Infisical at all without passing the Access policy. Humans log in through Cloudflare; automation uses a service token.
  2. Infisical's own auth — a machine identity (Universal Auth) scoped read-only to a single environment. Even past Cloudflare, a leaked CI credential can only read the keys it was granted.
┌──────────┐   CF Access    ┌───────────────┐   Infisical auth   ┌─────────┐
│  client  │ ─────────────▶ │ Cloudflare    │ ─────────────────▶ │ Pi:     │
│ (CLI/CI) │  service token │ Tunnel/Access │  machine identity  │ Infisical│
└──────────┘                └───────────────┘                    └─────────┘

The only secret material that lives on my laptop is the bootstrap credential to reach Infisical — the Cloudflare Access service token and the machine-identity client id/secret, in a single gitignored .infisical.env. Every actual app secret lives in Infisical and is fetched at runtime.

One copy of each key: shared vs. per-project

This is the structure that finally killed my duplication problem. I split secrets into two kinds:

  • Per-project secrets live in that project's own Infisical project — its database URL, its auth secret, its OAuth credentials.
  • Shared keys live once in a dedicated Global-Secrets project: the Gemini key, the Groq key, the Anthropic/OpenAI keys, the GitHub and Expo tokens, the Apple App Store Connect key. Things every app uses.

Rotate the Gemini key once in Global-Secrets, and every project picks up the new value on its next run. No fan-out, no "did I update all five repos".

A nuance worth knowing: cross-project secret imports are a paid feature, so on a free self-hosted tier you can't have a project natively pull from Global-Secrets. The fix is to do the merge at the two points where secrets are actually consumed — local dev and the deploy platform — instead of inside Infisical. That's exactly what the wrapper below does.

Development: the with-env wrapper

Locally, I never read a .env. Every package.json script that needs secrets runs through a tiny wrapper that shells out to infisical run. The clever bit is that it nests two runs — the shared project on the outside, the project's own secrets on the inside — so a single command gets both sets, with the project's values winning on any collision:

// tooling/with-env/with-env.mjs (the core decision — unit-tested, no I/O)
export function resolveExecution({ env, args }) {
  const [command, ...commandArgs] = args;
 
  // On Vercel the platform already injects env (and the CLI isn't installed),
  // so just run the command untouched. This guard keeps `next build` working
  // in CI even though the same script runs through Infisical locally.
  if (env.VERCEL) return { mode: "passthrough", command, commandArgs };
 
  const projectRun = buildRunArgs(env, env.INFISICAL_PROJECT_ID, "dev");
  const sharedId = env.INFISICAL_SHARED_PROJECT_ID;
 
  // Nest: infisical run <Global-Secrets> -- infisical run <project> -- <cmd>
  // Shared is outermost, so the project run executes LAST and wins on collisions.
  if (sharedId && sharedId !== env.INFISICAL_PROJECT_ID) {
    const sharedRun = buildRunArgs(env, sharedId, "prod");
    return {
      mode: "infisical",
      command: "infisical",
      commandArgs: [...sharedRun, "--", "infisical", ...projectRun, "--", command, ...commandArgs],
    };
  }
  return { mode: "infisical", command: "infisical", commandArgs: [...projectRun, "--", command, ...commandArgs] };
}

So pnpm dev becomes, under the hood:

infisical run --env=prod --projectId=<Global-Secrets> -- \
  infisical run --env=dev --projectId=<this-app> -- \
    next dev

Two things I baked in from experience:

  • Fail loudly. If the bootstrap credentials are missing, the wrapper throws a clear error instead of silently running with no secrets — a missing key should be a loud crash, not a mysterious 500 later.
  • Short-lived tokens. The machine identity's client id/secret is exchanged for a fresh, short-lived access token on each run. Nothing long-lived is written to disk.

The .env file isn't gone entirely — it survives as documented break-glass. If the Pi is ever unreachable, dotenv-cli -e .env -- <cmd> still works. But the .env.example committed to the repo is now just a contract — a list of which keys must exist in Infisical, with placeholder values — not a place real secrets live.

Production: syncing to Vercel

The apps deploy on Vercel, and here Infisical does the opposite of what it does locally. On Vercel I don't run the CLI (that's the VERCEL passthrough above) — instead Infisical pushes secrets into the Vercel project ahead of the build, using a native Secret Sync.

The same two-tier split applies:

  • Each project gets a Global-Secrets/prod → <project>/production sync, so the shared keys land in Vercel's environment automatically.
  • The project's own secrets sync into the same Vercel project.

A couple of hard-won notes:

  • Secret Syncs default to "deletion disabled." Removing a secret from the Infisical source does not delete it from Vercel. Usually what you want, but worth knowing so you're not surprised by orphaned values.
  • Let the database integration own DB vars. My Postgres lives on Neon, and the Vercel↔Neon integration writes POSTGRES_URL directly into Vercel. I do not mirror that into Infisical — a static copy goes stale the moment Neon rotates the pooler password. Infisical owns the hand-managed app secrets; the integration owns the database connection string. Each thing has exactly one owner.

So the full picture for any given key:

Local dev   →  with-env nests two `infisical run`s  →  process.env
Production  →  Infisical Secret Sync → Vercel env    →  process.env

Same secret, one source of truth, two delivery mechanisms — and my code just reads process.env.GOOGLE_GENERATIVE_AI_API_KEY in both places, none the wiser.

Gotchas I hit (so you don't)

A few things cost me time:

  • Infisical's UI stages changes. Adding a secret only stages it — you have to click Save Changes or it's discarded on navigation. A staged-but-unsaved secret reads as "0 secrets" from the CLI, which sends you chasing a ghost.
  • Shared keys must live in the environment the shared run reads. My wrapper reads Global-Secrets at the prod environment. A key I once added to the shared project's development environment was simply invisible — the command ran, the variable just wasn't there. Keep shared keys in prod.
  • App Connections are per-project on Vercel. Each Vercel project needs its own connection before you can create its Global-Secrets sync — you can't reuse one project's connection for another.

Takeaway

For a portfolio of small apps, a self-hosted Infisical on a Raspberry Pi hits a sweet spot the cloud vaults can't: free and unlimited, fully under my control, with one copy of every shared key feeding both local dev and production. The cloud vaults are the right call when someone else pays the bill and compliance wants a managed HSM — but for me, a Pi behind Cloudflare Access, nesting two infisical runs locally and syncing to Vercel in prod, replaced a sprawl of .env files with a single source of truth.

The best part: the next time I rotate a key, I do it once.


Tools mentioned, for the curious: Infisical (MIT, self-hostable), AWS Secrets Manager, Azure Key Vault, and HashiCorp Vault.

Comments

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

Loading comments…