Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Per-Project Environments with direnv

By Kokil Thapa | Last reviewed: September 2026

You switch from a Laravel 13 app to a legacy WordPress site and your shell still exports PHP 8.5 and Node 26. The wrong binary runs, Composer fails, and Vite builds against the wrong toolchain. Per-Project Environments with direnv fix that by loading a project’s variables, PATH entries, and tool versions the moment you enter its directory. I use this pattern daily across multiple client repos on Ubuntu, alongside environment variable hygiene and Git-based deploy workflows. This guide shows install steps, real .envrc files for PHP and Laravel, team setup, and the gotchas I hit in production-adjacent local work.

What are per-project environments with direnv and why should you use them?

direnv is a small shell extension that watches directory changes. When you cd into a folder containing an .envrc file, it evaluates that file and exports the variables it defines. When you leave, it reverses those changes. Your global shell profile stays clean.

That matters because modern stacks rarely share one toolchain. A Laravel 13 project needs PHP 8.3 or higher. A WooCommerce shop may run PHP 8.2. A Symfony 8.1 app may need PHP 8.4.1. Node versions differ too: Vite 8.x on one repo, an older build on another. Without per-project scoping, you rely on memory or brittle shell aliases.

On real client projects I often juggle legal-tech Laravel portals, WooCommerce stores, and sister sites on a shared Deployer pipeline. Each repo expects different APP_ENV, database names, and PHP binaries. direnv removes the “did I remember to run export?” step before php artisan migrate.

Per-Project Environments with direnvGlobal shell~/.bashrc onlycd project-a/.envrc loadscd project-b/.envrc swapsActive exports while inside project-a/PATH → PHP 8.3, Node 26 LTSDATABASE_URL, APP_ENV=localLeave folder → all reverted
Per-project environments with direnv swap PHP, Node, and env vars as you move between repositories.

direnv is not a full virtual machine. It does not replace Docker Compose for Laravel when you need MySQL, Redis, and queue workers in containers. It complements containers by setting connection strings and local binary paths before you run Artisan or npm.

Think of three layers:

  • Shell profile — user-wide defaults in ~/.bashrc or ~/.zshrc.
  • direnv .envrc — project-scoped exports and PATH changes.
  • .env file — application secrets read by Laravel, Symfony, or WordPress at runtime.

direnv sits between your shell and your app. It prepares the environment so php artisan serve and composer install hit the right binaries. Your framework still reads .env for database credentials. Many teams use both files together without conflict.

How do you install and configure direnv on Linux or macOS?

Installation is straightforward on Ubuntu 22/24, the platform I use for most server and local dev work. Package managers ship stable builds; the official project docs at direnv.net cover additional platforms.

Install the binary

On Ubuntu or Debian:

sudo apt update
sudo apt install direnv

On macOS with Homebrew:

brew install direnv

Verify the install:

direnv version
# direnv v2.34.x or newer

Hook direnv into your shell

direnv must hook into your shell on startup. Add one line to the end of your profile file.

For Bash (~/.bashrc):

eval "$(direnv hook bash)"

For Zsh (~/.zshrc):

eval "$(direnv hook zsh)"

Reload the shell or open a new terminal tab. Without this hook, .envrc files are ignored silently. That is the most common install mistake I see.

Allow your first project

direnv refuses to run unknown .envrc files until you explicitly trust them. This prevents malicious repos from exporting harmful variables.

  1. Create a test directory and add a minimal .envrc.
  2. cd into it; direnv prints a warning and a suggested allow command.
  3. Run direnv allow in that directory once.
  4. Leave and re-enter; the variables should load without prompts.

For teams, document the allow step in your README. New clones always need one direnv allow per machine. After that, automatic loading works on every visit.

How do you write an .envrc file for Laravel and PHP projects?

The .envrc file is plain shell code. direnv provides helper functions like layout, dotenv, and use that keep files readable. Below are patterns I use on Laravel 12 and 13 projects with PHP 8.3 or 8.4 and Node 26 LTS for Vite 8.x builds.

Basic Laravel .envrc

# .envrc — Laravel local dev
export APP_ENV=local

# Pin PHP when multiple versions exist (updateasdf, phpenv, or manual path)
export PATH="/usr/bin/php8.3/bin:$PATH"

