BuildWithMatija
  1. Home
  2. Blog
  3. Next.js
  4. Infisical CLI Secrets Workflow: Complete Next.js Guide

Infisical CLI Secrets Workflow: Complete Next.js Guide

Secure process-only secret injection with Infisical CLI for Next.js and Payload CMS, CI, Docker, and rotation steps.

16th July 2026·Updated on:28th July 2026··
Next.js
Infisical CLI Secrets Workflow: Complete Next.js Guide

⚡ Next.js Implementation Guides

In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.

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

  • Why process injection instead of a `.env` file
  • Platform configuration
  • Security rules to follow from the start
  • 1. Install the CLI
  • 2. Authenticate
  • 3. Link the repository
  • 4. Upload an existing `.env` to `dev`
  • 5. Verify an upload without exposing values
  • 6. Run the application with process-only injection
  • 7. Pull secrets into a local `.env` when you actually need one
  • 8. Create or update individual secrets
  • 9. Read a single secret safely
  • 10. Delete a secret
  • 11. Secret folders
  • 12. Required application keys
  • 13. Validate staging or production keys
  • 14. Machine identities for CI and runtime
  • 15. Test a machine identity
  • 16. Build-time injection with Docker BuildKit
  • 17. Runtime injection
  • 18. Rotating a credential
  • Application credentials (database, external API keys)
  • Universal Auth client secrets
  • Comparing the three secret-delivery paths
  • Troubleshooting
  • Command quick reference
  • FAQ
On this page:
  • Why process injection instead of a `.env` file
  • Platform configuration
  • Security rules to follow from the start
  • 1. Install the CLI
  • 2. Authenticate
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
  • CMS Architecture Review & Platform Blueprint
  • Next.js + Payload Advisory
  • AI Integration & Implementation

Resources

  • CMS Hub
  • B2B Website Strategy
  • E-commerce Hub
  • Blog
  • Case Studies

Payload CMS

  • Payload CMS Developer
  • Payload CMS Migration
  • Payload CMS Demos
  • All Payload CMS Resources

Discuss your project

Planning a rebuild, migration, application, workflow change, or platform decision? 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

Infisical CLI lets you inject secrets straight into a running process instead of writing them to a .env file, and that one property shapes almost every command in this guide. Pin the CLI version, link your project once, upload your existing .env as a starting point, then switch to infisical run for local development, CI builds, and production runtime. Version 0.43.84 has one quirk worth knowing before you touch it: --silent does not stop the secrets set command from printing a result table full of secret values, so every upload command in this guide redirects output to /dev/null. This article walks through the full path, from a fresh clone to a rotated production credential, using a Next.js and Payload CMS project as the running example.

I set this up for a client project that runs Payload CMS on Next.js across dev, staging, and production, with GitHub Actions building the Docker image and a Compose file handling runtime. The team had been passing .env files around in Slack before this, which is exactly the failure mode Infisical exists to prevent. What follows is the workflow that replaced it, including the two mistakes that cost me the most time: assuming --silent actually silenced the CLI, and pointing a CI runner at localhost:8085.

Why process injection instead of a .env file

A .env file sitting on disk is a file that can get committed, screenshotted, or copied into a support ticket. infisical run -- <command> skips that step by fetching secrets and setting them as environment variables directly on the child process. The values live in memory for that process only. Reach for infisical export --output-file when you genuinely need a local file, such as feeding a tool that only reads dotenv files, and treat that as the exception rather than the default.

Platform configuration

SettingValue
Local Infisical URLhttp://localhost:8085
OrganizationAdmin Org
ProjectWebsite Platform
Project/workspace ID00000000-0000-0000-0000-000000000000 (yours will differ)
Local project configuration.infisical.json
Supported environmentsdev, staging, prod
Default secret path/
Pinned CLI version0.43.84

localhost:8085 is a developer-facing local forward. CI runners and containers cannot reach it because localhost always resolves to the current host. Server bootstrap files need the approved HTTPS Infisical hostname instead, which shows up later in the CI and runtime sections.

Security rules to follow from the start

A few habits prevent most of the incidents that happen with secrets tooling:

  • Never commit .env, machine client secrets, access tokens, or exported values.
  • Use a personal Infisical account for development, and separate read-only Universal Auth machine identities for staging CI, production CI, and production runtime.
  • Keep development values out of staging and production; provision each environment independently.
  • Redirect every secrets set and export command's output, because CLI 0.43.84 prints values even with --silent set.
  • Skip shell tracing (set -x) anywhere near a secret command.
  • Rotate a credential immediately if it appears in a terminal transcript, CI log, screenshot, ticket, or chat message.

1. Install the CLI

Pin the version for reproducibility across every machine that touches the project.

