Add Infisical to Your Docker Stack with the CLI
Self-Hosted or Cloud — Replace plain-text .env files with Universal Auth in 30 minutes.
What you’ll build
Infisical replaces plain-text files with a secure secret vault. The platform centralizes environment variables across distributed services. Machine identities provide granular access control for automated systems. This tutorial implements Universal Auth via the command line interface. By the end, every API key in the stack flows from Infisical at runtime, not from a .env file on disk.
The final architecture eliminates the risk of committing sensitive credentials to version control. Wrapping execution commands with infisical run injects secrets directly into the child process environment. This method bypasses the need for disk-based .env files entirely. The following diagram illustrates the complete data flow from authentication through to the application.
CLI → Universal Auth → Infisical vault → infisical run → child process env → Docker Compose / Python app
Secrets remain encrypted at rest and in transit. The Infisical CLI handles the decryption handshake using temporary access tokens. A centralized audit log records every secret access event. This setup supports both Infisical Cloud and self-hosted instances. The integration fits seamlessly into existing Docker Compose workflows by replacing the --env-file flag with a dynamic runtime wrapper.
Before you start
Docker manages container lifecycles on the host machine. Git version control tracks configuration changes in the repository. The Infisical CLI fetches encrypted secrets from the remote instance. Minimum hardware specifications ensure stable performance for the Docker stack. Verify local tool versions before proceeding with the integration.
The local environment requires the following minimum software versions:
- Docker >= 24.0.0 —
docker --version— install at docs.docker.com/get-docker - Docker Compose >= 2.20.0 —
docker compose version— install at docs.docker.com/compose/install - Infisical CLI >= 0.30.0 —
infisical --version— installed in Step 1 below - Git >= 2.40.0 —
git --version
A minimum of 1.5 GB free RAM and 1.5 GB free disk space is required for the self-hosted path. This guide targets macOS and Linux environments. Windows users should perform all steps inside a Windows Subsystem for Linux (WSL) instance to ensure shell compatibility. The current user must have sudo privileges for package installation.
Pick: Infisical Cloud or self-hosted
Infisical Cloud offers a managed environment with zero infrastructure maintenance overhead. Self-hosted instances provide total control over data residency and network boundaries. Choosing the right deployment model depends on compliance requirements and resource availability. Both options support the Universal Auth method for machine-to-machine secret access. Compare the options below before proceeding.
| Feature | Infisical Cloud | Self-Hosted |
|---|---|---|
| Setup time | < 2 minutes | ~15 minutes |
| Monthly cost | Free tier / Paid Pro | Compute costs only |
| Auditability | Full (managed by Infisical) | Full (self-managed) |
| Network exit | Requires outbound internet | Can be fully air-gapped |
Infisical Cloud is recommended for first-time integrations and rapid prototyping. The cloud path eliminates the need to manage a database and Redis cache. Use the self-hosted path when internal policies prohibit storing credentials on third-party servers, or when operating in an air-gapped environment. Regardless of the path chosen, the CLI commands in the subsequent steps remain nearly identical — the only difference is one extra environment variable when running against a self-hosted host.
Step 1 — Install the Infisical CLI
The Infisical CLI bridges the secure vault and local processes at runtime. Package managers provide the most reliable installation path and simplify upgrades. Manual binary downloads are available for environments without package managers. The installation registers the infisical binary in the system path so any shell can invoke it.
macOS (Homebrew)
Homebrew manages the Infisical CLI lifecycle on macOS through an official tap maintained by Infisical.
brew install infisical/get-cli/infisical
Linux (Debian/Ubuntu)
The setup script configures the Cloudsmith APT repository, after which apt-get can install and upgrade the CLI through the standard package manager.
curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | sudo -E bash
sudo apt-get install -y infisical
Critical Warning: Never Use pip
NEVER run pip install infisical. That command installs an unrelated Python package that does not provide command-line vault functionality. Installing the Python package results in command-not-found errors or silent misbehavior when attempting to use infisical run. Always install through the Homebrew tap or the official Linux setup script shown above.
Verify: infisical --version returns a version string starting with 0.30 or higher.
Step 2 — Set up your Infisical instance
Infisical instances store project secrets in an encrypted database backend. Cloud projects provide immediate web-based access with no infrastructure setup. Self-hosted setups use Docker Compose to orchestrate the application, its database, and its cache. You must complete one path to obtain a project URL and admin login before creating machine identities.
Cloud
Navigate to https://app.infisical.com and create an account. Create a new Organization to house your projects. Inside the organization, click New Project and name it my-rag for continuity with the preceding tutorial in this series. The project dashboard provides the Project ID visible in the browser URL — copy it now, as it is required in every CLI command from Step 5 onward.
Self-hosted
Standing up a local Infisical instance requires Postgres for storage, Redis for session caching, and the Infisical application container. Create a directory named infisical-server, then create docker-compose.yml inside it with the following content. The image is pinned to a known-working tag — check Docker Hub at hub.docker.com/r/infisical/infisical/tags for the latest patch before a production deploy.
version: '3.8'
services:
db:
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_USER: infisical
POSTGRES_PASSWORD: infisicalpassword
POSTGRES_DB: infisical
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
infisical:
image: infisical/infisical:v0.159.22
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
depends_on:
- db
- redis
environment:
NODE_ENV: production
DB_CONNECTION_URI: postgresql://infisical:infisicalpassword@db:5432/infisical
REDIS_URL: redis://redis:6379
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
AUTH_SECRET: ${AUTH_SECRET}
SITE_URL: http://localhost:8080
volumes:
postgres_data:
redis_data:
Generate the two required secrets before starting:
export ENCRYPTION_KEY=$(openssl rand -hex 16)
export AUTH_SECRET=$(openssl rand -base64 32)
Start the stack:
docker compose up -d
Visit http://localhost:8080 and complete the admin registration. Create an Organization and a Project named my-rag.
Both paths converge here: you now have a project URL and an admin login. Proceed to Step 3 with the project URL open in a browser tab.
Verify (self-hosted only): curl -s http://localhost:8080/api/v1/health | python3 -m json.tool returns JSON containing "status":"ok". Cloud users can skip this check — the hosted service has no public health endpoint.
Step 3 — Create a Universal Auth machine identity
Machine identities enable non-interactive authentication for automated environments. Universal Auth issues a Client ID and Client Secret pair that the CLI exchanges for a short-lived access token. Path-scoping restricts an identity to a specific directory within the secret tree, so a leaked credential cannot read secrets outside the designated path. Create one identity per project and scope it tightly — this is the single most important security decision in this tutorial.
Open the Infisical UI and navigate to the my-rag project. Select Access Control from the left sidebar. Click the Identities tab, then Create Identity. Enter the name rag-cli-worker and select Universal Auth as the authentication method.
Under Trusted IPs, enter your machine’s public IP or a CIDR range. Leaving this open to 0.0.0.0/0 is acceptable for initial testing but should be locked down before production use.
Under Project Access, assign the identity to the dev environment and set the path to /openai-rag. This path restriction is critical: even if the Client Secret is exposed, an attacker can only read secrets within /openai-rag and cannot access any other path in the project.
Click Create. The UI displays the Client ID and Client Secret. Copy both values immediately into a password manager or secure note. The Client Secret is shown only once. If lost, generate a new one from the identity’s settings page — old secrets become invalid at the moment a new one is created.
Step 4 — Add your secrets to the project
The Infisical secret tree organizes environment variables by environment and path. Folders group related secrets so that identities with path-scoped access see only what they need. Migrating from .env files requires entering each key-value pair through the UI once. After that, Infisical becomes the single source of truth — no local .env required.
In the Infisical UI, open the my-rag project and select the dev environment under Secrets. Click Add Folder and name it openai-rag. Enter the folder and add secrets one by one using the Add Secret button.
Migrate the following keys from your existing .env file:
OPENAI_API_KEY=sk-proj-...
OPENAI_EMBED_MODEL=text-embedding-3-large
OPENAI_LLM_MODEL=gpt-4o-mini
QDRANT_URL=http://rag-qdrant:6333
Add each key name and its value in the corresponding fields in the Infisical editor. The folder structure ensures all four secrets sit under the /openai-rag path. Infisical versions every secret value — the previous value is preserved in history if a rollback is needed.
After saving, confirm that all four keys appear in the folder listing. The QDRANT_URL value remains http://rag-qdrant:6333 because Docker Compose resolves service names within the container network.
Verify: (UI step — no shell expression required. Confirm all four keys appear in the /openai-rag folder under the dev environment before continuing.)
Step 5 — Authenticate the CLI to your instance
The CLI requires the Client ID and Client Secret in the shell environment to perform the Universal Auth handshake. Setting these as environment variables avoids interactive login prompts in automated contexts. Self-hosted instances additionally require the INFISICAL_API_URL variable, because the CLI defaults to https://app.infisical.com when the variable is absent. The cached token written to disk does not auto-renew when the Client Secret rotates — rely on fresh environment variables for cron and CI.
Set the credentials in the current terminal session:
export INFISICAL_CLIENT_ID="<paste-from-step-3>"
export INFISICAL_CLIENT_SECRET="<paste-from-step-3>"
# Self-hosted only — omit for Infisical Cloud:
export INFISICAL_API_URL="https://your-self-hosted.example.com"
# Authenticate and cache the token:
infisical login --method universal-auth --plain --silent
Confirm that the CLI can reach the secrets. Find your Project ID in the Infisical dashboard URL (the segment after /project/).
infisical secrets --projectId <YOUR_PROJECT_ID> --env=dev --path=/openai-rag
The output lists the secret names and masked values for every key under /openai-rag. If the list is empty, verify that the identity has the correct path permission set in Step 3.
The CLI token is cached at ~/.infisical/. For production environments, cron jobs, and CI pipelines, do not rely on this cache. Set INFISICAL_CLIENT_ID and INFISICAL_CLIENT_SECRET fresh from a secure source at the start of each invocation. The cached token does not auto-renew when the Client Secret is rotated in the UI — any process depending on the cache will start returning 401 errors after rotation until the cache is refreshed manually.
Verify: infisical secrets --projectId <id> --env=dev --path=/openai-rag 2>&1 | grep OPENAI_API_KEY returns the key name.
Step 6 — Wrap commands with infisical run
infisical run is the canonical CLI pattern for injecting secrets into a child process. The command authenticates via INFISICAL_CLIENT_ID and INFISICAL_CLIENT_SECRET already present in the shell, fetches the secrets at the specified path, exports them into the child process environment, and then execs the command. The exit code from the child process propagates back to the caller unchanged — a failing Docker Compose command returns a non-zero exit code to cron, CI, and calling scripts without any extra logic.
The -- separator is mandatory. Everything before -- controls the Infisical CLI. Everything after -- is the command to execute.
Before — using a plain .env file:
docker compose --env-file .env --profile manual run --rm ingest python /app/scripts/ingest.py
After — fetching secrets at runtime from Infisical:
infisical run --projectId <YOUR_PROJECT_ID> --env=dev --path=/openai-rag -- \
docker compose --profile manual run --rm ingest python /app/scripts/ingest.py
Standard output and standard error from the child process flow through to the terminal unmodified. To run a fully self-contained one-shot command without pre-exporting credentials, set them inline:
INFISICAL_CLIENT_ID="..." INFISICAL_CLIENT_SECRET="..." \
infisical run --projectId <id> --env=dev --path=/openai-rag -- \
docker compose --profile manual run --rm ingest python /app/scripts/ingest.py
Inline assignment keeps the credentials out of shell history on many systems. Switching between environments requires only changing --env=dev to --env=staging or --env=prod — no file edits needed.
Verify: infisical run --projectId <id> --env=dev --path=/openai-rag -- printenv OPENAI_API_KEY prints the key value.
Step 7 — Apply Infisical to the RAG stack from post #1
Migrating the RAG stack removes the .env file dependency at every invocation point. The previous tutorial used env_file: ../.env in docker-compose.yml to pass credentials into the ingest container. Removing that directive forces the stack to depend on the caller’s environment — which infisical run populates at runtime. Deleting the local .env file eliminates the risk of accidental commits and plaintext exposure on disk.
Open the rag-llamaindex-qdrant-docker project directory. Verify that every key in the local .env is now present in the Infisical vault at /openai-rag under the dev environment, then remove the file:
rm .env
Open docker/docker-compose.yml and locate the env_file directive on the ingest service:
# BEFORE — reads credentials from disk at compose time
services:
ingest:
build:
context: .
dockerfile: Dockerfile
env_file:
- ../.env
profiles: ["manual"]
Remove the env_file block entirely. Docker Compose inherits environment variables from the calling shell, so any variable exported by infisical run is automatically visible inside containers started by that Compose invocation.
# AFTER — inherits credentials from the infisical run wrapper
services:
ingest:
build:
context: .
dockerfile: Dockerfile
profiles: ["manual"]
Run the ingest pipeline:
infisical run --projectId <id> --env=dev --path=/openai-rag -- \
docker compose -f docker/docker-compose.yml --profile manual run --rm ingest python /app/scripts/ingest.py
Run the query script:
infisical run --projectId <id> --env=dev --path=/openai-rag -- \
docker compose -f docker/docker-compose.yml --profile manual run --rm query python /app/scripts/query.py "What is retrieval-augmented generation?"
Removing env_file has a useful side effect: the stack now refuses to start if OPENAI_API_KEY is absent from the environment. This prevents silent failures where a missing .env caused the application to run with empty credentials and produce confusing API errors.
Verify: infisical run --projectId <id> --env=dev --path=/openai-rag -- docker compose --profile manual run --rm ingest python /app/scripts/ingest.py 2>&1 | tail -5 shows ingest completion output.
Step 8 — Make it permanent in CI/cron
Cron jobs and CI runners execute in minimal shell environments with no inherited user variables. Every infisical run invocation requires INFISICAL_CLIENT_ID and INFISICAL_CLIENT_SECRET to be present before the command is called. A wrapper script reads these from a permissions-restricted file pair and exports them before invoking infisical run. Storing the credentials under /etc/infisical/ with 0600 permissions ensures only the designated service user can read them.
Add the cron entry using crontab -e:
0 3 * * * /usr/local/bin/run-rag-ingest.sh >> /var/log/rag-ingest.log 2>&1
Create the wrapper script at /usr/local/bin/run-rag-ingest.sh:
#!/usr/bin/env bash
set -euo pipefail
INFISICAL_CLIENT_ID="$(cat /etc/infisical/client-id)"
INFISICAL_CLIENT_SECRET="$(cat /etc/infisical/client-secret)"
export INFISICAL_CLIENT_ID INFISICAL_CLIENT_SECRET
exec infisical run \
--projectId "abc-123-your-project-id" \
--env=prod \
--path=/openai-rag \
-- docker compose \
-f /home/user/rag-llamaindex-qdrant-docker/docker/docker-compose.yml \
--profile manual run --rm ingest python /app/scripts/ingest.py
Prepare the credential files on the host. The printf command avoids appending a trailing newline that can silently break the cat read in the script.
sudo mkdir -p /etc/infisical
printf '%s' "$CLIENT_ID" | sudo tee /etc/infisical/client-id > /dev/null
printf '%s' "$CLIENT_SECRET" | sudo tee /etc/infisical/client-secret > /dev/null
sudo chmod 600 /etc/infisical/client-id /etc/infisical/client-secret
sudo chown root:root /etc/infisical/client-id /etc/infisical/client-secret
chmod +x /usr/local/bin/run-rag-ingest.sh
Adjust chown to match the cron user if the job does not run as root. A systemd service is a cleaner alternative to cron for long-running or retry-based tasks; use an EnvironmentFile=/etc/infisical/infisical.env directive that sources both variables.
Docker Compose also supports file-based secrets via /run/secrets/<name>. Mounting secrets as files rather than environment variables is more secure for sharing credentials across multiple services in the same Compose project, because the values are never exposed in docker inspect output. One critical pitfall: cap_drop: [ALL] in a Docker service definition breaks file-based secret reads because the tmpfs mount requires the DAC_OVERRIDE Linux capability. If using file-based mounts, drop capabilities selectively and retain DAC_OVERRIDE explicitly.
Verify: sudo -u <cron-user> /usr/local/bin/run-rag-ingest.sh exits 0.
Troubleshooting
Infisical CLI operations depend on correct network routing, valid credentials, and matching API versions. Configuration errors typically surface as 401 Unauthorized responses or empty injected environments. Verifying both the shell environment and vault permissions resolves the majority of integration failures.
Infisical CLI says “no token” or asks me to login on every command
INFISICAL_CLIENT_ID and INFISICAL_CLIENT_SECRET are not set in the current shell. The CLI cannot perform the Universal Auth handshake without these two variables present before the command runs. Set both variables and re-run infisical login --method universal-auth --plain --silent to refresh the cached token. See docs/troubleshooting.md.
Self-hosted instance shows 404 on /api/v3/auth/universal-auth/login
The CLI version running locally does not match the API surface exposed by the server. Older server versions may only implement v1 or v2 of the authentication endpoint. Upgrade the CLI to align with the server version, or update the server Docker image to a newer minor release. Check server logs with docker compose logs infisical. See docs/troubleshooting.md.
Universal Auth login returns 401 unauthorized
The Client ID or Client Secret provided to the CLI is incorrect, has been revoked, or the identity’s trusted IP scope excludes the calling machine’s current public IP address. Verify the secret value against the identity in the dashboard and confirm the calling IP falls within the allowed CIDR range. See docs/troubleshooting.md.
Cron job runs but secrets are empty
The wrapper script is not exporting INFISICAL_CLIENT_ID and INFISICAL_CLIENT_SECRET, or the files under /etc/infisical/ are not readable by the user that cron runs the job as. Cron provides no inherited session variables. Check file permissions with ls -la /etc/infisical/ and ensure the cron user has read access to both files. See docs/troubleshooting.md.
How do I rotate the client secret without breaking the running stack?
Generate a new client secret from the identity’s settings page in the Infisical UI while the existing secret remains valid. Update /etc/infisical/client-secret with the new value on every host that runs the wrapper script, then revoke the old secret in the dashboard. Processes already running with secrets injected by infisical run continue operating with the values present in their environment at startup time until restarted. See docs/troubleshooting.md.
Where to go next
Centralized secret management at the CLI level is one layer of a complete secrets strategy. Infisical provides deeper integrations for application code, container orchestration platforms, and API gateways. Each step up the integration stack reduces the surface area of secret exposure further.
- Want to wrap your Python code instead of the CLI? → Add Infisical to Your Python App with the SDK
- Need an MCP server in front of all this? → Add an MCP Server to Your RAG (companion post)
- First post in the series → Build a RAG System with LlamaIndex and Qdrant in Docker
Investigate the Infisical Kubernetes Operator for cloud-native secret synchronization into pods via SecretProviderClass resources. Monitoring the audit log for anomalous access patterns can surface compromised machine identity credentials before damage occurs. Always apply the principle of least privilege when scoping new identities — one path, one environment, one purpose. Rotating the client secret on a 90-day schedule and enabling email alerts for failed authentication attempts are the two highest-value hardening steps after the initial integration is stable.
Repo activity
Licence and credits
Licence: MIT for code (see LICENSE-CODE), CC-BY-4.0 for prose (see LICENSE-PROSE). By Alec Silva Couto, 2026.