# Load .env into direnv too (optional; Laravel reads .env separately)
dotenv_if_exists

# Node for Vite
use node 26

# Project-specific paths
export COMPOSER_MEMORY_LIMIT=-1
path_add PATH "./vendor/bin"

Commit .envrc to Git. Never commit secrets inside it. Keep passwords in .env, which stays gitignored. If you need to reference a private value, load it from a local-only file:

dotenv_if_exists .env.local

PHP version pinning with layout and use

If you use asdf or mise for runtime management, direnv integrates cleanly:

# .envrc
use asdf
layout node

The use asdf directive reads .tool-versions in the repo root. That file might contain:

php 8.3.12
nodejs 26.0.0

When you enter the directory, direnv activates those versions. This pairs well with Vite config for Laravel projects where Node and PHP must align with CI.

.envrc Evaluation Flowcd repo/Find .envrcCheck allow hashRun exportsShell readyTypical .envrc actionsexport APP_ENV=localuse node 26path_add PATH vendor/bindotenv_if_existssource_up optional parentOn cd ..Undo all exportsRestore prior PATHClear project varsNo stale env leak
How direnv evaluates .envrc on entry and cleanly reverses exports when you leave the project directory.

Database and service URLs for local stacks

When MySQL 9.7 or MariaDB 12.3 runs on localhost, point Laravel’s test runner at a dedicated database:

export DB_CONNECTION=mysql
export DB_HOST=127.0.0.1
export DB_PORT=3306
export DB_DATABASE=myapp_local
export DB_USERNAME=dev
export DB_PASSWORD=dev

# Override only for PHPUnit/Pest in this shell
export DB_DATABASE_TEST=myapp_test

Pair this with database migrations in team environments so every developer hits the same schema naming rules. direnv does not run migrations; it only ensures the connection env is correct before you run them.

Monorepo and nested layouts

Some repos nest apps under apps/api and apps/web. Use source_up to inherit a parent .envrc:

# apps/api/.envrc
source_up
export APP_NAME="My API"

The parent .envrc at the monorepo root sets shared Node and PHP versions. Child folders add app-specific overrides. This avoids duplicating ten lines across four packages.

Symfony and WordPress variants

Symfony 8.1 apps often rely on APP_ENV and APP_DEBUG at the shell level for console commands. A minimal Symfony .envrc:

export APP_ENV=dev
export APP_DEBUG=1
export SYMFONY_PHP="/usr/bin/php8.4"
path_add PATH "./bin"

WordPress 7.1 local setups may export WP_ENV and path to WP-CLI:

export WP_ENV=development
path_add PATH "$HOME/.composer/vendor/bin"

For deeper Symfony config patterns, see Symfony environment config for multi-environment apps.

How does direnv compare to Docker, dev containers, and manual exports?

Teams ask whether direnv replaces containers. In practice you pick the lightest tool that solves the reproducibility gap you actually have.

ApproachBest forSetup costReproduces OS packagesSwitch speed
direnv + .envrcPATH, env vars, runtime version pins on hostLow — one file per repoNoInstant on cd
Docker ComposeFull stack: PHP, MySQL, Redis, queuesMedium — images and compose fileYes, inside containersSlower — container start
Dev containersIDE-integrated, identical dev boxesMedium–highYesMedium
Manual exportSingle-project solo devNoneNoError-prone
VagrantLegacy VM parity, full OS matchHigh — VM provisioningYesSlow boot

My default for Laravel work on Ubuntu: direnv on the host for PHP, Composer, and Node; Docker Compose when the project needs services the host should not run globally. That hybrid keeps laptop fans quiet while still giving Redis and MySQL isolated ports. Read dev containers for reproducible development and Vagrant for reproducible dev environments when you need full parity with production Linux versions managed through Linux system administration practices.

Hybrid Local StackHost + direnvPHP 8.3 / Node 26Composer, Artisan, npmFast iterationDocker ComposeMySQL 9.7, Redis 8.10Mailpit, MinIOIsolated portsTCP.envrc sets DATABASE_URL → 127.0.0.1:3307Laravel .env matches compose published portSame pattern on CI via GitLab variables
A practical split: per-project environments with direnv on the host plus Docker for data services many Laravel apps need.

CI pipelines do not use direnv by default. GitLab CI and GitHub Actions set variables in YAML. Mirror your .envrc exports as documented env keys so local and CI stay aligned. I document the mapping in project wikis and cross-link GitLab CI/CD for PHP projects for the pipeline side.

How do teams share and secure direnv configuration?

Per-project environments only help when the whole team commits the same baseline. Treat .envrc like application code: reviewed in pull requests, versioned, and tested on a fresh clone.

What to commit versus gitignore

  • Commit: .envrc, .envrc.example, .tool-versions, and non-secret defaults.
  • Gitignore: .env, .env.local, and any file holding API keys or DB passwords.
  • Document: required system packages, Docker services, and the one-time direnv allow step.

Provide an example file new hires can copy:

# .envrc.example — copy to .envrc and customize
export APP_ENV=local
use node 26
dotenv_if_exists .env.local
path_add PATH "./vendor/bin"

When onboarding someone to a booking platform like Adventure Third Pole Trek, clear env docs cut setup from hours to minutes. The same applies to legal-tech portals where document upload paths must match across machines.

Editor and IDE integration

VS Code and PhpStorm terminals inherit direnv exports if they launch as login shells. If your IDE terminal ignores direnv, open the integrated terminal after cd into the project from a hooked shell, or configure the IDE to source the same profile. Validate with:

which php
php -v
echo $APP_ENV

Use the JSON formatter or regex tester from the same browser session; they are unrelated to direnv but handy when debugging env-driven API config during local work.

Security considerations

The allow mechanism exists because .envrc runs arbitrary shell code. Never allow unknown repos blindly. After pulling changes that modify .envrc, direnv prompts for re-allow because the file hash changed. That is a feature, not noise.

Do not store production credentials in .envrc. Production secrets belong in the server environment or your deploy tool’s encrypted store. For staging parity, read how to set up a staging environment that mirrors production and keep production URLs out of committed files.

What are common direnv mistakes and how do you fix them?

Most failures look like “it works in my terminal but not yours.” These fixes cover the patterns I see on client handoffs and multi-repo maintenance days.

direnv: command not found after install

The hook line is missing or placed before completion plugins that overwrite it. Put eval "$(direnv hook bash)" at the very end of ~/.bashrc. Open a new terminal; do not rely on source in a subshell that skips profile files.

Variables do not unload when leaving the directory

A long-running process or subshell may retain old values. direnv unloads when the shell’s current working directory changes. Background jobs started before cd keep their inherited env. Restart queue workers and dev servers after switching projects.

Conflict between dotenv and Laravel .env

dotenv in direnv exports variables into your shell. Laravel also loads .env into PHP. Duplicated keys can confuse debugging. I prefer dotenv_if_exists only for local overrides like .env.local, not the main Laravel file. Laravel’s own docs on configuration at laravel.com describe runtime loading; keep shell and app layers distinct in your mental model.

Wrong PHP binary despite .envrc

Another tool may prepend PATH later in your profile. Run which -a php to see ordering. Move conflicting managers below the direnv hook or set an absolute path in .envrc:

export PATH="/usr/bin/php8.3:$PATH"
# or
layout php

On servers with PHP 8.3, 8.4, and 8.5 side-by-side, explicit paths beat assumptions. That mirrors how I configure PHP-FPM on Ubuntu production boxes.

CI/local drift

If CI uses PHP 8.2 but your .envrc pins 8.3, tests pass locally and fail in the pipeline. Pin the same minor version in .tool-versions, .envrc, and .gitlab-ci.yml. For infrastructure-level env separation, compare with Terraform workspaces and environments and managing multiple environments in IaC — different layer, same discipline.

direnv Not Loading?.envrc ignoredHook installed?direnv allow run?Add hook to profileRun direnv allowStill wrong PHP?Check which -a php and PATH order
Decision tree for fixing per-project environments with direnv when exports never appear or show stale values.

Run direnv status inside the project for a concise report: loaded or not, which .envrc file, and hash state. That single command saves long threads in team chat.

Key Takeaways

  • Install direnv, add the shell hook once, then run direnv allow per repo to activate per-project environments with direnv.
  • Commit .envrc and .tool-versions; keep secrets in gitignored .env or .env.local files only.
  • Pair direnv with Docker Compose when you need MySQL, Redis, or mail catchers — not everything belongs on the host.
  • Pin PHP and Node versions explicitly; Laravel 13 needs PHP 8.3+, and Vite 8.x expects a current Node 26 LTS toolchain.
  • Mirror local .envrc exports in CI variables so pipelines and laptops run the same minor versions.
  • Re-run direnv allow after trusted teammates change .envrc; the hash guard is intentional security.

People Also Ask

Does direnv work with fish shell?

Yes. Install direnv, then run direnv hook fish | source in your fish config. The same .envrc file works across Bash, Zsh, and fish with no changes. Teams mixed on shells can share one committed config.

Can direnv load variables from a .env file automatically?

Yes. Use dotenv or dotenv_if_exists inside .envrc to export keys from a dotenv file into your shell. Laravel still reads .env through PHP; use direnv’s dotenv helpers mainly for shell-only tools like custom scripts or Make targets.

Is direnv safe to use on client projects with secrets?

Safe when you follow the allow workflow and never commit secrets. Treat .envrc as executable code subject to review. Store passwords and API keys in gitignored env files or your password manager, and export them locally through .env.local loaded by direnv.

How is direnv different from using nvm or asdf alone?

Version managers switch runtimes when you invoke them or when configured globally. direnv ties those switches to directory entry and exit automatically. Combining use asdf in .envrc with asdf’s per-project .tool-versions gives you hands-free changes every time you cd.

Build consistent local setups across every repo

Per-Project Environments with direnv cost almost nothing to adopt and pay off the first time you stop running the wrong PHP binary against a production-bound Laravel app. Start with one active repo: add the hook, write a ten-line .envrc, document the allow step, and align CI variables next. If you want help standardising dev environments across a multi-app portfolio or a legal-tech platform, see custom software development services, browse the project portfolio, or contact us to talk through your stack on kokil.com.np.

Frequently Asked Questions

direnv loads an .envrc file when you enter a project folder and unloads it when you leave, scoping PHP, Node, PATH, and env vars to that repo only.

On Ubuntu 22 or 24, run sudo apt update and sudo apt install direnv, then verify with direnv version. On macOS, use brew install direnv. Add eval "$(direnv hook bash)" to the end of ~/.bashrc, or eval "$(direnv hook zsh)" in ~/.zshrc, then open a new terminal. Without that hook, .envrc files are ignored silently, which is the most common install mistake I see on client handoffs. Create a test .envrc, cd into the folder, and run direnv allow once to trust the file before variables load automatically on every visit.

direnv refuses to execute unknown .envrc files until you explicitly trust them, because .envrc is arbitrary shell code that could export harmful variables from a malicious repo. Running direnv allow in a directory records a hash of that file on your machine. If a teammate later changes .envrc in a pull request, direnv prompts for re-allow because the hash changed. That guard is intentional security, not noise. Document the one-time allow step in your README so new clones on each developer laptop take minutes instead of turning into a debugging thread about missing exports.

The .envrc file is plain shell code. A practical Laravel pattern exports APP_ENV=local, pins PHP with export PATH="/usr/bin/php8.3/bin:$PATH" when multiple versions exist on Ubuntu, adds dotenv_if_exists for optional local overrides, runs use node 26 for Vite 8.x builds, sets COMPOSER_MEMORY_LIMIT=-1, and uses path_add PATH "./vendor/bin". Commit .envrc to Git but never put passwords or API keys inside it. Keep secrets in gitignored .env or .env.local. Laravel 13 needs PHP 8.3 or higher, so pin the same minor version your GitLab CI pipeline uses to avoid tests passing locally and failing in CI.

Yes. direnv is open source, installed via apt on Ubuntu or Homebrew on macOS, with no licensing cost per developer or project.

No. direnv complements containers rather than replacing them. It is best for PATH entries, environment variables, and runtime version pins on the host with low setup cost and instant switching when you cd between repos. Docker Compose fits when you need a full stack with PHP, MySQL 9.7, Redis, and queue workers isolated inside containers. My default on Ubuntu for Laravel work is direnv on the host for PHP, Composer, and Node 26 LTS, plus Docker Compose when the project needs data services the laptop should not run globally. That hybrid keeps local fans quiet while still giving Redis and MySQL isolated ports.

Think of three layers. Your shell profile holds user-wide defaults in ~/.bashrc or ~/.zshrc. direnv .envrc holds project-scoped exports and PATH changes loaded when you enter the repo. Laravel .env holds application secrets read by PHP at runtime through the framework configuration system. direnv sits between your shell and your app, preparing the environment so php artisan serve and composer install hit the right binaries. Using dotenv inside .envrc exports keys into your shell as well, which can duplicate Laravel .env keys and confuse debugging. I prefer dotenv_if_exists only for local overrides like .env.local, not the main Laravel file.

If you use asdf or mise for runtime management, add use asdf and layout node to .envrc. direnv then reads .tool-versions at the repo root, which might list php 8.3.12 and nodejs 26.0.0. When you enter the directory, those versions activate automatically. This pairs well with Laravel projects where Node 26 LTS and PHP 8.3 must align with CI. On servers with PHP 8.3, 8.4, and 8.5 side by side, an absolute path like export PATH="/usr/bin/php8.3:$PATH" beats assumptions. Run which -a php if another tool prepends PATH after the direnv hook and the wrong binary still runs.

Use direnv when you switch between repos that need different PHP, Node, or env vars—typical for Laravel, Symfony, and WordPress work on one machine.

Treat .envrc like application code: commit it, review changes in pull requests, and provide .envrc.example with non-secret defaults. Also commit .tool-versions when using asdf. Gitignore .env, .env.local, and any file holding API keys or database passwords. Document required system packages, Docker services, and the one-time direnv allow step for fresh clones. Never store production credentials in .envrc; production secrets belong on the server or in your deploy tool encrypted store. Mirror local .envrc exports as documented env keys in GitLab CI or GitHub Actions YAML so pipelines and laptops run the same minor PHP version.

The shell hook is missing or placed before completion plugins that overwrite it. Put eval "$(direnv hook bash)" at the very end of ~/.bashrc, open a new terminal instead of relying on source in a subshell, and confirm direnv version runs. If the hook works but variables never appear, you may not have run direnv allow in that directory yet. Run direnv status inside the project for a concise report showing whether the file is loaded, which .envrc path is active, and the hash state. That single command saves long threads in team chat when exports never appear or show stale values.

Yes. Use dotenv or dotenv_if_exists inside .envrc to export keys from a dotenv file into your shell session. dotenv_if_exists is safer for optional local files because it skips missing paths without error. Laravel still reads .env through PHP at runtime, so use direnv dotenv helpers mainly for shell-only tools like custom scripts, Make targets, or PHPUnit overrides such as export DB_DATABASE_TEST=myapp_test. Duplicated keys between direnv dotenv and Laravel .env can confuse debugging, so keep shell and application layers distinct in your mental model and reserve dotenv_if_exists for .env.local overrides.

Some repos nest apps under paths like apps/api and apps/web. In a child folder .envrc, call source_up to inherit the parent .envrc at the monorepo root, then add app-specific overrides such as export APP_NAME="My API". The parent file sets shared Node 26 and PHP versions once. Child folders add only what differs per package. This avoids duplicating ten lines across four packages and keeps version pins consistent when a booking platform or legal-tech portal shares one repository with multiple deployable apps. Each nested directory still needs its own direnv allow if it contains a separate .envrc file.

Yes. Install direnv through your platform package manager, then add direnv hook fish piped to source in your fish config, equivalent to the Bash and Zsh hook lines. The same committed .envrc file works across Bash, Zsh, and fish with no changes, which helps mixed teams where one developer uses fish and another uses Zsh on macOS. You still run direnv allow once per trusted project directory on each machine. Validate loaded exports with which php, php -v, and echo $APP_ENV from an IDE terminal that inherits a hooked login shell.

Variables may not unload when leaving a directory if a long-running dev server or queue worker started before you cd elsewhere; restart those processes after switching projects. Wrong PHP despite .envrc usually means another version manager prepends PATH later in your profile, so run which -a php and set an absolute binary path. CI and local drift happens when .envrc pins PHP 8.3 but GitLab CI uses 8.2; align .tool-versions, .envrc, and .gitlab-ci.yml on the same minor version. Symfony 8.1 apps need PHP 8.4.1 while a WooCommerce shop may run PHP 8.2, so per-project scoping beats relying on memory or brittle shell aliases across client repos.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: