Framework Guide

Laravel + Forge Secrets Management

Forge stores your .env file as plaintext on the server. Every team member with Forge access can read every production secret. There's no history, no audit trail, and no way to know who changed STRIPE_SECRET last Tuesday. This guide replaces that workflow with secr — encrypted storage, versioned secrets, and automated deploys.

How Laravel Handles Secrets

Laravel reads environment variables from the .env file at boot, then exposes them through two functions:

FunctionReads fromAfter config:cache
env('DB_HOST').env file via vlucas/phpdotenvReturns null — .env is not loaded
config('database.host')Config files (which call env())Returns cached value from bootstrap/cache/config.php

Critical: After running php artisan config:cache, Laravel ignores the .env file entirely. Calls to env() outside of config files will return null. Always use config() in application code, and only call env() inside config files under config/. This is a Laravel best practice regardless of secr.

Step 1: Set Up Your Project

Install the secr CLI on your development machine and initialize the project:

Terminal
npm i -g @secr/cli
secr login
secr init

This creates .secr.json in your Laravel project root. Add it to version control — it contains only the project slug, no secrets.

Step 2: Import Your Existing .env

Terminal
# Import your local development env
secr set --from-env .env --env development

# Import production values from Forge
# (copy your .env from Forge's UI into a temp file first)
secr set --from-env .env.production --env production

# Import staging if you have one
secr set --from-env .env.staging --env staging

All of Laravel's standard variables are imported: APP_KEY, DB_PASSWORD, MAIL_PASSWORD, REDIS_PASSWORD, AWS_SECRET_ACCESS_KEY — all encrypted at rest in secr.

Step 3: Local Development

Inject secrets into php artisan serve without a local .env file:

Terminal
# Start Laravel with secrets injected
secr run -- php artisan serve

# Or with Herd/Valet — pull to .env instead
secr pull --format dotenv --env development > .env

secr run sets secrets as environment variables on the process. Laravel's vlucas/phpdotenv won't override variables that already exist in the environment, so secr values take priority even if a stale .env file is present.

Using Laravel Herd or Valet? These tools manage the PHP process themselves, so secr run won't work. Use secr pull to write a .env file instead. Add a Composer script to make it easy:

composer.json
{
  "scripts": {
    "env:pull": "secr pull --format dotenv --env development > .env",
    "env:pull:staging": "secr pull --format dotenv --env staging > .env"
  }
}

Step 4: Queues, Scheduler & Tinker

Laravel has multiple entry points that all need secrets. Wrap each one with secr run:

Terminal
# Queue worker
secr run -- php artisan queue:work redis --tries=3

# Scheduler
secr run -- php artisan schedule:work

# Tinker (interactive REPL)
secr run -- php artisan tinker

# Migrations
secr run -- php artisan migrate --force

# Run tests against staging secrets
secr run --env staging -- php artisan test

Every process gets the same secrets from secr. No more copying .env files between terminal tabs or forgetting to restart the queue worker after a secret change.

Step 5: Deploy to Forge

Replace Forge's built-in .env editor with secr. The secr CLI runs inside Forge's deploy script to pull secrets and write the .env file before config:cache runs.

1. Install the CLI on your Forge server

SSH into your Forge server (or use a Forge recipe) and install the secr CLI globally:

Server SSH
npm i -g @secr/cli

2. Create a deploy token

Your local machine
secr token create --name "forge-production"

3. Store the token on the server

In Forge, go to your site → Environment and add the secr token. Or set it as a server-level environment variable so all sites can use it:

Forge .env (minimal — just the secr config)
SECR_TOKEN=secr_tok_...
SECR_ORG=my-org
SECR_PROJECT=my-laravel-app

4. Update the deploy script

In Forge, go to your site → Deployments → Deploy Script and replace the default script:

Forge Deploy Script
cd /home/forge/myapp.com

git pull origin $FORGE_SITE_BRANCH

# Pull secrets from secr and write .env
secr pull --format dotenv --env production \
  --org $SECR_ORG --project $SECR_PROJECT > .env

composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader

php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

php artisan queue:restart

( flock -w 10 9 || exit 1
    echo 'Restarting FPM...'; sudo -S service $FORGE_PHP_FPM reload ) 9>/tmp/fpmlock

The key line is secr pull --format dotenv --env production > .env. This overwrites the .env file with the latest secrets from secr before config:cache bakes them into the cached config. Queue workers pick up the new values after queue:restart.

Staging & Multiple Sites

If your Forge server hosts both production and staging, use different secr environments in each deploy script:

Forge Sitesecr pull command
myapp.comsecr pull --format dotenv --env production > .env
staging.myapp.comsecr pull --format dotenv --env staging > .env

Each site pulls from the correct secr environment. Staging never sees production database credentials, and there's no risk of copy-pasting the wrong .env between sites.

CI: Run Tests with Secrets

.github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8
        env:
          MYSQL_DATABASE: testing
          MYSQL_ROOT_PASSWORD: password
        ports: ['3306:3306']
        options: --health-cmd="mysqladmin ping" --health-interval=10s

    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: 8.3
          extensions: mbstring, pdo_mysql

      - name: Pull secrets
        uses: secr-dev/secr-action@v1
        with:
          token: ${{ secrets.SECR_TOKEN }}
          org: my-org
          project: my-laravel-app
          environment: staging

      - run: composer install --no-interaction --prefer-dist
      - run: php artisan config:cache
      - run: php artisan test --parallel

The secr action exports all secrets as environment variables. config:cache bakes them into the cached config, then artisan test runs with the same secrets staging uses.

Scan for Leaked Secrets

Laravel projects are notorious for .env files slipping into version control. The default .gitignore covers .env but not .env.production or .env.staging. Scan your repo:

Terminal
# Scan the whole project
secr scan

# Install a pre-commit hook
secr guard install

The scanner detects Laravel's APP_KEY=base64:... pattern, database URLs, mail passwords, AWS credentials, Stripe keys, and 20+ more patterns.

Common Laravel Patterns

Rotate APP_KEY safely

Laravel's APP_KEY is used for encryption. When you rotate it, update secr and redeploy:

Terminal
# Generate a new key
php artisan key:generate --show
# Output: base64:AbCdEfGhIjKlMnOpQrStUvWxYz123456789=

# Update it in secr
secr set APP_KEY "base64:AbCdEfGhIjKlMnOpQrStUvWxYz123456789=" \
  --env production --description "Laravel encryption key — rotated 2025-02-01"

# Deploy to Forge (triggers the deploy script which pulls from secr)
# Or trigger manually in Forge's dashboard

Promote secrets between environments

Copy all secrets from staging to production when you're ready to go live:

Terminal
# Preview what would change
secr promote --from staging --to production --dry-run

# Promote (skip existing keys by default)
secr promote --from staging --to production

# Overwrite existing values
secr promote --from staging --to production --overwrite

Templates for required variables

Define the secrets every Laravel environment must have. Catch missing variables before they cause 500 errors:

Terminal
secr template add APP_KEY
secr template add APP_URL
secr template add DB_HOST
secr template add DB_DATABASE
secr template add DB_USERNAME
secr template add DB_PASSWORD
secr template add MAIL_HOST
secr template add MAIL_USERNAME
secr template add MAIL_PASSWORD

# Validate before deploying
secr template validate --env production

Forge recipe for CLI installation

Use a Forge recipe to install the secr CLI on all your servers at once:

Forge Recipe
# Install Node.js (if not already present) and secr CLI
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
npm i -g @secr/cli

# Verify
secr --version

Complete Workflow

Local dev

secr run -- php artisan serve injects secrets. With Herd/Valet, use secr pull to write .env.

Forge deploys

Deploy script runs secr pull > .env before config:cache. Secrets are always current.

Queue workers

queue:restart after deploy picks up the new cached config. No separate .env needed.

CI pipeline

secr GitHub Action exports secrets for config:cache and artisan test.

Why Not Just Use Forge's .env Editor?

 Forge .env editorsecr
Encryption at restPlaintext on server diskAES-256-GCM + KMS
Version historyNoneFull version history per key
Audit trailNoneWho changed what, when
Multi-environmentOne .env per siteUnlimited environments per project
Team access controlForge account = full accessRBAC: owner, admin, developer, viewer
Local dev syncCopy-paste from Forge UIsecr run or secr pull
Secret scanningNone20+ patterns + pre-commit hook

Stop editing .env in Forge

npm i -g @secr/cli

secr init

secr set --from-env .env --env production