bash
# Persistent installation
pnpm add --global @infisical/cli@0.43.84
infisical --version

Expected output:

text
infisical version 0.43.84

For a one-off run without a global install:

bash
pnpm dlx @infisical/cli@0.43.84 --version

Swap infisical for pnpm dlx @infisical/cli@0.43.84 in any command below if you'd rather not install globally.

2. Authenticate

Start the local Infisical forward, then log in:

bash
infisical login --domain=http://localhost:8085

This opens a browser callback. Complete it with your personal account. The CLI stores the session in its local credential vault and never writes the login token into the repository.

Confirm the session is active:

bash
infisical login status --domain=http://localhost:8085

If the browser callback doesn't fire on its own, open the URL the CLI prints in the terminal.

3. Link the repository

A checked-in .infisical.json links the repo to the right project and defaults to dev. To recreate that linkage from scratch:

bash
infisical init --domain=http://localhost:8085

Pick your organization, then your project, from the prompts. The resulting file holds project metadata only:

json
{
  "workspaceId": "00000000-0000-0000-0000-000000000000",
  "defaultEnvironment": "dev",
  "gitBranchToEnvironmentMapping": null
}

4. Upload an existing .env to dev

Check which keys you're about to upload before you run anything, since this command shows names only:

bash
awk -F= '/^[A-Za-z_][A-Za-z0-9_]*=/{print $1}' .env | sort

Then upload every valid entry as a shared secret at the root path:

bash
infisical secrets set \
  --domain=http://localhost:8085 \
  --env=dev \
  --path=/ \
  --type=shared \
  --file=.env \
  --silent \
  >/dev/null

This creates any missing keys and updates the ones that already exist, including empty values. Comment lines in the source file are skipped automatically. The output redirect matters here: without it, the terminal fills with a table of your secret values despite --silent being set. Keep dev, staging, and production values independent; don't push the same .env into more than one environment.

5. Verify an upload without exposing values

Injecting secrets into a short Node script and checking for presence is safer than printing them:

bash
infisical run \
  --domain=http://localhost:8085 \
  --env=dev \
  --silent \
  -- node -e '
    const expected = [
      "DATABASE_URL",
      "NEXT_PUBLIC_SERVER_URL",
      "PAYLOAD_SECRET",
      "DEFAULT_SITE_SLUG",
      "SITE_HOST_MAP",
      "INTEGRATIONS_MODE",
    ]
    const missing = expected.filter((key) => !process.env[key])
    console.log(JSON.stringify({
      injection: "ok",
      expected: expected.length,
      present: expected.length - missing.length,
      missing,
    }))
    process.exit(missing.length ? 2 : 0)
  '

Steer clear of printenv, env, infisical secrets --output=json, or piping an export to standard output for this check. Each of those prints the actual values.

6. Run the application with process-only injection

For day-to-day development:

bash
infisical run \
  --domain=http://localhost:8085 \
  --env=dev \
  -- pnpm dev

If your package.json wraps this in a script (for example pnpm dev:infisical), use that instead. The same pattern works for any command that needs environment access:

bash
# Seed development data
infisical run \
  --domain=http://localhost:8085 \
  --env=dev \
  -- pnpm db:seed

# Run catalog validation with the same environment
infisical run \
  --domain=http://localhost:8085 \
  --env=dev \
  -- pnpm catalog:validate

The child process reads process.env normally. infisical run never touches .env on disk.

7. Pull secrets into a local .env when you actually need one

bash
infisical export \
  --domain=http://localhost:8085 \
  --env=dev \
  --path=/ \
  --format=dotenv \
  --output-file=.env \
  --silent \
  >/dev/null

chmod 600 .env

Confirm the keys and file permissions without displaying values:

bash
awk -F= '/^[A-Za-z_][A-Za-z0-9_]*=/{print $1}' .env | sort
stat -c 'mode=%a' .env   # macOS: stat -f 'mode=%Lp' .env

You're looking for mode=600. Your .gitignore should already exclude .env* except .env.example, but check before every commit anyway:

bash
git status --short
git check-ignore -v .env

8. Create or update individual secrets

For a quick one-off during development:

bash
infisical secrets set \
  EXAMPLE_KEY=example-value \
  --domain=http://localhost:8085 \
  --env=dev \
  --path=/ \
  --type=shared \
  >/dev/null

For anything sensitive, put the value in a permission-restricted temporary file rather than your shell history, or use the Infisical dashboard directly:

bash
chmod 600 /path/to/update.env

infisical secrets set \
  --domain=http://localhost:8085 \
  --env=dev \
  --path=/ \
  --type=shared \
  --file=/path/to/update.env \
  --silent \
  >/dev/null

9. Read a single secret safely

Printing a secret value should be rare enough that it feels deliberate every time:

