Self-hosting Infisical gives any application a central, provider-neutral place to store secrets, separate development, staging, and production values, and issue separate identities to developers and servers, all without tying your setup to Vercel, a single cloud provider, or a stack-specific SDK. This guide covers the complete implementation: standing up Infisical on a dedicated VPS behind HTTPS, structuring projects and environments, creating machine identities for staging and production, and injecting secrets into systemd services and Docker containers. Everything here works for any application that reads configuration from environment variables, and I'll use my own Next.js and Payload CMS stack as the running example throughout.
In the previous article, I covered why .env files stop being a sufficient secrets-management system once an application picks up multiple developers, deployment environments, servers, and external services. This one covers what replaces them. I wanted something that felt close to managing environment variables in Vercel, without being tied to Vercel, and that would work across local development, staging, production, Docker containers, background workers, and plain VPS deployments regardless of framework. Infisical is what I landed on, and this guide walks through the full setup.
What this guide covers
Self-hosting Infisical on a dedicated VPS
Putting it behind HTTPS
Creating development, staging, and production environments
Connecting a local project, whatever it's built with
Giving staging and production servers separate machine identities
Injecting secrets into a systemd service
Injecting secrets into Docker containers
Handling build-time variables correctly (using Next.js as the example)
Backing up the Infisical database and encryption keys
Rotating secrets without falling back to copied .env files
The end result looks like this regardless of what you're running:
text
Developer laptop
└── Infisical CLI
└── development secrets
Staging VPS
└── staging machine identity
└── staging secrets
Production VPS
└── production machine identity
└── production secrets
Infisical VPS
├── Infisical
├── PostgreSQL
├── Redis
└── HTTPS reverse proxy
Your application keeps reading ordinary environment variables through whatever mechanism your language or framework already uses, process.env in Node.js being the obvious one. Infisical controls where those values are stored, who may retrieve them, and which environment each application receives. The application code itself doesn't need to know Infisical exists.
What this guide assumes
This guide assumes Ubuntu or Debian on the servers, Docker and Docker Compose on the Infisical VPS, a domain such as secrets.example.com, separate staging and production servers, and a small team or small-scale production deployment. The application examples use Next.js with Payload CMS and pnpm, since that's the stack I run most often, but nothing about Infisical itself is specific to that combination. Swap in a Django app, a Go service, a Rails app, or anything else that reads configuration from the environment, and the Infisical side of this guide stays the same.
Infisical's official Docker Compose template runs the application, PostgreSQL, and Redis on a single Docker host. Infisical documents this deployment model as suitable for development, testing, proofs of concept, and smaller deployments that don't require high availability, with a documented minimum of two CPU cores, 4 GB RAM, and 20 GB disk for the complete stack, and higher resources recommended beyond that.
For this guide, I'd use at least:
text
4 vCPU
8 GB RAM
50 GB SSD
A smaller server works fine for testing, but a secrets manager is infrastructure that every one of your applications may depend on during startup, so I'd rather give it comfortable headroom from the start. For a larger organization or a high-availability requirement, run Infisical against external PostgreSQL and Redis services or a more resilient orchestration setup.
The target architecture
The Infisical server should sit separately from your application servers, regardless of what those application servers run.
text
Internet
│
▼
secrets.example.com
│
▼
Nginx with TLS
│
▼
Infisical container
├── PostgreSQL container
└── Redis container
Application servers connect to the Infisical API over HTTPS:
text
Production VPS
│
├── Client ID
├── Client Secret
│
▼
Infisical Universal Auth
│
▼
Short-lived access token
│
▼
Production secrets
│
▼
Application process
The application server never stores the database password, application secret, storage credentials, SMTP password, and every external API key in a local .env.production file. It stores one restricted bootstrap credential that lets it request the correct secrets from Infisical, and that's true whether the process at the bottom of that chain is Next.js, Payload, or something else entirely.
Step 1: Prepare the Infisical VPS
Start by creating a dedicated installation directory:
bash
sudomkdir -p /opt/infisical
sudochown"$USER":"$USER" /opt/infisical
cd /opt/infisical
Verify that Docker and Docker Compose are installed:
bash
docker --version
docker compose version
Download the official Docker Compose template and example configuration:
The downloaded .env contains example credentials that must not be used in production. Infisical explicitly marks the example encryption and authentication values as unsafe for production use.
Step 2: Pin the Infisical image
The current official Compose template contains:
yaml
image:infisical/infisical:latest
It also includes a comment telling operators to pin the image to a specific tag. Open the file:
bash
nano docker-compose.prod.yml
Change:
yaml
image:infisical/infisical:latest
To a specific version that you've reviewed:
yaml
image:infisical/infisical:<PINNED_VERSION>
A floating latest tag has no place in infrastructure that stores the credentials for every other application you run. The same principle applies to PostgreSQL and Redis: pin versions, test upgrades in a non-production environment, back up the database, and upgrade deliberately.
Step 3: Bind Infisical to localhost
The official template publishes the backend directly on port 80:
yaml
ports:-80:8080
I prefer to expose Infisical only through the reverse proxy. Change the backend port mapping to:
yaml
services:backend:ports:-"127.0.0.1:8080:8080"
The service is now reachable from the VPS itself at http://127.0.0.1:8080 and stays off the public network interface. Nginx will terminate TLS and forward requests to this local port.
Step 4: Generate the platform secrets
Infisical requires an encryption key and an authentication secret. Generate the encryption key:
bash
openssl rand -hex 16
Generate the authentication secret:
bash
openssl rand -base64 32
Generate a PostgreSQL password using hexadecimal characters so it can safely appear inside a connection URI:
bash
openssl rand -hex 24
Infisical currently documents ENCRYPTION_KEY as a random 16-byte hexadecimal string and AUTH_SECRET as a random 32-byte Base64 string. Open the configuration:
bash
nano /opt/infisical/.env
Replace the sample values with your generated values:
Leave the rest of the optional integration settings empty unless you need them. The platform requires an absolute SITE_URL, including the protocol. PostgreSQL is the persistent data layer here, and Redis handles caching and background work.
Check the permissions again:
bash
chmod 600 /opt/infisical/.env
Step 5: Start Infisical
Start the stack:
bash
cd /opt/infisical
docker compose \
-f docker-compose.prod.yml \
up -d
Make sure the DNS record points to the Infisical VPS:
text
secrets.example.com A <INFISICAL_VPS_IP>
Then request the certificate:
bash
sudo certbot \
--nginx \
-d secrets.example.com
Verify the result:
bash
curl -fsS https://secrets.example.com/api/status
Running the public Infisical interface over plain HTTP isn't an option here. Infisical uses secure cookies for authenticated sessions, and its own documentation recommends SSL for self-hosted deployments.
Step 7: Create the administrator account
Open https://secrets.example.com and create your account as soon as the installation becomes reachable. In the Docker Compose installation flow, the first registered user becomes the instance administrator, so this step needs to happen before the instance is shared or widely exposed. For a stricter initial setup, temporarily restrict port 443 to your IP address while creating the first account.
After creating the administrator, configure SMTP for password resets and invitations, invite the developers who need access, enable multi-factor authentication, review whether public sign-up should stay available, and confirm that only the reverse proxy can reach the backend port.
Step 8: Configure SMTP
Infisical uses email for invitations, password resets, and notifications. Add the SMTP configuration to /opt/infisical/.env:
cd /opt/infisical
docker compose \
-f docker-compose.prod.yml \
restart backend
Test the password-reset flow before relying on the instance for production infrastructure.
Step 9: Create the application project
In Infisical, create a Secrets Management project:
text
Project name: My Application
Configure three environments:
text
Development dev
Staging staging
Production prod
Infisical projects organize secrets into environments, folders, and individual values, and the same key can exist in each environment with a different value. Using a Next.js and Payload stack as an example:
Key
Development
Staging
Production
DATABASE_URL
Local database
Staging database
Production database
PAYLOAD_SECRET
Development value
Staging value
Production value
S3_BUCKET
Development bucket
Staging bucket
Production bucket
BREVO_API_KEY
Test or development key
Staging key
Production key
Keep the key names consistent across environments regardless of what framework is consuming them. A starting set for a Payload application might look like:
A different stack would list different keys, but the structure, one project, three environments, consistent naming, stays the same. You can import an existing .env file through the Infisical interface, but review every value first and make sure it lands in the correct environment.
Step 10: Keep .env.example in Git
Infisical becomes the source of truth for the actual secret values, but the repository should still document which variables the application expects. Create:
For a production server, pin the CLI version instead of silently installing an arbitrary future release. Infisical explicitly recommends version pinning for production environments.
Step 12: Connect your local project
Navigate to the application directory, whatever it contains:
bash
cd /path/to/my-application
Log in:
bash
infisical login
The current CLI can interactively select a self-hosted instance, or you can define your domain explicitly:
Infisical's project configuration file stores the project reference, default environment, and optional self-hosted domain. The documentation notes that this file is usually committed to the repository, so review the configured domain carefully before committing it. The file itself never contains the secret values.
Step 13: Run your application locally through Infisical
Your application code doesn't need to import an Infisical SDK. For a Next.js and Payload project, Payload keeps reading its configuration from process.env exactly as before:
Payload officially supports reading environment variables directly from process.env inside its configuration, and the same is true for essentially any framework that reads configuration from the process environment. Start the application through Infisical:
bash
infisical run --env=dev -- pnpm dev
Infisical retrieves the development values and injects them into the pnpm dev process. Swap pnpm dev for whatever your stack uses to start locally, python manage.py runserver, go run ., rails server, and the pattern holds. You can add a convenience script:
json
{"scripts":{"dev":"next dev","dev:secrets":"infisical run --env=dev -- pnpm dev","build":"next build","start":"next start"}}
Run:
bash
pnpm dev:secrets
Verify that a value exists without printing the value itself:
bash
infisical run --env=dev -- \
node -e "console.log('DATABASE_URL available:', Boolean(process.env.DATABASE_URL))"
Expected output:
text
DATABASE_URL available: true
Step 14: Handle build-time variables (the Next.js case)
Runtime injection through infisical run covers most frameworks completely, but a few tools also embed configuration into a build artifact rather than reading it purely at runtime. Next.js is the clearest example, so it's worth walking through even in an otherwise stack-agnostic setup.
Server-side values like these should never be prefixed with NEXT_PUBLIC_:
Next.js replaces NEXT_PUBLIC_ references during next build, embedding the values directly into the browser bundle, where they stay fixed after the build completes. If your framework has an equivalent build-time embedding step, check its documentation for the same distinction before wiring up Infisical, since the fix is always the same: run the build itself through infisical run so the values are present at build time, not just at process startup. Never solve a missing browser variable by marking a secret public.
Step 15: Create a production machine identity
Developers should log in as themselves. Servers, regardless of what they run, shouldn't use a developer account. For production, create a machine identity:
text
Organization Settings
→ Access Control
→ Identities
→ Create identity
Name it clearly:
text
my-application-production
Configure Universal Auth, which gives the machine identity a Client ID and Client Secret. The server exchanges these for a short-lived access token. Next, add the identity to the project:
Project: My Application
Environment: Production
Path: /
The production identity should never read development, staging, or unrelated client projects. Create a separate identity for staging:
text
my-application-staging
Give it access only to the staging environment. The identities stay separate because their permission sets are different, and that separation matters regardless of what's actually running on either server.
Step 16: Store the bootstrap credential on the VPS
The production server still needs an initial credential that lets it authenticate to Infisical. Create a dedicated application user if one doesn't already exist:
This file is the remaining secret-zero credential. It's still sensitive, but it's far narrower than a complete production .env file: it can be revoked independently, and its permissions can be limited to one project and one environment, no matter what application ends up consuming the secrets it unlocks.
Step 17: Create a reusable Infisical execution wrapper
Create:
bash
sudo nano /usr/local/bin/my-application-infisical
Add:
bash
#!/usr/bin/env bash# File: /usr/local/bin/my-application-infisicalset -euo pipefail
set -a
source /etc/my-application/infisical.env
set +a
export PATH="/usr/local/bin:/usr/bin:/bin"export INFISICAL_TOKEN="$(
infisical login \
--method=universal-auth \
--client-id="$INFISICAL_CLIENT_ID" \
--client-secret="$INFISICAL_CLIENT_SECRET" \
--silent \
--plain
)"exec infisical run \
--projectId="$INFISICAL_PROJECT_ID" \
--env="$INFISICAL_ENV" \
--path="/" \
-- "$@"
The wrapper does two things: it exchanges the Universal Auth credentials for a short-lived access token, then starts whatever command you hand it with the correct secrets injected. Infisical's CLI requires --projectId when authenticating through a machine identity, and it selects the environment and path through --env and --path. Because the wrapper just wraps a command, it works identically whether that command is pnpm start, gunicorn, or a compiled Go binary.
Step 18: Build and migrate with production secrets
Some frameworks, Next.js and Payload among them, need environment variables present while evaluating configuration during the build or a migration step, not just at process startup. Run the build through the wrapper:
bash
cd /srv/my-application/current
sudo -u my-application \
/usr/local/bin/my-application-infisical \
pnpm build
The migration now receives the production database connection from Infisical rather than from a local file. Run migrations before restarting the production application, and confirm a recent database backup exists first.
sudo systemctl status my-application
sudo journalctl -u my-application -f
The startup flow is now the same regardless of stack: systemd calls the execution wrapper, which logs in through Universal Auth, gets a short-lived access token, runs infisical run, and finally starts your application.
Step 20: Apply a changed secret
Suppose you replace BREVO_API_KEY. Update the value in the production environment inside Infisical. The currently running process will still have the old value in memory, since environment variables don't update inside an already running process. Restart the service:
bash
sudo systemctl restart my-application
Infisical supports a --watch option that restarts a command automatically when secrets change, but its documentation recommends that feature for development rather than production. I prefer explicit production restarts tied to a reviewed configuration change, and that preference holds regardless of what's running behind the wrapper.
Step 21: Use Infisical with Docker
For a Docker deployment, install the Infisical CLI inside the application image. For an Alpine-based image:
dockerfile
# File: Dockerfile
RUN apk add --no-cache bash curl
RUN curl -1sLf \
"https://artifacts-cli.infisical.com/setup.apk.sh" \
| sh
RUN apk add --no-cache infisical
The application image still starts its normal command; the entrypoint adds the authentication and secret-injection step in front of it. Infisical's Docker integration follows this same general pattern for any base image: install the CLI in the container and use infisical run as the wrapper around whatever the container was already going to run.
The container receives only the bootstrap configuration through the host file, and the actual application secrets get retrieved when the container starts. A user with unrestricted access to the Docker daemon effectively has root-level control over every container and its configuration, so access to Docker itself needs to stay restricted regardless of which secrets manager sits behind it.
Step 23: Handle Docker build-time values
Runtime injection doesn't automatically cover build-time variables. This matters most for anything like Next.js's NEXT_PUBLIC_* values, which get embedded during the build step rather than read at container startup.
When building the image, provide the Infisical access token as a BuildKit secret rather than a normal Docker build argument. Authenticate on the build machine:
The access token gets mounted only for that build instruction instead of landing in a normal image layer or build argument. Keep in mind that any value your framework deliberately marks as public, like NEXT_PUBLIC_* in Next.js, is meant to end up in the browser bundle. BuildKit protects the Infisical token here, not values you've already decided should be public.
Step 24: Separate application paths when necessary
For a single application process, storing secrets at the project root is usually enough:
text
/
As the architecture grows, folders can separate access:
The web process might receive /app, a background worker /workers, and a migration job /migrations, regardless of what language each of those components is written in. Infisical supports injecting secrets from one or several paths:
bash
infisical run \
--path="/app" \
--path="/shared" \
-- pnpm start
Don't build out a complicated folder hierarchy before you actually have different permission requirements. Folders earn their place once genuinely separate workloads need different subsets of secrets.
Step 25: Rotate a machine identity credential
To rotate the production Client Secret, generate a new Client Secret for the existing machine identity, add it to /etc/my-application/infisical.env, restart the application, confirm it starts correctly, and revoke the old Client Secret. The new process authenticates with the new bootstrap credential immediately. None of this requires rotating the database password, storage key, or any other application secret just because the machine identity credential changed, which is one of the main benefits of separating application secrets from machine authentication in the first place.
Step 26: Back up Infisical correctly
The PostgreSQL database contains the persistent Infisical data. Create a dump:
The official Docker Compose guide identifies the PostgreSQL volume as the critical persistent store and provides pg_dump as the backup mechanism, but the database alone isn't the complete recovery plan. Your deployment also depends on /opt/infisical/.env, which contains ENCRYPTION_KEY, AUTH_SECRET, POSTGRES_PASSWORD, and SITE_URL. Because ENCRYPTION_KEY is required for Infisical's own encryption and decryption operations, a working recovery plan needs both the database backup and the original platform encryption key together.
Back up the following through an encrypted backup system: the PostgreSQL dump, /opt/infisical/.env, docker-compose.prod.yml, the Nginx configuration, TLS and DNS recovery information, and the pinned Infisical version. Keep the only copy off the Infisical VPS itself, and actually test restoring it. An untested backup is a guess, not a recovery plan.
Step 27: Plan for Infisical availability
Without --watch, infisical run retrieves secrets when the application starts and injects them into the new process. Once an application is running, its environment variables already sit in process memory, so a temporary Infisical outage won't stop that existing process. It can, however, block a new deployment, an application restart, a container replacement, a server reboot, a migration command, or a new worker from starting, no matter what that worker happens to run.
That's why the Infisical instance itself needs monitoring, database backups, resource headroom, controlled upgrades, a documented restore process, and a stable domain with a valid TLS certificate. A secrets manager removes configuration sprawl, and it also becomes part of your deployment control plane, so it deserves the same operational care as anything else on that critical path.
Common problems
The CLI connects to Infisical Cloud instead of your instance
Check whether the Client ID is correct, whether the Client Secret is current, whether the identity was actually added to the project, and whether it has access to the selected environment and folder path. Authentication and project access are separate steps, so a machine identity can authenticate successfully and still lack access to a project.
Local development works, but production doesn't
Check INFISICAL_PROJECT_ID, INFISICAL_ENV, INFISICAL_DOMAIN, INFISICAL_PATH, the production machine identity's permissions, and outbound HTTPS connectivity from the server. The staging identity shouldn't be assumed to have production access just because it exists in the same project.
A changed secret isn't being picked up
Restart the process:
bash
sudo systemctl restart my-application
Or recreate the container:
bash
docker compose \
-f docker-compose.production.yml \
up -d \
--force-recreate
Environment variables don't mutate inside an already running process, in Node.js or anywhere else.
A build-time value like NEXT_PUBLIC_* is still old
Rebuild the application. Next.js freezes NEXT_PUBLIC_ values during next build, so restarting the existing build artifact won't pick up a new value; the build has to run again.
The database was restored, but secrets can't be read
Confirm the restored deployment uses the original ENCRYPTION_KEY. A database dump without the matching encryption configuration is an incomplete recovery package.
Also confirm that SITE_URL=https://secrets.example.com matches the actual public URL.
What changed compared with .env files
Before this setup, a developer laptop held .env.local, the staging VPS held .env.staging, the production VPS held .env.production, and the CI runner held its own copy of the production values, all independently. After this setup, Infisical holds development, staging, and production centrally, developers authenticate with a personal login, and the staging and production servers authenticate through their own machine identities.
The application code stays just as simple as it was before:
What changed is entirely operational: one source of truth, consistent key names, explicit environment separation, separate human and machine access, revocable server credentials, a documented startup workflow, and a repeatable recovery model, none of which depends on which framework or language sits on top of it.
FAQ
Does Infisical only work with Node.js applications?
No. Infisical's CLI wraps any command through infisical run -- <command>, so it works the same way for a Python, Go, Ruby, or PHP process as it does for Next.js or Payload. The examples here use Node.js because that's the stack I run, but the pattern generalizes directly.
Do I need separate Infisical projects for separate applications?
Usually yes, one project per application keeps environments and access boundaries clean, especially when different applications belong to different clients. A small internal toolset with shared ownership can sometimes live in one project with folders separating the pieces, but that's a judgment call based on who needs access to what.
What's the actual difference between Infisical and a tool like Vault?
Infisical focuses on a simpler, Vercel-like mental model of projects, environments, and secrets, with a CLI and dashboard that get you running quickly. HashiCorp Vault and OpenBao add dynamic credentials, PKI, and more advanced policy engines, at the cost of more operational complexity. Most small-to-mid-size teams get everything they need from Infisical without touching Vault's extra surface area.
Can I run Infisical without Docker?
The official deployment path is Docker Compose, which is also the path documented and supported by Infisical itself. Running it another way is possible in principle but puts you outside the tested deployment model, so I'd stick with Docker Compose unless you have a specific reason not to.
What happens to my running application if the Infisical server goes down?
Nothing, as long as the application is already running, since its environment variables live in process memory once the process has started. An Infisical outage blocks new deployments, restarts, and container replacements until the instance comes back, which is exactly why it needs its own monitoring and backup plan.
Conclusion
Self-hosting Infisical doesn't remove environment variables from the picture. It gives them a proper management and delivery system that works the same way no matter what's consuming them. Developers authenticate as people, application servers authenticate as machine identities, staging and production receive separate values, and the application itself keeps reading standard environment variables without getting tightly coupled to a vendor-specific SDK.
The critical implementation decisions carry across any stack: run Infisical on dedicated infrastructure, put it behind HTTPS, pin the deployed versions, keep the backend port private, separate development, staging, and production, use Universal Auth for VPS workloads, store only restricted bootstrap credentials locally, restart production explicitly after secret changes, handle build-time values where your framework requires it, and back up both PostgreSQL and the Infisical encryption configuration together.
The result is a provider-neutral secrets workflow that works across local development, systemd, Docker, and whatever application framework you're actually running, Next.js and Payload CMS included.