---
title: "Infisical CLI Secrets Workflow: Complete Next.js Guide"
slug: "infisical-cli-nextjs-payload-secrets-workflow"
published: "2026-07-16"
updated: "2026-07-28"
categories:
  - "Next.js"
tags:
  - "Infisical CLI"
  - "secrets management"
  - "process injection"
  - "Next.js secrets"
  - "Payload CMS secrets"
  - "Docker BuildKit secrets"
  - "GitHub Actions CI"
  - "Universal Auth machine identity"
  - "dotenv export"
  - "secret rotation"
  - "infisical run"
  - "bootstrap file"
llm-intent: "reference"
audience-level: "intermediate"
framework-versions:
  - "infisical cli"
  - "next.js"
  - "payload cms"
  - "docker"
  - "docker buildkit"
status: "stable"
llm-purpose: "Infisical CLI secures Next.js and Payload CMS secrets by injecting them into processes—no .env files. Get the step-by-step CI, Docker BuildKit, and…"
llm-prereqs:
  - "Access to Infisical CLI"
  - "Access to Next.js"
  - "Access to Payload CMS"
  - "Access to Docker"
  - "Access to Docker BuildKit"
llm-outputs:
  - "Completed outcome: Infisical CLI secures Next.js and Payload CMS secrets by injecting them into processes—no .env files. Get the step-by-step CI, Docker BuildKit, and…"
---

**Summary Triples**
- (Infisical CLI, injects-secrets-into, child process environment (avoids writing .env to disk))
- (infisical run, executes-command-with, secrets set as environment variables on the spawned process)
- (infisical export --output-file, writes, a dotenv file for tools that require a file (use sparingly))
- (infisical-cli@0.43.84, has-bug, --silent does not suppress secrets set output)
- (secrets set commands, should-redirect-output, to /dev/null on CLI v0.43.84 to avoid printing secret values)
- (Recommended workflow, replaces, sharing .env files with process-based secret injection)
- (CI builds, should-use, infisical run or BuildKit secret injection for secure secrets at build/runtime)
- (Docker BuildKit, can-inject, secrets into the build step without writing them to disk)
- (GitHub Actions, builds, the Docker image and can fetch secrets during the workflow)
- (Common mistake, is, pointing a CI runner at localhost:8085 (causes failures))
- (Secret rotation, requires, update secret in Infisical, then restart/redeploy consumers to pick up new values)
- (Project setup, steps, pin CLI version, link project, upload .env as starting point, switch to infisical run)
- (When a file is needed, use, infisical export --output-file and treat the file as an exception)
- (Best practice, advocates, process-only secrets and minimal file persistence)

### {GOAL}
Infisical CLI secures Next.js and Payload CMS secrets by injecting them into processes—no .env files. Get the step-by-step CI, Docker BuildKit, and…

### {PREREQS}
- Access to Infisical CLI
- Access to Next.js
- Access to Payload CMS
- Access to Docker
- Access to Docker BuildKit

### {STEPS}
1. Install and pin Infisical CLI
2. Authenticate and start local forward
3. Link repository with .infisical.json
4. Upload your .env to dev safely
5. Verify injection without printing values
6. Run app with process-only injection
7. Export .env only when necessary
8. Create, update, and delete secrets
9. Provision machine identities for CI/runtime
10. Use BuildKit for build-time injection
11. Runtime injection via bootstrap file
12. Rotate credentials and validate health

<!-- llm:goal="Infisical CLI secures Next.js and Payload CMS secrets by injecting them into processes—no .env files. Get the step-by-step CI, Docker BuildKit, and…" -->
<!-- llm:prereq="Access to Infisical CLI" -->
<!-- llm:prereq="Access to Next.js" -->
<!-- llm:prereq="Access to Payload CMS" -->
<!-- llm:prereq="Access to Docker" -->
<!-- llm:prereq="Access to Docker BuildKit" -->
<!-- llm:output="Completed outcome: Infisical CLI secures Next.js and Payload CMS secrets by injecting them into processes—no .env files. Get the step-by-step CI, Docker BuildKit, and…" -->

# Infisical CLI Secrets Workflow: Complete Next.js Guide
> Infisical CLI secures Next.js and Payload CMS secrets by injecting them into processes—no .env files. Get the step-by-step CI, Docker BuildKit, and…
Matija Žiberna · 2026-07-16

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

| Setting | Value |
|---|---|
| Local Infisical URL | `http://localhost:8085` |
| Organization | `Admin Org` |
| Project | `Website Platform` |
| Project/workspace ID | `00000000-0000-0000-0000-000000000000` (yours will differ) |
| Local project configuration | `.infisical.json` |
| Supported environments | `dev`, `staging`, `prod` |
| Default secret path | `/` |
| Pinned CLI version | `0.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

| Method | When to use | Trade-off |
|---|---|---|
| `infisical run -- <command>` | Local dev, CI steps, most day-to-day work | Values live only in the child process; nothing to clean up |
| `infisical export --output-file` | A tool that only reads dotenv files | Creates a real file on disk that needs `chmod 600` and careful `.gitignore` handling |
| Bootstrap file + machine identity | CI runners and production containers | Requires 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

## LLM Response Snippet
```json
{
  "goal": "Infisical CLI secures Next.js and Payload CMS secrets by injecting them into processes—no .env files. Get the step-by-step CI, Docker BuildKit, and…",
  "responses": [
    {
      "question": "What does the article \"Infisical CLI Secrets Workflow: Complete Next.js Guide\" cover?",
      "answer": "Infisical CLI secures Next.js and Payload CMS secrets by injecting them into processes—no .env files. Get the step-by-step CI, Docker BuildKit, and…"
    }
  ]
}
```