Monorepos
Monorepos often mix shared platform config with app-specific settings. Varlock supports both per-package schemas and shared root config, with @import() to compose them. This guide walks through common layout choices, incremental adoption, CI, and containers.
Schema layout
Section titled “Schema layout”The recommended pattern is one .env.schema per project — each app or service owns a schema next to its code and imports whatever shared config it needs from the repo root or sibling packages. Avoid funneling every variable into a single central env directory; keep each project’s config with the project.
One schema per project (recommended)
Section titled “One schema per project (recommended)”Give each app or service its own .env.schema. This keeps config next to the code that uses it, lets you adopt varlock one package at a time, and works well even when services are mostly independent.
.env.schema # shared root config (imported by apps that need it)packages/ web/.env.schema api/.env.schemaVarlock resolves env files from each package’s current working directory — it does not walk up to a parent directory automatically. Use @import() when a package needs config defined elsewhere.
Sharing config from the root or siblings
Section titled “Sharing config from the root or siblings”Put cross-cutting config — database URLs used by many services, shared feature flags, platform identifiers — in a schema at the repo root, then import the root directory from each project that needs it:
# Import only the shared keys this app needs# @import(../../, pick=[DATABASE_URL, REDIS_URL, SENTRY_*])# @import(../api/, pick=[API_PUBLIC_URL])# ---# App-specific config# @type=url @requiredPUBLIC_SITE_URL=By default every key in the imported source is brought in, but importing only the keys an app actually needs is usually the cleaner default — use pick=[...] (allowlist) or omit=[...] (denylist) so each schema declares exactly what it depends on.
Importing the directory (../../) rather than the schema file (../../.env.schema) means all files like .env.local and environment-specific are loaded too — not just the schema — following the same precedence as the current directory. Imported definitions merge with the importing schema, and the importing file wins. See the Imports guide for partial imports, precedence, and conditional loading.
Use @import() to pull in a root or shared schema without copying keys, to split a large schema into focused files (e.g. .env.database, .env.features), or to share a directory of common env files across services (# @import(../../shared-config/)).
You can also import directly from a sibling package, which lets you keep a config item with its logical owner instead of hoisting everything to the root. Define the item once in the package that owns it, then pick just what you need — like the ../api/ import in the example above.
Pointing varlock at the right directory
Section titled “Pointing varlock at the right directory”In most cases you don’t need anything here — a .env.schema in each package, loaded from its working directory, is the right default. Reach for the options below only when a package’s env files live somewhere other than its cwd.
When an app’s env files live outside its default cwd (or you need multiple roots), set varlock.loadPath in that package’s package.json:
{ "varlock": { "loadPath": "./" }}You can also pass an explicit path on the CLI with --path (or -p) — useful in CI or scripts that run from the monorepo root:
npm exec -- varlock load --path apps/web/pnpm exec -- varlock load --path apps/web/bunx varlock load --path apps/web/vlx -- varlock load --path apps/web/yarn exec -- varlock load --path apps/web/For loading from multiple directories in one package, see Loading from multiple directories in the Vite integration docs (the same loadPath array works for all commands).
Next.js apps
Section titled “Next.js apps”Next.js requires a workspace-root @next/env override and has extra footguns when only some apps use varlock. See Next.js in a monorepo for package-manager overrides, incremental adoption, and troubleshooting — then return here for shared schema layout and CI patterns.
Turborepo and strict env mode
Section titled “Turborepo and strict env mode”Turborepo v2+ enables Strict Environment Mode by default. Tasks only receive env vars that are listed in turbo.json, so any var varlock reads from the ambient environment — not just your @currentEnv flag — must be declared there, or it won’t reach the task. This includes:
- your environment flag (e.g.
APP_ENV), or varlock may load the wrong.env.[currentEnv]file - the CI-detection vars that built-ins like
VARLOCK_ENVrely on (CI,VERCEL,WORKERS_CI_BRANCH, etc.) - any var passed in at the root
turboinvocation, or otherwise present in the environment, that aVARLOCK_*setting or a resolver in your schema depends on
Declare these in globalEnv (or per-task env) in turbo.json. Full explanation and examples are in Using currentEnv in Turborepo.
Incremental adoption
Section titled “Incremental adoption”You do not need to migrate every package at once. A practical order:
- Pick one app — run
varlock initin that package and commit its.env.schema. - Wire the integration for that stack only (Next.js plugin, Vite plugin, or
varlock runfor scripts). - Expand shared config — extract common keys to a root or shared schema and
@import()them from apps as you go. - Leave untouched apps alone — packages without a
.env.schemashould keep using their existing env loading until you migrate them.
Mixed stacks (JavaScript and other languages)
Section titled “Mixed stacks (JavaScript and other languages)”Monorepos often combine Node apps with Python, Go, or other runtimes. Varlock’s CLI works the same everywhere: load and validate from a schema, then exec your command with the resolved env injected.
npm exec -- varlock run -- python manage.py runserverpnpm exec -- varlock run -- python manage.py runserverbunx varlock run -- python manage.py runservervlx -- varlock run -- python manage.py runserveryarn exec -- varlock run -- python manage.py runserverRun the command from the service directory (or pass --path to its env folder). Each language can keep its own .env.schema; there is no requirement to share a single root file. For more detail, see Other languages.
Container builds
Section titled “Container builds”Container images often copy a single app directory, not the whole monorepo. If your schema uses @import() to pull files from sibling packages or the repo root, those paths must exist in the build context — otherwise the load graph fails at build time.
The Docker guide covers the official image, multi-stage copies of the varlock binary, and running varlock run as your container entrypoint.
Today you may need to copy imported env files explicitly in your Dockerfile (mirroring the import tree). A planned varlock flatten command will collapse an import graph into local files for slim Docker contexts — see the Docker guide.