Give your homelab secrets a proper home with Infisical (#dev #homelab #docker #security #selfhosted)

Overview
Quick question: how many .env files are scattered across your projects right now, and how many of them contain the
same database password? Yeah, I thought so. If you already run a homelab, you have everything you need to fix that.
In this post we'll self-host Infisical, an open-source secrets manager, and then read a secret from it in Python.
Before we continue, I think it's important to say that this is not an ad or sponsored post. I found out about this solution, added to my homelab, and I'm pretty happy with it. So I decided to share because maybe this can help someone else.
The .env sprawl
It always starts small. One .env for that Python script, an appsettings.json for the C# API, a password inline in
a docker-compose.yml because you were going to move it later (you weren't). A few months in, the same Postgres
password lives in six places, and you're not sure which projects still use that weather API key.
This was already annoying, but now that everybody is vibe-coding and creating microslop left and right, it got worse. Coding agents read your working tree, and whatever is sitting there can end up in a commit, a log file, or a pasted chat transcript. An agent can't leak a secret that was never on disk in the first place.
And rotating a leaked key that's copied across a dozen files? That's the kind of chore that makes people just... not rotate keys.
I know, I know: You're a "smart vibe-coder" and added all the latest and brightest harnesses to your agent, so it won't be able to read your secrets... right? right?!
Well, let's keep on talking about secret management. :)
Enter Infisical
Infisical is an open-source secrets manager. You store each secret once, organized by project, environment (dev/staging/prod), and folder path, and your apps fetch them at runtime through an SDK, the CLI, or the REST API. If you've used Azure Key Vault or AWS Secrets Manager, it's the same idea, except this one runs on your own hardware.
You get a web dashboard to manage everything, machine identities so your apps authenticate as themselves instead of borrowing your personal login, and a record of who read or changed what.
What does the free plan include?
As you know, open source doesn't mean free, but they do have a free tier and it includes quite a lot, actually. The dashboard, the API, CLI, SDKs, the Kubernetes operator, webhooks, machine identities, secret referencing, secret scanning, and the integrations (GitHub Actions, Terraform, and friends). For a homelab, that covers everything you'll realistically need.
Some features are license-gated even when you self-host: secret versioning with point-in-time recovery, RBAC, automatic secret rotation, SAML SSO, and longer audit log retention. Good to know they exist, but I haven't missed them at home.
Since nothing is perfect, there's a "cost" with the self-hosted, free tier
Before you copy-paste anything, let's be fair about those costs:
- Three more containers to babysit. Infisical needs Postgres and Redis. If you already run Postgres for other things (most homelabs do), that's only two.
- It becomes a dependency. If your apps fetch secrets at startup and Infisical is down, they don't start. For a homelab that's usually acceptable, but keep it in mind before putting anything critical behind it.
- Backups matter now. All your secrets live in that Postgres database, encrypted with the
ENCRYPTION_KEYyou'll generate below. Lose the key and your database backups become confetti. Store the key (and a database dump) somewhere safe, outside the box that runs Infisical. - The bootstrap problem doesn't fully go away. Your app still needs two environment variables to authenticate to Infisical. The win is that it's always the same two, they're scoped and revocable, and everything else lives in one place.
Still with me? Let's build it.
Setting it up
We'll run three containers on a shared Docker network: Postgres, Redis, and Infisical itself. One heads-up before we
start: the passwords below are placeholders, and now that this post is published, they're about as secret as password1,
so generate your own.
Step 0: the network
The containers find each other by name on a user-defined network:
1docker network create my-docker-network
Step 1: Postgres
This is where Infisical persists everything.
1docker pull postgres:17
2docker run \
3 -d \
4 --name postgres \
5 --restart=unless-stopped \
6 --network=my-docker-network \
7 -e POSTGRES_PASSWORD=SuperDuperPostgres42 \
8 -e PGDATA=/var/lib/postgresql/data/pgdata \
9 -p 5432:5432 \
10 --health-cmd='pg_isready -U postgres || exit 1' \
11 --health-interval=30s \
12 --health-timeout=10s \
13 --health-retries=3 \
14 --health-start-period=10s \
15 -v /mnt/postgres/data:/var/lib/postgresql/data \
16 postgres:17
If you already have a Postgres instance on that network, skip this and reuse it.
Step 2: Redis
Infisical uses Redis for caching and for its job queue (BullMQ), and that second part shapes the whole config. A default Redis with eviction enabled would silently drop queued jobs under memory pressure, so we run it with a small config file:
1# /srv/infisical/redis/redis.conf
2
3# --- The important one --------------------------------------------------
4# Infisical runs its job queue (BullMQ) on this instance, in addition to
5# caching. Eviction would silently drop queued jobs. Do NOT change this
6# to allkeys-lru.
7maxmemory-policy noeviction
8
9# Guardrail so a runaway can't eat the host. With noeviction, hitting this
10# limit means writes get rejected (OOM error) instead of evicted, so set
11# it generously. Infisical's working set is tiny; 512mb is huge headroom.
12maxmemory 512mb
13
14# --- Auth ----------------------------------------------------------------
15# Redis has NO authentication by default. Any container on the shared
16# docker network could otherwise read/write this instance freely.
17requirepass SuperDuperRedis42
18
19# --- Persistence ---------------------------------------------------------
20# AOF preserves pending queue jobs across restarts. Cheap insurance.
21appendonly yes
22appendfsync everysec
23
24# --- Don't touch ---------------------------------------------------------
25# timeout 0 (default): BullMQ workers hold idle blocking connections.
26# A nonzero client timeout would kill them. Leave it at 0.
27timeout 0
Then the container. REDISCLI_AUTH is there so the health check's redis-cli ping can authenticate without the
password showing up in the command line:
1docker pull redis:alpine
2docker run -d \
3 --name redis \
4 --network=my-docker-network \
5 --restart unless-stopped \
6 -v /srv/infisical/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro \
7 -v /srv/infisical/redis/data:/data \
8 -e REDISCLI_AUTH="SuperDuperRedis42" \
9 --health-cmd "redis-cli ping | grep -q PONG" \
10 --health-interval=10s \
11 --health-timeout=3s \
12 --health-retries=5 \
13 redis:alpine \
14 redis-server /usr/local/etc/redis/redis.conf
Step 3: secrets for the secrets manager
Yes, I see the irony. Infisical needs two values of its own:
1openssl rand -hex 16 # -> ENCRYPTION_KEY (32-char hex)
2openssl rand -base64 32 # -> AUTH_SECRET (base64, for JWT signing)
ENCRYPTION_KEY encrypts your secrets at rest; AUTH_SECRET signs the session tokens. Save both in your password
manager right now, before you lose the terminal scrollback. The ENCRYPTION_KEY especially: without it, the database
contents are unrecoverable. That's the whole point of it.
Step 4: prepare the database
Infisical gets its own database and a user that owns it. Connect to Postgres:
1docker exec -it postgres psql -U postgres
And run:
1CREATE DATABASE infisical;
2CREATE USER infisical WITH PASSWORD 'SuperDuperInfisical42';
3ALTER DATABASE infisical OWNER TO infisical;
4
5-- Switch to the new database before the next part. This is important.
6\c infisical
7
8ALTER SCHEMA public OWNER TO infisical;
9GRANT ALL ON SCHEMA public TO infisical;
10CREATE EXTENSION IF NOT EXISTS pgcrypto;
Remember, if you don't use \c infisical, the schema ownership and the extension will be set inside default database.
Step 5: the main course
Now Infisical itself. Paste in the two values from step 3:
1docker pull infisical/infisical:latest
2docker run -d \
3 --name infisical \
4 --network=my-docker-network \
5 --restart unless-stopped \
6 -e NODE_ENV=production \
7 -e ENCRYPTION_KEY="paste-the-hex-key-here" \
8 -e AUTH_SECRET="paste-the-base64-secret-here" \
9 -e DB_CONNECTION_URI="postgresql://infisical:SuperDuperInfisical42@postgres:5432/infisical" \
10 -e REDIS_URL="redis://:SuperDuperRedis42@redis:6379" \
11 -e SITE_URL="http://192.168.0.42:8080" \
12 -e TRUSTED_PROXY_CIDRS="172.16.0.0/12,192.168.0.0/16" \
13 -p 8080:8080 \
14 --health-cmd "node -e \"require('http').get('http://127.0.0.1:8080/api/status',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))\"" \
15 --health-interval=15s \
16 --health-timeout=5s \
17 --health-retries=5 \
18 --health-start-period=40s \
19 infisical/infisical:latest
A few notes on this one:
SITE_URLmust be an absolute URL, protocol included. It's used for links in emails and OAuth redirects, so point it at the address you'll actually type in the browser. You can skip that if you don't use any of those features.TRUSTED_PROXY_CIDRSonly matters if Infisical sits behind a reverse proxy: it tells Infisical which proxies to trust for forwarded-IP headers, so the audit log records real client IPs instead of the proxy's. If you expose the port directly, you can drop it.- If the database user's password has special characters, URL-encode them in
DB_CONNECTION_URI. - I'm using
latestto keep this copy-pasteable, but on your homelab, pin a version tag and upgrade on purpose, not by accident. (Or keep it atlatestand let's play upgrade roulette! :D)
The first boot runs the database migrations, so give it a minute and watch docker logs -f infisical if you're
curious. Once it's up, open the site in your browser and create your admin account. From there, create a
project, pick an environment, and add your first secrets. That part is all point-and-click.
A quick checklist for you:
- In the general area, go to Access Control and create a new Machine Identity.
- For that new identity, create a client secret.
- Create a Project
- Add secrets to it
- Then on access control (for the project), add the Machine Identity.
- and lastly, give that identity a role in your project. (Viewer should be enough.)
Reading a secret from Python
Storing secrets is only half the job; the apps need to fetch them. For that, Infisical has machine identity (from the previous section, remember?) credentials that belong to a service instead of a person, so you can scope and revoke them without touching your own account.
Then, in your project:
1pip install infisicalsdk
1import os
2from infisical_sdk import InfisicalSDKClient
3
4
5client = InfisicalSDKClient(host="http://infisical:8080")
6
7# Authenticate with a machine identity (Universal Auth)
8client.auth.universal_auth.login(
9 client_id=os.environ["INFISICAL_CLIENT_ID"],
10 client_secret=os.environ["INFISICAL_CLIENT_SECRET"],
11)
12
13secret = client.secrets.get_secret_by_name(
14 secret_name="DATABASE_URL",
15 project_id="your-project-id",
16 environment_slug="prod", # matches your environment slug (dev/staging/prod)
17 secret_path="/",
18)
19
20print(secret.secretValue)
A few things worth pointing out:
- The
hostabove works for a container on the same Docker network, whereinfisicalresolves by name. From your desktop, use the same address asSITE_URLor something likelocalhost:8080. - The only secrets left in the environment are the client ID and client secret. Everything else is fetched at runtime and held in memory, so nothing ends up committed to a repo or baked into an image.
- The read is scoped to one project, one environment, one path. A leaked dev identity can't read prod.
Rotating a password stops being a scavenger hunt: change it in Infisical, restart the consumers, done.
Where to go from here
If you don't want to touch code at all, the Infisical CLI can inject secrets as environment variables into any
process (infisical run -- python main.py), and there are SDKs for most languages plus integrations for GitHub
Actions, Kubernetes, and Terraform. Same vault, many doors.
Your homelab is the perfect place to practice doing this properly. The next time a project needs a connection string, it already has a home.
If you want a ready-to-use example, I created this snippet.
Hope it helps! :)
References: