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:
| Function | Reads from | After config:cache |
|---|---|---|
| env('DB_HOST') | .env file via vlucas/phpdotenv | Returns 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:
npm i -g @secr/cli
secr login
secr initThis 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
# 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 stagingAll 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:
# Start Laravel with secrets injected
secr run -- php artisan serve
# Or with Herd/Valet — pull to .env instead
secr pull --format dotenv --env development > .envsecr 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:
{
"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:
# 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 testEvery 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:
npm i -g @secr/cli2. Create a deploy token
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:
SECR_TOKEN=secr_tok_...
SECR_ORG=my-org
SECR_PROJECT=my-laravel-app4. Update the deploy script
In Forge, go to your site → Deployments → Deploy Script and replace the default 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/fpmlockThe 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 Site | secr pull command |
|---|---|
| myapp.com | secr pull --format dotenv --env production > .env |
| staging.myapp.com | secr 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
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 --parallelThe 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:
# Scan the whole project
secr scan
# Install a pre-commit hook
secr guard installThe 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:
# 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 dashboardPromote secrets between environments
Copy all secrets from staging to production when you're ready to go live:
# 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 --overwriteTemplates for required variables
Define the secrets every Laravel environment must have. Catch missing variables before they cause 500 errors:
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 productionForge recipe for CLI installation
Use a Forge recipe to install the secr CLI on all your servers at once:
# 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 --versionComplete 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 editor | secr | |
|---|---|---|
| Encryption at rest | Plaintext on server disk | AES-256-GCM + KMS |
| Version history | None | Full version history per key |
| Audit trail | None | Who changed what, when |
| Multi-environment | One .env per site | Unlimited environments per project |
| Team access control | Forge account = full access | RBAC: owner, admin, developer, viewer |
| Local dev sync | Copy-paste from Forge UI | secr run or secr pull |
| Secret scanning | None | 20+ patterns + pre-commit hook |
Stop editing .env in Forge
npm i -g @secr/cli
secr init
secr set --from-env .env --env production