Deployment
Production configuration
This page shows you what to set, what to override, and what to verify before a Wheels app takes real traffic. You'll configure config/environment.cfm and config/production/settings.cfm, wire secrets through .env and the process environment, and walk a pre-boot checklist that catches the failures most teams only notice at 3am.
You'll learn:
- How to flip an app into production mode and what that changes
- Which settings are production-critical and what to override
- How Wheels reads environment variables —
.env, then the process environment - A ten-point checklist to run before first production boot
Flip the environment switch
Section titled “Flip the environment switch”config/environment.cfm selects the active environment. The scaffold ships with development hardcoded:
<cfscript> set(environment="development");</cfscript>In production, read WHEELS_ENV from the process instead so the same artifact can boot dev, staging, and prod:
<cfscript> set(environment=env("WHEELS_ENV", "production"));</cfscript>Wheels boots, calls config/environment.cfm, and then loads config/settings.cfm followed by config/<environment>/settings.cfm. Anything you set() in the per-environment file wins.
What switching to production changes, automatically:
showErrorInformation→false— error pages stop leaking stack traces.sendEmailOnError→true— seterrorEmailToAddressor you'll get nothing.showDebugInformation→false— the floating debug panel is hidden.cacheActions,cacheImages,cachePages,cachePartials,cacheQueries→ alltrue.redirectAfterReload→true— the reload URL 302s after success, strippingreloadandpasswordfrom the query string. Defaults tofalseand flips totruefor bothproductionandmaintenance.- URL-based environment switching is disabled unless you explicitly re-enable it (disabled by default in
production,testing, andmaintenance). - Migrations refuse to run
down(allowed only in development).
You do not set any of those manually. Setting environment to production is the switch.
Configure config/production/settings.cfm
Section titled “Configure config/production/settings.cfm”Create config/production/settings.cfm. This file is read only when the active environment is production:
<cfscript> // Real datasource — never H2 or SQLite in production. set(dataSourceName=env("WHEELS_DATASOURCE", "myapp_prod")); set(dataSourceUserName=env("WHEELS_DB_USER", "")); set(dataSourcePassword=env("WHEELS_DB_PASSWORD", ""));
// Rotate this per-deploy. An empty value disables ?reload= entirely — // see the pre-boot checklist, item 2. set(reloadPassword=env("WHEELS_RELOAD_PASSWORD", ""));
// Persistent CSRF encryption key — required when csrfStore="cookie". set(csrfCookieEncryptionSecretKey=env("CSRF_KEY", ""));
// Error notifications. set(errorEmailToAddress=env("ERROR_EMAIL", "ops@example.com")); set(errorEmailFromAddress="noreply@example.com");
// URL rewriting: "On" when your web server rewrites /foo → /index.cfm/foo. set(URLRewriting="On");</cfscript>Nothing in that block is invented. Each key is defined in vendor/wheels/events/init/:
| Setting | Default |
| --- | --- |
| dataSourceName | app folder name (lowercased) |
| dataSourceUserName, dataSourcePassword | "" |
| reloadPassword | "" |
| csrfCookieEncryptionSecretKey | "" |
| errorEmailToAddress, errorEmailFromAddress | "" |
| URLRewriting | detected from CGI |
| subpath | "" |
How Wheels reads environment variables
Section titled “How Wheels reads environment variables”env("NAME", "default") is the canonical accessor. It checks application.env first, then server.system.environment, then returns the default.
application.env is populated by the scaffold's Application.cfc at onApplicationStart():
- Reads
.envat the app root. - Resolves
WHEELS_ENVfrom.envfirst, thenjava.lang.System.getenv("WHEELS_ENV"). - Reads
.env.<environment>on top if present — so.env.productionoverrides.env. - Interpolates
${VAR}references between entries. - Copies the merged struct to
application.envinsideonApplicationStart().
The practical rule: commit nothing secret. Ship .env.example with empty values, inject real values at deploy time. A deploy script, systemd EnvironmentFile=, Dockerfile ENV, or Kubernetes secret-mount all work.
WHEELS_ENV=productionWHEELS_DATASOURCE=myapp_prodWHEELS_DB_USER=myappWHEELS_DB_PASSWORD=xxxxxxxxxxWHEELS_RELOAD_PASSWORD=rotate-me-per-deployCSRF_KEY=base64-aes-key-from-secrets-managerERROR_EMAIL=ops@example.comSwitching between dev, staging, and prod
Section titled “Switching between dev, staging, and prod”Three mechanisms, in order from safest to most dangerous:
Per-environment artifacts (recommended)
Section titled “Per-environment artifacts (recommended)”Each deploy target exports WHEELS_ENV before the CFML server boots. config/environment.cfm reads it, the right per-env settings file loads, and that's it. Restart required to change environments.
URL-based reload (development only)
Section titled “URL-based reload (development only)”?reload=true&password=<reloadPassword> re-runs onApplicationStart. ?reload=production&password=<reloadPassword> switches the running app into production. Switching to the environment that's already active is a no-op.
Wheels disables URL-based switching automatically in production, testing, and maintenance environments unless you explicitly set(allowEnvironmentSwitchViaUrl=true) in your per-env settings. Leave it disabled. A cached reload password plus URL switching plus production is a bad combination.
The reload password is compared in constant time, rate-limited (5 failures per client IP per 5 minutes), and every attempt is logged to wheels_security.log — the full threat model and hardening guidance live in Security Hardening — Reload password. Rotate it on every production deploy anyway — it's trivial to rotate and the value ends up in web-server logs if someone misconfigures TLS termination.
The maintenance environment
Section titled “The maintenance environment”set(environment="maintenance") puts Wheels into planned-downtime mode — the framework responds with HTTP 503 and renders app/events/onmaintenance.cfm instead of running the normal request lifecycle, unless the client IP is listed in ipExceptions. Use it for long migrations or config rollouts.
Pre-boot checklist
Section titled “Pre-boot checklist”Run through this before the first production request. Each item maps to a framework check — if you skip it, Wheels won't always fail loudly.
environmentisproduction. Verify with?controller=wheels&action=info(dev only) or by checkingapplication.wheels.environmentin a smoke-test action. If it saysdevelopment,showErrorInformationis stilltrueand caching is off.reloadPasswordis set and rotated. An empty password disables?reload=entirely — both URL-based environment switching and plain?reload=truerequire a non-empty configured password plus a matchingpasswordparameter (the gate fails closed, #3062). Wheels writes awheels_securitywarning log on boot if it's blank. Set it to a random value unique to this deploy so operators can still reload over HTTP; leave it blank only if you never want URL reload to work. Depth: Security Hardening — Reload password.showErrorInformationisfalse. Theproductionswitch flips this, but a strayset(showErrorInformation=true)inconfig/settings.cfmwill leak stack traces. Grep for it.dataSourceNamepoints at a real database. Not H2, notwheelstestdb, not a shared staging instance. Confirm with a read-only action that callsmodel("User").count()or similar.csrfCookieEncryptionSecretKeyis set ifcsrfStore="cookie".csrfStoredefaults to"session"unconditionally — cookie storage is always an explicit opt-in. WithcsrfStore="cookie"and no key, the first CSRF check throwsWheels.Security.MissingCsrfKey.- Session storage matches the node topology.
sessionflash storage (the default when session management is on) requires sticky sessions or a session-replication setup behind the load balancer. With multiple nodes and no stickiness, useflashStorage="cookie"explicitly. - Error email is wired.
sendEmailOnErroris auto-enabled in production. IferrorEmailToAddressis empty, Wheels still tries to send — the address is read at error time — and the mail fails silently. Set it. - URL rewriting matches the web server.
set(URLRewriting="On")only works if nginx/Apache/Tuckey rewrites/posts/1to/index.cfm/posts/1. If rewrites aren't configured, leave it at"Partial"or"Off", or every link breaks. allowEnvironmentSwitchViaUrlis not re-enabled. Confirm by greppingconfig/production/settings.cfm— don't set it totrue. If you need to toggle environments, redeploy.- Caches are warmed before traffic cutover. The very first request to a freshly started node is dominated by one-time CFML-to-bytecode compilation — profiling a stock app puts roughly 85% of a ~1.2s cold start in the engine's compiler, versus sub-millisecond once warm. That cost is paid once per process, so the goal is to move it off the first real visitor and onto the deploy sequence:
- Apps scaffolded with
wheels newship a/uproute (app/controllers/Up.cfc).wheels deploy's proxy healthcheck probes/upbefore flipping traffic, so the dispatch → controller → render path is compiled on the new node before it serves anyone. Touch your hottest models in that action (e.g.model("Post").count()) to warm their ORM column metadata too — the first query against a model is the other notable one-time cost. - Not using
wheels deploy? Curl your latency-sensitive endpoints from the deploy script before enabling the node at the load balancer. A single warm-up request per controller is usually enough, sincecacheControllerConfig,cacheModelConfig,cacheDatabaseSchema, andcacheFileCheckingare always-on in every environment.
- Apps scaffolded with
- Template inspection is set to
never(engine-level). In development the CFML engine re-checks each template's source timestamp on every request so edits are picked up live; in production that filesystem stat is pure per-request overhead. Set the engine's template-inspection mode toneverso it trusts the compiled-class cache and skips the check — Lucee: admin → Performance/Caching → "Inspect Templates" =Never(orinspectTemplate="never"in.CFConfig.json); Adobe ColdFusion: enable "Trusted Cache". This is engine configuration, not a Wheels setting, so it isn't flipped byenvironment="production". Pair it with a warm-up (item 10) and a cache-clear/reload step in your deploy so a new release's templates still recompile once.
What production does not change
Section titled “What production does not change”A few things you might expect to flip, that don't:
- Routes are not cached.
config/routes.cfmis re-evaluated on every reload. There's no "compiled routes" mode — route dispatch is cheap enough that it doesn't matter. - Migrations are not auto-run.
autoMigrateDatabaseisfalsein every environment. Run migrations explicitly from your deploy flow —wheels migrate latest, or one of the no-CLI paths — or opt in withset(autoMigrateDatabase=true). .envfiles are not required. If you don't ship one,application.envstays empty andenv()falls through toserver.system.environment. That's fine — it's why systemdEnvironmentFile=and DockerENVboth work out of the box.