BuildWithMatija
  1. Home
  2. Blog
  3. Docker
  4. Self-Host Infisical: Ultimate VPS Guide for Secrets

Self-Host Infisical: Ultimate VPS Guide for Secrets

Step-by-step self-hosting of Infisical on a VPS with HTTPS, Nginx, machine identities, systemd, Docker, build-time…

15th July 2026·Updated on:28th July 2026··
Docker
Self-Host Infisical: Ultimate VPS Guide for Secrets

🐳 Docker & DevOps Implementation Guides

Complete Docker guides with optimization techniques, deployment strategies, and automation prompts to streamline your containerization workflow.

No spam. Unsubscribe anytime.

📄View markdown version
0

Frequently Asked Questions

About the author

Matija Žiberna

Matija Žiberna

Full-stack developer, co-founder

AboutResume

Self-taught full-stack developer sharing lessons from building software and startups.

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

Contents

  • What this guide covers
  • What this guide assumes
  • The target architecture
  • Step 1: Prepare the Infisical VPS
  • Step 2: Pin the Infisical image
  • Step 3: Bind Infisical to localhost
  • Step 4: Generate the platform secrets
  • Step 5: Start Infisical
  • Step 6: Configure Nginx and HTTPS
  • Step 7: Create the administrator account
  • Step 8: Configure SMTP
  • Step 9: Create the application project
  • Step 10: Keep `.env.example` in Git
  • Step 11: Install the Infisical CLI locally
  • Step 12: Connect your local project
  • Step 13: Run your application locally through Infisical
  • Step 14: Handle build-time variables (the Next.js case)
  • Step 15: Create a production machine identity
  • Step 16: Store the bootstrap credential on the VPS
  • Step 17: Create a reusable Infisical execution wrapper
  • Step 18: Build and migrate with production secrets
  • Step 19: Run the application with systemd
  • Step 20: Apply a changed secret
  • Step 21: Use Infisical with Docker
  • Step 22: Configure Docker Compose
  • Step 23: Handle Docker build-time values
  • Step 24: Separate application paths when necessary
  • Step 25: Rotate a machine identity credential
  • Step 26: Back up Infisical correctly
  • Step 27: Plan for Infisical availability
  • Common problems
  • The CLI connects to Infisical Cloud instead of your instance
  • The server receives `401` or `403`
  • Local development works, but production doesn't
  • A changed secret isn't being picked up
  • A build-time value like `NEXT_PUBLIC_*` is still old
  • The database was restored, but secrets can't be read
  • Infisical works locally but not behind Nginx
  • What changed compared with `.env` files
  • FAQ
  • Conclusion
On this page:
  • What this guide covers
  • What this guide assumes
  • The target architecture
  • Step 1: Prepare the Infisical VPS
  • Step 2: Pin the Infisical image
Build with Matija logo

Build with Matija

Senior-led B2B websites, applications, content systems, and digital infrastructure. Business-first, full-stack, AI-assisted, no handoffs.

Services

  • B2B Website Development
  • Headless CMS
  • AI Integration & Implementation
  • Next.js + Payload Advisory
  • Digital Platform Discovery
  • Website Growth Review

Resources

  • Case Studies
  • How I Work
  • Blog
  • Topics
  • CMS Hub
  • E-commerce Hub
  • B2B Website Strategy
  • Dashboard

Headless CMS

  • CMS Architecture Review
  • Payload CMS Developer
  • CMS Migration
  • Multi-Tenant CMS
  • Payload vs Sanity
  • Payload vs WordPress
  • Payload vs Contentful

Discuss the system

Planning a rebuild, migration, application, or workflow change? Start with the business problem and the system behind it.

Book a discovery callContact me →
© 2026Build with Matija•All rights reserved•Privacy Policy•Terms of Service
BuildWithMatija
Get In Touch

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
sudo mkdir -p /opt/infisical
sudo chown "$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:

bash
curl -fsSL \
  -o docker-compose.prod.yml \
  https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml

curl -fsSL \
  -o .env \
  https://raw.githubusercontent.com/Infisical/infisical/main/.env.example

Protect the configuration file:

bash
chmod 600 .env

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:

dotenv
# File: /opt/infisical/.env
ENCRYPTION_KEY=<GENERATED_HEX_ENCRYPTION_KEY>
AUTH_SECRET=<GENERATED_BASE64_AUTH_SECRET>

POSTGRES_USER=infisical
POSTGRES_PASSWORD=<GENERATED_POSTGRES_PASSWORD>
POSTGRES_DB=infisical

DB_CONNECTION_URI=postgresql://infisical:<GENERATED_POSTGRES_PASSWORD>@db:5432/infisical
REDIS_URL=redis://redis:6379

SITE_URL=https://secrets.example.com

TELEMETRY_ENABLED=false

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

Check the containers:

bash
docker compose \
  -f docker-compose.prod.yml \
  ps

Inspect the logs if a container isn't healthy:

bash
docker compose \
  -f docker-compose.prod.yml \
  logs --tail=100

Test the local API:

bash
curl -fsS http://127.0.0.1:8080/api/status

You should get a successful response from the Infisical backend.

Step 6: Configure Nginx and HTTPS

Install Nginx and Certbot:

bash
sudo apt-get update
sudo apt-get install -y nginx certbot python3-certbot-nginx

Create the Nginx virtual host:

bash
sudo nano /etc/nginx/sites-available/secrets.example.com

Add:

nginx
# File: /etc/nginx/sites-available/secrets.example.com
server {
    listen 80;
    listen [::]:80;

    server_name secrets.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_read_timeout 300;
        proxy_send_timeout 300;
    }
}

Enable the site:

bash
sudo ln -s \
  /etc/nginx/sites-available/secrets.example.com \
  /etc/nginx/sites-enabled/secrets.example.com

Test the configuration and reload Nginx:

bash
sudo nginx -t
sudo systemctl reload nginx

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:

dotenv
# File: /opt/infisical/.env
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=<SMTP_USERNAME>
SMTP_PASSWORD=<SMTP_PASSWORD>
SMTP_FROM_ADDRESS=secrets@example.com
SMTP_FROM_NAME=Infisical

Restart the backend:

bash
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:

KeyDevelopmentStagingProduction
DATABASE_URLLocal databaseStaging databaseProduction database
PAYLOAD_SECRETDevelopment valueStaging valueProduction value
S3_BUCKETDevelopment bucketStaging bucketProduction bucket
BREVO_API_KEYTest or development keyStaging keyProduction 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:

dotenv
# File: Infisical → My Application → dev/staging/prod
NODE_ENV=production
APP_ENV=
SERVER_URL=

DATABASE_URL=
DATABASE_URL_UNPOOLED=

PAYLOAD_SECRET=

S3_ENDPOINT=
S3_REGION=
S3_BUCKET=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=

REDIS_URL=

SMTP_HOST=
SMTP_PORT=
SMTP_USERNAME=
SMTP_PASSWORD=

BREVO_API_KEY=

CRON_SECRET=
PREVIEW_SECRET=

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:

dotenv
# File: .env.example

NODE_ENV=
APP_ENV=
SERVER_URL=

DATABASE_URL=
DATABASE_URL_UNPOOLED=

PAYLOAD_SECRET=

S3_ENDPOINT=
S3_REGION=
S3_BUCKET=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=

REDIS_URL=

SMTP_HOST=
SMTP_PORT=
SMTP_USERNAME=
SMTP_PASSWORD=

BREVO_API_KEY=

CRON_SECRET=
PREVIEW_SECRET=

This documents the configuration contract without containing credentials, and it's worth keeping around no matter which application reads it.

Step 11: Install the Infisical CLI locally

On macOS:

bash
brew install infisical/get-cli/infisical

On Debian or Ubuntu:

bash
curl -1sLf \
  "https://artifacts-cli.infisical.com/setup.deb.sh" \
  | sudo -E bash

sudo apt-get update
sudo apt-get install -y infisical

Infisical also distributes the CLI through NPM:

bash
npm install -g @infisical/cli

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:

bash
export INFISICAL_DOMAIN="https://secrets.example.com"

Initialize the project:

bash
infisical init

Select the Infisical project when prompted. This creates .infisical.json. Update it to include the self-hosted domain and default environment:

json
{
  "workspaceId": "<INFISICAL_PROJECT_ID>",
  "defaultEnvironment": "dev",
  "domain": "https://secrets.example.com"
}

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:

ts
// File: src/payload.config.ts

import { buildConfig } from 'payload'
import { postgresAdapter } from '@payloadcms/db-postgres'

export default buildConfig({
  secret: process.env.PAYLOAD_SECRET ?? '',
  serverURL: process.env.SERVER_URL,
  db: postgresAdapter({
    pool: {
      connectionString: process.env.DATABASE_URL,
    },
  }),
})

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_:

text
DATABASE_URL
PAYLOAD_SECRET
S3_SECRET_ACCESS_KEY
SMTP_PASSWORD
BREVO_API_KEY

A value prefixed with NEXT_PUBLIC_ is meant for browser access:

text
NEXT_PUBLIC_SITE_URL
NEXT_PUBLIC_ANALYTICS_ID
NEXT_PUBLIC_MAPBOX_TOKEN

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:

text
Project Settings
  → Access Control
  → Machine Identities
  → Add identity

Grant it read-only access to:

text
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:

bash
sudo adduser --system \
  --group \
  --home /srv/my-application \
  my-application

Create the configuration directory:

bash
sudo install \
  -d \
  -m 750 \
  -o root \
  -g my-application \
  /etc/my-application

Create the bootstrap file:

bash
sudo nano /etc/my-application/infisical.env

Add:

dotenv
# File: /etc/my-application/infisical.env
INFISICAL_DOMAIN=https://secrets.example.com
INFISICAL_PROJECT_ID=<PROJECT_ID>
INFISICAL_ENV=prod

INFISICAL_CLIENT_ID=<PRODUCTION_CLIENT_ID>
INFISICAL_CLIENT_SECRET=<PRODUCTION_CLIENT_SECRET>

INFISICAL_DISABLE_UPDATE_CHECK=true

Protect it:

bash
sudo chown root:my-application \
  /etc/my-application/infisical.env

sudo chmod 640 \
  /etc/my-application/infisical.env

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-infisical

set -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="/" \
  -- "$@"

Make it executable:

bash
sudo chmod 750 \
  /usr/local/bin/my-application-infisical

sudo chown root:my-application \
  /usr/local/bin/my-application-infisical

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.

Test it:

bash
sudo -u my-application \
  /usr/local/bin/my-application-infisical \
  node -e "console.log(Boolean(process.env.PAYLOAD_SECRET))"

Expected output:

text
true

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

Run Payload migrations through the same wrapper:

bash
cd /srv/my-application/current

sudo -u my-application \
  /usr/local/bin/my-application-infisical \
  pnpm payload migrate

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.

Step 19: Run the application with systemd

Create the service:

bash
sudo nano /etc/systemd/system/my-application.service

Add:

ini
# File: /etc/systemd/system/my-application.service
[Unit]
Description=My application
After=network-online.target
Wants=network-online.target

[Service]
Type=simple

User=my-application
Group=my-application

WorkingDirectory=/srv/my-application/current

ExecStart=/usr/local/bin/my-application-infisical pnpm start

Restart=on-failure
RestartSec=5

Environment=NODE_ENV=production
Environment=PATH=/usr/local/bin:/usr/bin:/bin

NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=full

[Install]
WantedBy=multi-user.target

Swap pnpm start in ExecStart for whatever command actually starts your application. Reload systemd, then enable and start it:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now my-application

Check the status and follow the logs:

bash
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

Create an entrypoint:

bash
# File: docker/infisical-entrypoint.sh

#!/usr/bin/env bash

set -euo pipefail

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="${INFISICAL_PATH:-/}" \
  -- "$@"

Copy it into the image:

dockerfile
# File: Dockerfile

COPY docker/infisical-entrypoint.sh \
  /usr/local/bin/infisical-entrypoint

RUN chmod +x \
  /usr/local/bin/infisical-entrypoint

ENTRYPOINT ["/usr/local/bin/infisical-entrypoint"]

CMD ["pnpm", "start"]

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.

Step 22: Configure Docker Compose

Store the bootstrap credentials on the host:

bash
sudo mkdir -p /etc/my-application

sudo nano \
  /etc/my-application/infisical.env

Add:

dotenv
# File: /etc/my-application/infisical.env
INFISICAL_DOMAIN=https://secrets.example.com
INFISICAL_PROJECT_ID=<PROJECT_ID>
INFISICAL_ENV=prod
INFISICAL_CLIENT_ID=<CLIENT_ID>
INFISICAL_CLIENT_SECRET=<CLIENT_SECRET>

Protect the file:

bash
sudo chmod 600 \
  /etc/my-application/infisical.env

The Compose service can use it:

yaml
# File: docker-compose.production.yml

services:
  app:
    image: my-application:<VERSION>
    restart: unless-stopped

    env_file:
      - /etc/my-application/infisical.env

    environment:
      NODE_ENV: production
      INFISICAL_DISABLE_UPDATE_CHECK: "true"

    ports:
      - "127.0.0.1:3000:3000"

Start it:

bash
docker compose \
  -f docker-compose.production.yml \
  up -d

Check the logs:

bash
docker compose \
  -f docker-compose.production.yml \
  logs --tail=100 app

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:

bash
export INFISICAL_DOMAIN="https://secrets.example.com"

export INFISICAL_TOKEN="$(
  infisical login \
    --method=universal-auth \
    --client-id="$INFISICAL_CLIENT_ID" \
    --client-secret="$INFISICAL_CLIENT_SECRET" \
    --silent \
    --plain
)"

Build:

bash
DOCKER_BUILDKIT=1 docker build \
  --secret id=infisical_token,env=INFISICAL_TOKEN \
  --build-arg INFISICAL_PROJECT_ID="$INFISICAL_PROJECT_ID" \
  --build-arg INFISICAL_ENV="$INFISICAL_ENV" \
  --build-arg INFISICAL_DOMAIN="$INFISICAL_DOMAIN" \
  -t my-application:<VERSION> \
  .

In the builder stage:

dockerfile
# File: Dockerfile

ARG INFISICAL_PROJECT_ID
ARG INFISICAL_ENV
ARG INFISICAL_DOMAIN

RUN --mount=type=secret,id=infisical_token,required=true \
    export INFISICAL_TOKEN="$(cat /run/secrets/infisical_token)" && \
    export INFISICAL_DOMAIN="$INFISICAL_DOMAIN" && \
    infisical run \
      --projectId="$INFISICAL_PROJECT_ID" \
      --env="$INFISICAL_ENV" \
      --path="/" \
      -- pnpm build

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:

text
/
├── app
├── workers
├── migrations
└── automation

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:

bash
cd /opt/infisical

docker compose \
  -f docker-compose.prod.yml \
  exec -T db \
  pg_dump \
    -U infisical \
    infisical \
  | gzip \
  > "infisical-$(date +%F-%H%M%S).sql.gz"

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

Set the self-hosted domain:

bash
export INFISICAL_DOMAIN="https://secrets.example.com"

Or add it to .infisical.json:

json
{
  "domain": "https://secrets.example.com"
}

The server receives 401 or 403

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.

Infisical works locally but not behind Nginx

Check:

bash
curl -fsS http://127.0.0.1:8080/api/status
curl -fsS https://secrets.example.com/api/status
sudo nginx -t
sudo journalctl -u nginx --tail=100

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:

ts
process.env.DATABASE_URL
process.env.PAYLOAD_SECRET
process.env.S3_SECRET_ACCESS_KEY

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.

For the broader reasoning behind this setup, read How to Manage Environment Variables and Secrets Across Environments. For the complete application deployment layer, read Self-Host Next.js and Payload on VPS: Complete Guide.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija