DevOps Guide
Managing Secrets in CI/CD Pipelines
Your application needs secrets in four places: local development, CI pipelines, preview deployments, and production. Most teams manage each one differently — GitHub Actions secrets here, Vercel env vars there, a shared .env for local dev. This guide shows you how to unify them under a single source of truth.
The CI/CD Secrets Problem
As applications grow, secrets fragment across platforms. A typical team ends up managing credentials in multiple places:
- —GitHub Actions secrets for CI test runs and deployments.
- —Vercel or Netlify env vars for preview and production builds.
- —Docker Compose env_file entries for containerized services.
- —Local .env files shared over Slack for developer machines.
When a database password rotates, you update it in four different dashboards. When a new API key is added, you coordinate across teams to make sure every platform gets the value. Inevitably, something gets missed and a pipeline breaks at 2 AM.
Common Mistakes
Hardcoding secrets in pipeline files
Values pasted directly into workflow YAML, Dockerfiles, or build scripts. These get committed to version control and are visible to anyone with repo access.
Too many platform-specific secrets
The same DATABASE_URL stored in GitHub Actions, Vercel, Netlify, and Railway. When it changes, you update one and forget the others.
No rotation process
Secrets set once and never changed. When an employee leaves or a key is compromised, nobody knows which pipelines use the affected credential.
Using production secrets in CI
Test pipelines running against production databases because nobody set up a separate staging environment. One bad test can corrupt live data.
Best Practices
Regardless of which tools you use, these principles apply to every CI/CD setup:
Single source of truth
Secrets should be defined in one place and pulled by every platform that needs them. When a value changes, you update it once.
Least privilege
CI pipelines should only have access to the secrets they need. A linting job does not need the production database URL. Use environment-scoped tokens.
Rotation without downtime
When you update a secret in your secrets manager, the next pipeline run should pick up the new value automatically. No manual updates across platforms.
Audit trail
Every secret read and write should be logged with who, when, and from where. If a credential leaks, you need to trace the blast radius.
GitHub Actions with secr
secr provides a composite GitHub Action that pulls secrets and exports them as environment variables for the rest of the job. You store a single SECR_TOKEN in your GitHub repo — everything else comes from secr.
1. Create a deploy token
secr token create --name "github-actions-ci"2. Add it to GitHub
Go to your repo → Settings → Secrets and variables → Actions and add SECR_TOKEN as a repository secret.
3. Use the action in your workflow
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Pull secrets from secr
uses: secr-dev/secr-action@v1
with:
token: ${{ secrets.SECR_TOKEN }}
org: my-org
project: my-app
environment: staging
- run: npm ci
- run: npm test
- run: npm run buildSecrets are exported as environment variables and automatically masked in GitHub Actions logs. The pipeline reads them via process.env exactly like local development.
Key benefit: When you add a new secret or rotate an existing one in secr, every CI run automatically picks up the latest values. No manual updates in the GitHub UI.
Vercel Deploys
The @secr/vercel package pulls secrets before your framework builds. Add it as a build step and store only the secr connection details in Vercel's env vars:
npx @secr/vercel && next buildAdd three environment variables in Vercel's settings:
SECR_TOKEN=secr_tok_... # Deploy token from secr
SECR_ORG=my-org # Your organization slug
SECR_PROJECT=my-app # Your project slugThe package automatically detects Vercel's VERCEL_ENV (production, preview, or development) and pulls the matching environment from secr. Preview deployments get staging secrets. Production deployments get production secrets.
Netlify Deploys
The @secr/netlify-plugin is a Netlify Build Plugin that injects secrets into process.env before your build starts:
npm install @secr/netlify-plugin[[plugins]]
package = "@secr/netlify-plugin"
[plugins.inputs]
org = "my-org"
project = "my-app"Add SECR_TOKEN as an environment variable in Netlify's dashboard. The plugin runs during onPreBuild and makes all secrets available for the build process.
Docker Builds
For containerized applications, use secr run as the entrypoint to inject secrets at container startup. This keeps secrets out of the image layers entirely:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build
# Install secr CLI
RUN npm i -g @secr/cli
# Inject secrets at runtime, not build time
CMD ["secr", "run", "--", "node", "dist/server.js"]Pass the token and configuration as environment variables when running the container:
services:
app:
build: .
environment:
- SECR_TOKEN=${SECR_TOKEN}
- SECR_ORG=my-org
- SECR_PROJECT=my-app
- SECR_ENVIRONMENT=productionNever use ARG for secrets. Docker build arguments are visible in the image history via docker history. Always inject secrets at runtime, not during the build stage.
The Pattern: One Source, Every Platform
Regardless of where your code runs, the workflow is the same:
| Platform | Integration | Environment |
|---|---|---|
| Local development | secr run -- npm dev | development |
| GitHub Actions | secr-dev/secr-action@v1 | staging |
| Vercel preview | npx @secr/vercel | staging (auto-detected) |
| Vercel production | npx @secr/vercel | production (auto-detected) |
| Netlify | @secr/netlify-plugin | production |
| Docker | secr run -- node server.js | Set via SECR_ENVIRONMENT |
Secrets are defined once in secr, organized by environment, and pulled on demand by each platform. When you promote a secret from staging to production, use the promotion command:
# Promote a tested value from staging to production
secr promote DATABASE_URL --from staging --to production
# Promote all secrets for a release
secr pull -e staging | secr set --from-stdin -e productionOne source of truth for every pipeline
Stop managing secrets across four different dashboards. Define them once, pull them everywhere, and rotate without touching a single pipeline config.
npm i -g @secr/cli
secr init
secr set DATABASE_URL="postgresql://..." -e production
secr run -- npm test