log in
consulting hosting industries the daily tools about contact

Doppler vs. .env Files: What 'Simple' Actually Costs You

I ran .env files on single-server stacks for years. Doppler didn't sell me on features — it sold me on the operational debt I'd quietly been carrying.

I ran .env files on single-server stacks for a long time and told myself it was fine because the server was managed, the team was small, and a secrets manager felt like enterprise overhead for problems I didn't have. Then a client asked me to produce an audit trail of everyone who'd accessed their Stripe secret key over the past six months. I had nothing. That was the moment I started taking this more seriously.

The .env Approach (and Why It Works Until It Doesn't)

On a typical single-server Laravel stack, the setup looks like this: you have a .env file sitting in /var/www/yourapp, owned by www-data, not committed to git, deployed manually or via a deploy script that SSHs in and does what it needs to do. It works. I've shipped dozens of apps this way.

The operational pattern ends up being:

  • Developer needs a new secret → someone SSHes in and edits .env
  • New server → someone copies .env over via SCP or pastes it from a shared note somewhere
  • Contractor offboarded → you hope someone remembers to rotate the secrets they touched
  • Secret leaked → you have no idea when it happened or who had access

None of these are catastrophic in isolation. Together, over two or three years on a real client account, they add up to a posture that's genuinely hard to defend.

The real cost isn't the breach you had. It's the audit you can't do, the rotation you skipped because it was painful, and the .env copy that lived in someone's ~/Desktop folder for six months.

What Doppler Actually Does

Doppler is a secrets manager with a CLI, a dashboard, and integrations for most deployment targets. The pitch is that instead of distributing secret values, you distribute access to Doppler, and Doppler injects secrets at runtime.

For a single-server stack, the operational model shifts to:

  • Secrets live in Doppler, versioned and audited
  • The server authenticates to Doppler via a service token scoped to that environment
  • Your app either uses the Doppler CLI to inject env vars at startup, or fetches them via the API
  • Onboarding/offboarding happens in the Doppler dashboard, not via SSH

That last point is the one that actually matters day-to-day.

A Working Setup for Laravel on a Single Server

Here's how I set this up for a Laravel app running under Supervisor on Ubuntu.

First, install the Doppler CLI on the server:

apt-get install -y apt-transport-https
curl -sLf --retry 3 --tlsv1.2 --proto "=https" \
  'https://packages.doppler.com/public/cli/gpg.DE2A7741A397C129.key' \
  | gpg --dearmor -o /usr/share/keyrings/doppler-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/doppler-archive-keyring.gpg] \
  https://packages.doppler.com/public/cli/deb/debian any-version main" \
  | tee /etc/apt/sources.list.d/doppler-cli.list
apt-get update && apt-get install doppler

Then configure a service token for the environment:

doppler configure set token dp.st.prd.xxxxxxxxxxxxxxxxxxxx
doppler configure set project myapp
doppler configure set config prd

The Supervisor config for your queue worker changes from running php artisan queue:work directly to wrapping it with Doppler:

[program:myapp-worker]
command=doppler run -- php /var/www/myapp/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=www-data
stdout_logfile=/var/log/supervisor/myapp-worker.log

For PHP-FPM, I inject the env vars into the pool config. Doppler can write them directly:

doppler secrets download --no-file --format=env-no-quotes >> /etc/php/8.2/fpm/pool.d/myapp.conf

I don't love that approach for production — it writes values to disk, which partially defeats the purpose. What I actually do is run a small deploy hook that populates an ephemeral env file on each deploy, then removes it after PHP-FPM reloads. Imperfect, but it keeps the values out of the repo and off developer machines.

For reading secrets in Laravel without relying on the injected environment at all, you can use the Doppler API directly:

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;

class DopplerSecrets
{
    public function get(string $name): string
    {
        return Cache::remember("doppler_secret_{$name}", 300, function () use ($name) {
            $response = Http::withToken(config('services.doppler.token'))
                ->get("https://api.doppler.com/v3/configs/config/secret", [
                    'project' => config('services.doppler.project'),
                    'config' => config('services.doppler.config'),
                    'name' => $name,
                ]);

            if ($response->failed()) {
                throw new \RuntimeException("Failed to fetch secret: {$name}");
            }

            return $response->json('value.computed');
        });
    }
}

I cache with a 5-minute TTL. Fetching every secret on every request is a bad time — Doppler's API is fast but it's still a network call, and you will eventually hit rate limits if your traffic spikes.

The Gotchas That Bit Me

The chicken-and-egg problem. You need a Doppler token to authenticate to Doppler. That token has to live somewhere on the server. If you're already comfortable securing a .env file, securing a single service token isn't fundamentally harder — but don't let anyone tell you Doppler eliminates secrets from your infrastructure entirely. It reduces them to one, which is meaningfully better.

Caching stale secrets. When you rotate a secret in Doppler, your running processes don't know until they restart or the cache expires. This burned me once with a payment processor key rotation — the old key was revoked before I restarted the workers. Build a deploy/rotation checklist that includes supervisorctl restart and PHP-FPM reload.

The free tier is limited on audit logging. Doppler's audit logs, which were the whole reason I started evaluating this, are gated behind paid plans. The free tier will get you the workflow and the sync, but if the audit trail is your primary motivation (it was mine), check the pricing page before you commit.

doppler run -- adds ~200ms to process startup. For long-running processes under Supervisor, irrelevant. For a short CLI command called frequently — a cron that runs every minute, for example — that latency accumulates. I use the API client above for those cases instead.

Environment parity breaks differently. With .env, every developer has a local copy and prod differences are usually obvious. With Doppler, developers authenticate to a dev config, which is great, but if someone misconfigures the environment mapping (dev vs stg vs prd), the failure mode is subtle. Pin environment configs explicitly in your deploy scripts and don't rely on the CLI's remembered config being correct on the server.

When I'd Reach for Doppler

Multi-developer teams where secrets rotate or people turn over. The friction of "edit the .env on the server" is lowest when it's just you. The moment you have two developers and a contractor, Doppler's access controls pay for themselves the first time someone leaves.

Clients with compliance requirements. Healthcare and biotech clients I work with will ask about secret access auditing. Having a dashboard where I can show access logs is worth the monthly cost without further analysis.

Multiple environments on a single server or a small cluster. Keeping dev, staging, and prod secrets in sync without copying files around is where Doppler genuinely shines. The environment promotion flow (promote staging secrets to prod with one CLI command) is something .env files can't replicate without custom tooling.

When I Wouldn't Bother

If it's a solo project, a single environment, and I'm the only one with server access — the .env approach with a strong chmod 600, a good backup, and discipline about not putting it in Slack is honestly fine. The operational cost of Doppler (the service token management, the CLI setup, the caching layer) is real, and it only pays off when the team or the secret surface area grows past a certain threshold.

I've also seen teams introduce Doppler and then immediately use it wrong — storing non-sensitive config in it, bypassing it for "quick" changes by just editing the server directly, or losing track of the service token they stored in LastPass under a departed employee's account. The tool doesn't enforce discipline. Your process still has to.

The Bottom Line

The operational cost of .env files isn't in any one place — it's in the rotation you didn't do, the offboarding you half-finished, and the audit you couldn't produce. Doppler doesn't eliminate that cost, but it makes paying it much harder to avoid. On any project where I'm not the only stakeholder touching secrets, I'd set it up from day one — the early friction is small compared to retrofitting it when something goes wrong.

Need help shipping something like this? Get in touch.