BoxLang 🚀 A New JVM Dynamic Language Learn More...
A per-application .env for ColdBox apps that share one
CFML engine with other sites.
On a typical shared Windows/IIS server, every site runs in the same
Adobe ColdFusion (or Lucee) JVM. Loading .env the usual
way puts each value into JVM system properties or the OS environment.
Then DB_PASSWORD from one site overwrites another's, and
every site can read every other site's secrets. ColdBox's
getSystemSetting() only looks there, so it can't help.
AppDotEnv keeps each app's values in that app's own
application scope, and reads them the way ColdBox does:
Application.cfc parses the app's
.env into application.dotenv before
ColdBox boots.application.dotenv.getSystemSetting(),
getSystemProperty() and getEnv() take the
same arguments as ColdBox's methods of those names, and return what
they returned in development. Switching is a find-and-replace.DotEnv@appdotenv.?fwreinit=<password> re-reads the
file and looks for it again, so changing a value or moving the file
needs no restart.| Environment | File | Why |
|---|---|---|
| Development | <webroot>/.env
| The same file CommandBox loads for the dev server. |
| Production | <webroot>/../.env
| One level above the webroot, so the web server can never serve it. |
The webroot file wins when both exist. When neither exists the app still boots on defaults, and the startup log names the production path it expected:
C:\websites\example.com\
.env <- production secrets
wwwroot\ <- IIS physical path and the app root
The location is chosen again on every load, so a reinit follows a file that was added, removed or moved between the two places. While both exist the webroot file still wins, so delete the webroot copy when you move to the production layout.
box install appdotenv
ColdBox loads modules after it reads your config, so the module can't
load the file itself. A few lines in Application.cfc do that.
public boolean function onApplicationStart(){
// Before the ColdBox bootstrap: config CFCs read application.dotenv.
application.dotenv = new modules.appdotenv.models.DotEnv( appRoot = COLDBOX_APP_ROOT_PATH ).load();
application.cbBootstrap = new coldbox.system.Bootstrap( COLDBOX_CONFIG_FILE, COLDBOX_APP_ROOT_PATH, COLDBOX_APP_KEY, COLDBOX_APP_MAPPING );
application.cbBootstrap.loadColdbox();
return true;
}
public boolean function onRequestStart( string targetPage ){
// The module re-reads .env on every authorized reinit. These lines cover the two cases it
// can't: an app that started before they were deployed, and a ColdBox that failed to boot
// or reinit (its controller is gone, so ColdBox boots again on this request). Fix the
// file, load any page, and the app recovers without a restart.
var cbKey = len( trim( COLDBOX_APP_KEY ) ) ? COLDBOX_APP_KEY : "cbController";
if ( !application.keyExists( "dotenv" ) ) {
application.dotenv = new modules.appdotenv.models.DotEnv( appRoot = COLDBOX_APP_ROOT_PATH ).load();
} else if ( !application.keyExists( cbKey ) ) {
application.dotenv.reload();
}
application.cbBootstrap.onRequestStart( arguments.targetPage );
return true;
}
The dotted path assumes the default install location,
modules/appdotenv/. If the module loads without
application.dotenv, it throws
AppDotEnv.NotBooted with the line to add.
Settings in the Application.cfc pseudo-constructor
(this.datasources, this.mappings and so on)
run before onApplicationStart, so they can't rely on application.dotenv.
The test runner is a separate application with its own
this.name, so it has its own application scope, and
ColdBox's VirtualApp boots ColdBox inside it. Your root
Application.cfc never runs there, so without this step
every spec fails with AppDotEnv.NotBooted.
This is the onRequestStart from the standard ColdBox app
template with the dotenv lines added. It relies on the template's
rootPath variable and /root mapping:
public boolean function onRequestStart( targetPage ){
// Set a high timeout for long running tests
setting requestTimeout="9999";
// The test runner is its own application, so it needs its own application.dotenv before
// ColdBox boots. It reads the app's real .env; ?fwreinit re-reads it.
if ( !application.keyExists( "dotenv" ) ) {
application.dotenv = new root.modules.appdotenv.models.DotEnv( appRoot = rootPath ).load();
} else if ( url.keyExists( "fwreinit" ) ) {
application.dotenv.reload();
}
// New ColdBox Virtual Application Starter
request.coldBoxVirtualApp = new coldbox.system.testing.VirtualApp( appMapping = "/root" );
// If hitting the runner or specs, prep our virtual app
if ( getBaseTemplatePath().replace( expandPath( "/tests" ), "" ).reFindNoCase( "(runner|specs)" ) ) {
request.coldBoxVirtualApp.startup();
}
// Reload for fresh results
if ( url.keyExists( "fwreinit" ) ) {
if ( server.keyExists( "lucee" ) ) {
pagePoolClear();
}
request.coldBoxVirtualApp.restart();
}
return true;
}
If your tests/Application.cfc looks different, two
things matter: create application.dotenv before ColdBox
starts, and point appRoot at the app root, not at
/tests. The template's onRequestEnd shuts
the virtual app down after each request, but that only removes
ColdBox's keys, so the instance lasts across test requests.
VirtualApp.restart() doesn't announce
preReinit, which is why the fwreinit branch
reloads the file itself.
// config/ColdBox.cfc, config/modules/*.cfc
reinitPassword : application.dotenv.getSystemSetting( "COLDBOX_REINIT_PASSWORD", "" ),
defaultSecret : application.dotenv.getSystemSetting( "CMS_SECRET" ), // no default: a missing key stops the boot
// handlers, models, interceptors
property name="dotenv" inject="DotEnv@appdotenv";
var to = dotenv.getSystemSetting( "CONTACT_TO", "[email protected]" );
A key that is in the file with a blank value
(CMS_SECRET=) returns "", as
ColdBox does. Only a key found nowhere stops the boot.
ColdBox works out ENVIRONMENT from JVM and OS values,
not your file. To read it from the file, add this to config/ColdBox.cfc:
function detectEnvironment(){
return application.dotenv.getSystemSetting( "ENVIRONMENT", "production" );
}
Other modules' ModuleConfig.cfc defaults that call
getSystemSetting() use ColdBox's version, so they don't
see the file. Set those values in
config/modules/<module>.cfc instead.
| Method | Does |
|---|---|
init( path = "", appRoot = "" )
| path names the file exactly;
appRoot resolves it as described above, again on
every load(). |
load() / reload()
| Read the file. A missing file isn't an error. |
getSystemSetting( key, [default] )
| File, then Java system property, then OS env, then default. |
getSystemProperty( key, [default] )
| File, then Java system property, then default. |
getEnv( key, [default] )
| File, then OS env, then default. |
get( key, [default] )
| Alias of getSystemSetting(). |
has( key )
| Whether getSystemSetting() would succeed
without a default. |
keys()
| Sorted key names from the file. Safe to log. |
describe()
| One log-safe line: key count and path. The module logs it on load. |
getPath(), isLoaded(), getLoadedAt()
| What the last load read (or looked for), and when. |
syncWithRequest( [params] )
| Deprecated. Reloads on any fwreinit, even
with the wrong password. Remove it from
onRequestStart; the module reloads on ColdBox's preReinit. |
Without a default, the lookups throw
SystemSettingNotFound, the type ColdBox uses. The key is
in message and the file path only in
extendedInfo. ColdBox's default public error page doesn't
show extendedInfo, but its BugReport.cfm and
Whoops.cfm templates do, so don't use those in production.
A key in the file wins over JVM and OS values even when it's blank,
so another site's JVM-global setting can't replace it. Keys missing
from the file fall back to the JVM and the OS, as ColdBox's methods
do. On a shared server those are shared by every site, so keep every
key the app reads in its .env. Values live in the
component's variables scope, so dumping the application
scope doesn't print them.
The same rules as CommandBox, which loads this file into the dev server: a JSON object is read as JSON (simple values only), and anything else as a Java properties file. Coming from other dotenv tools, watch for these:
KEY="abc" is
"abc", quotes included.# and ! start a comment only at the start
of a line. KEY=abc #1 is abc #1.\t, \n,
\uXXXX, \\), and a single backslash before
anything else is dropped. Write Windows paths as
C:\\data or C:/data.=, : or whitespace ends the key, so
KEY: value and KEY value both work.\ at the end of a line continues the value on the
next line.export means nothing: export KEY=1 sets a
key named export.${VAR} interpolation.Two deliberate differences from CommandBox: the file is read as UTF-8 (CommandBox reads it as ISO-8859-1, which garbles non-ASCII characters), and key lookups ignore case, so keys that differ only in case collide. CRLF line endings and a UTF-8 BOM are fine.
2.0 makes each value match what ColdBox's
getSystemSetting() returned in development. Check your
.env files before upgrading:
# comment becomes part of the value. Move
it to its own line.export KEY=value no longer sets KEY.
Remove export."" instead of falling
through to the JVM, the OS or the default.Then replace onRequestStart's dotenv lines with the ones
above (drop syncWithRequest()), add the TestBox lines,
and restart the application once. An instance already in the
application scope keeps the old version's code until the application restarts.
box install appdotenv.Application.cfc lines above, and the ones in tests/Application.cfc.getSystemSetting(,
getSystemProperty( and getEnv( in
config/ with
application.dotenv.getSystemSetting( and so on. Inject
DotEnv@appdotenv wherever code called them at runtime.
Add detectEnvironment() if you set
ENVIRONMENT in the file..gitignore: .env, .env.*, !.env.example
.env.example with every key and only
harmless values..deployignore): /.*
web.config: add .env to
<hiddenSegments> under requestFiltering..env.production at the repo root. The
.env.* pattern already ignores it. This module never
reads that name; it's only the copy you edit and deploy from.<webroot>\..\.env. Remove inherited permissions
and grant Read only to the CFML engine's service account and Administrators.?fwreinit=<your
reinit password>.The file is outside the webroot and IIS has no permission to read it,
so it can't be served. But every site in a shared engine runs as the
same service account, so CFML code in another site on that
server could still fileRead() it. Use a separate engine
instance or a sandbox when that matters.
Adobe ColdFusion 2023 and 2025, BoxLang 1 (CFML compatibility), Lucee 6. Tested on ColdBox 8.
Apache 2.0.
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Fix release.json to point to the master branch
2.0 makes every value match what ColdBox's getSystemSetting() returned in development. See
"Upgrading to 2.0" in the README, check your .env files, and restart the application once
after upgrading: an instance already in the application scope keeps the old code until then.
java.util.Properties. Quotes are kept, #
starts a comment only at the start of a line, backslashes are escapes, and export has no
meaning. The file is still read as UTF-8."", as ColdBox does. It no
longer falls through to JVM and OS values or the default.preReinit, which fires only for a reinit that
passed the password check and honours a custom reinitKey. The README's onRequestStart
code no longer calls syncWithRequest(); it reloads in place when ColdBox isn't running, so
a failed boot recovers without a restart.init() no longer reads the disk. getPath() is blank until the first load().tests/Application.cfc change TestBox needs.getSystemSetting(), getSystemProperty() and getEnv(), with the same arguments as
ColdBox's methods of those names, each checking the file first. get() is now an alias of
getSystemSetting()..env files, as CommandBox reads them.DotEnv built with appRoot now looks for the file on every load(), so ?fwreinit
follows a .env moved between the webroot and the folder above it. It used to keep the path
chosen at startup, and a moved file broke every request until the app restarted.syncWithRequest(). It still reloads on any fwreinit, even with the wrong password.
Remove it from onRequestStart.SystemSettingNotFound names the key in message and the file path only in
extendedInfo. ColdBox's default public error page doesn't show extendedInfo; its
BugReport.cfm and Whoops.cfm templates do.DotEnv parses an app's own .env into
application.dotenv before ColdBox boots, so config CFCs can read it without the JVM or the
OS environment. It reads the webroot .env in development and the one above the webroot in
production.syncWithRequest() reloads the file on a ColdBox reinit, including a reinit sent as a POST
field.DotEnv@appdotenv and logs where it was loaded from,
key names and counts only.
$
box install appdotenv