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

Terminal
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

.github/workflows/ci.yml
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 build

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

Vercel Build Command
npx @secr/vercel && next build

Add three environment variables in Vercel's settings:

Vercel Environment Variables
SECR_TOKEN=secr_tok_...       # Deploy token from secr
SECR_ORG=my-org              # Your organization slug
SECR_PROJECT=my-app          # Your project slug

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

Terminal
npm install @secr/netlify-plugin
netlify.toml
[[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:

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

docker-compose.yml
services:
  app:
    build: .
    environment:
      - SECR_TOKEN=${SECR_TOKEN}
      - SECR_ORG=my-org
      - SECR_PROJECT=my-app
      - SECR_ENVIRONMENT=production

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

PlatformIntegrationEnvironment
Local developmentsecr run -- npm devdevelopment
GitHub Actionssecr-dev/secr-action@v1staging
Vercel previewnpx @secr/vercelstaging (auto-detected)
Vercel productionnpx @secr/vercelproduction (auto-detected)
Netlify@secr/netlify-pluginproduction
Dockersecr run -- node server.jsSet 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:

Terminal
# 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 production

One 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