bash
infisical secrets get SECRET_NAME \
  --domain=http://localhost:8085 \
  --env=dev \
  --path=/

For a presence check instead, inject it into a process:

bash
infisical run \
  --domain=http://localhost:8085 \
  --env=dev \
  --silent \
  -- node -e '
    const key = "SECRET_NAME"
    console.log(process.env[key] ? `${key}: present` : `${key}: missing`)
    process.exit(process.env[key] ? 0 : 2)
  '

10. Delete a secret

Deletion touches shared external state, so double-check the environment, path, and key first:

bash
infisical secrets delete SECRET_NAME \
  --domain=http://localhost:8085 \
  --env=dev \
  --path=/

Restart anything that depends on the deleted secret and confirm it comes back healthy.

11. Secret folders

Most projects keep everything at the root path (/). Folders become useful once a specific integration needs isolation from the rest of the secrets.

bash
infisical secrets folders get \
  --domain=http://localhost:8085 \
  --env=dev \
  --path=/ \
  --output=json

Whatever --path you use, keep it consistent across secrets set, run, and export for that integration.

12. Required application keys

For a Next.js and Payload CMS deployment, staging and production validation typically checks:

text
DATABASE_URL
DATABASE_UNPOOLED_URL
SERVER_URL
NEXT_PUBLIC_SERVER_URL
PAYLOAD_SECRET
CRON_SECRET
S3_BUCKET
S3_ENDPOINT

The application layer also tends to use:

text
DEFAULT_SITE_SLUG
SITE_HOST_MAP
INTEGRATIONS_MODE
CACHE_REDIS_URL
CACHE_REDIS_PREFIX
CACHE_REDIS_MAX_ENTRY_BYTES

If your project carries DATABASE_URL_DIRECT from an older setup, rename it to DATABASE_UNPOOLED_URL. Migration scripts written against the newer name won't find the old one.

13. Validate staging or production keys

Run this from an authenticated developer machine, never from a CI runner pointed at localhost:

bash
infisical run \
  --domain=http://localhost:8085 \
  --env=staging \
  -- bash deployment-templates/validate-infisical-env.sh staging runtime

infisical run \
  --domain=http://localhost:8085 \
  --env=prod \
  -- bash deployment-templates/validate-infisical-env.sh production runtime

A good validator reports missing key names only and flags obviously invalid database hosts, without ever printing a value.

14. Machine identities for CI and runtime

Copying your personal login token onto a server defeats the point of having separate access levels. Create three Universal Auth identities instead:

  1. Staging CI/build/migration — read-only access to staging, path /.
  2. Production CI/build/migration — read-only access to prod, path /.
  3. Production runtime — read-only access to prod, path /.

Each identity gets its own bootstrap file on the target host:

text
/etc/infisical/website-platform-staging.env
/etc/infisical/website-platform-production.env
dotenv
INFISICAL_API_URL=https://secrets.example.com
INFISICAL_PROJECT_ID=00000000-0000-0000-0000-000000000000
INFISICAL_ENV=staging
INFISICAL_SECRET_PATH=/
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=replace-me
INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=replace-me
INFISICAL_DISABLE_UPDATE_CHECK=true

Lock down permissions on both files, and never commit a completed one:

bash
sudo chown root:<runner-group> /etc/infisical/website-platform-staging.env
sudo chown root:<runner-group> /etc/infisical/website-platform-production.env
sudo chmod 640 /etc/infisical/website-platform-staging.env
sudo chmod 640 /etc/infisical/website-platform-production.env

Production runtime can go tighter still, with root:root ownership and mode 600.

15. Test a machine identity

From the target host:

bash
INFISICAL_BOOTSTRAP_FILE=/etc/infisical/website-platform-staging.env \
  sh scripts/infisical-exec.sh \
  bash deployment-templates/validate-infisical-env.sh staging runtime

A wrapper script like this one reads the root-protected bootstrap file, exchanges the Universal Auth client credential for a short-lived token, strips the long-lived credential out of the child environment, runs infisical run, and hands the application process its values.

16. Build-time injection with Docker BuildKit

CI (GitHub Actions or Bitbucket Pipelines) runs BuildKit inside the same exec wrapper. Infisical typically injects three build-time values:

text
PAYLOAD_SECRET
DATABASE_URL
NEXT_PUBLIC_SERVER_URL

BuildKit receives each one through --secret id=...,env=..., and the Dockerfile mounts them under /run/secrets/ only for the duration of pnpm build. They never become build arguments and never land in an image layer. If you're used to --build-arg, resist the pull toward it here:

text
--build-arg PAYLOAD_SECRET=...

Build arguments get baked into image metadata, which is the opposite of what a secret mount is for.

17. Runtime injection

Staging and production Compose files mount a single bootstrap credential:

text
/run/secrets/infisical_bootstrap

The image entrypoint authenticates to Infisical, fetches the target environment, drops privileges to a non-root user, and starts the server with the fetched values already in process.env. After changing any runtime secret, restart the container so it picks up the new value:

bash
docker compose restart app

18. Rotating a credential

Application credentials (database, external API keys)

  1. Rotate the credential at its source.
  2. Update the corresponding Infisical environment.
  3. Restart or redeploy the application.
  4. Check /api/health.
  5. Revoke the old credential once the new one is confirmed working.

Updating Infisical without rotating the credential at its source leaves the old value live wherever it was issued.

Universal Auth client secrets

  1. Generate a new client secret for that exact machine identity.
  2. Update the matching /etc/infisical/*.env bootstrap file.
  3. Restart the workload or rerun the validation wrapper.
  4. Confirm injection succeeded and the application is healthy.
  5. Revoke the old client secret.

Comparing the three secret-delivery paths

MethodWhen to useTrade-off
infisical run -- <command>Local dev, CI steps, most day-to-day workValues live only in the child process; nothing to clean up
infisical export --output-fileA tool that only reads dotenv filesCreates a real file on disk that needs chmod 600 and careful .gitignore handling
Bootstrap file + machine identityCI runners and production containersRequires provisioning a Universal Auth identity per environment, but keeps long-lived credentials off developer machines

Troubleshooting

"Environment with slug 'demo' not found" Use one of the environments actually configured for the project: dev, staging, or prod.

"Injecting 0 Infisical secrets" The project, environment, and path all resolve, but nothing is accessible from them. Check the project linkage, the environment slug, the path (usually /), that secrets exist there, and that your account or machine identity has read access.

"Folder with path '/' ... was not found" Either the environment slug is wrong, or that environment has never had a root secret or folder initialized. Recheck both.

"File does not exist" --file=.env needs a local file to already exist. Pull it fresh with infisical export --output-file=.env or recreate it from an approved source.

Login works, but commands hit Infisical Cloud instead of your self-hosted instance Pass --domain=http://localhost:8085 on every command, or set it once for scripts:

bash
export INFISICAL_API_URL=http://localhost:8085

The local URL works, but CI or a container can't connect localhost always points at the current process's own host. CI runners and containers need the actual HTTPS Infisical hostname reachable from where they run, configured in the bootstrap file.

The CLI prints secret values despite --silent Redirect the command's output:

bash
infisical secrets set ... --file=.env --silent >/dev/null
infisical export ... --output-file=.env --silent >/dev/null

If a value already made it into a saved log somewhere, rotate it.

Command quick reference

bash
# Login
infisical login --domain=http://localhost:8085

# Link repository
infisical init --domain=http://localhost:8085

# Upload .env to dev
infisical secrets set \
  --domain=http://localhost:8085 \
  --env=dev \
  --path=/ \
  --type=shared \
  --file=.env \
  --silent >/dev/null

# Run without writing .env
infisical run \
  --domain=http://localhost:8085 \
  --env=dev \
  -- pnpm dev

# Pull to a local file
infisical export \
  --domain=http://localhost:8085 \
  --env=dev \
  --path=/ \
  --format=dotenv \
  --output-file=.env \
  --silent >/dev/null
chmod 600 .env

# Validate staging
infisical run \
  --domain=http://localhost:8085 \
  --env=staging \
  -- bash deployment-templates/validate-infisical-env.sh staging runtime

FAQ

Does --silent actually hide secret values in Infisical CLI 0.43.84? No, not for secrets set. That version prints a result table containing the values regardless of the flag. Redirect standard output to /dev/null on every upload or export command until you've confirmed a fix in a newer release.

Why does my CI runner fail to connect to http://localhost:8085? Because localhost resolves to the runner's own container, not your development machine. CI and production hosts need the real HTTPS Infisical hostname configured in their bootstrap files.

Should I use one Universal Auth identity for both staging and production? No. Provision a separate read-only identity for staging CI, production CI, and production runtime. If one leaks, the blast radius stays limited to a single environment.

What's the difference between infisical run and infisical export? infisical run injects secrets into a child process's memory and never writes them to disk. infisical export writes them into a real file, which is useful only when a tool genuinely requires a dotenv file to read from.

Can I pass a secret as a Docker build argument instead of a secret mount? You can, but you shouldn't. Build arguments get recorded in image metadata. A BuildKit --secret mount exposes the value only inside the build step and leaves nothing behind in the image.

Setting this up once, with the right environment separation and machine identities, removes the temptation to pass a .env file around informally later. Let me know in the comments if you run into a rougher edge with a different CLI version, and subscribe for more practical development guides.

Thanks, Matija