Skip to content

Environment reference

Everything Manager for Craft reads from the environment. Anything marked required in production will stop the container starting if it is missing or obviously wrong - the specification asks a self-hosted deployment to refuse insecure defaults, and the only reliable moment to do that is before the first request.

Application

VariableDefaultNotes
APP_KEY-Required. Encrypts TOTP secrets. Back it up separately from the database, or a restore yields unreadable second factors.
APP_URL-Required. The address browsers and connectors actually use. Cookie security and generated links depend on it.
APP_ENVproduction
APP_DEBUGfalseThe container refuses to start with this on in production.

Database

VariableDefaultNotes
DB_CONNECTIONpgsqlPostgreSQL only. The audit log needs a trigger and privileges that are not portable.
DB_HOSTpostgres
DB_PORT5432
DB_DATABASEmanager
DB_USERNAMEmanagerSee the least-privilege role in install.md.
DB_PASSWORD-Required. The container refuses to start in production on an empty or well-known value.

Redis

VariableDefaultNotes
REDIS_HOSTredis
REDIS_PORT6379
REDIS_PASSWORDnull
REDIS_CLIENTpredisphpredis also works; the image has both.
CACHE_STOREredis
QUEUE_CONNECTIONredis
MANAGER_NONCE_STOREredisMust be shared and atomic. array or file would let a replay through on a second worker; manager:doctor fails on either.

Redis backs replay protection, which fails closed: while it is unreachable, connector requests are rejected with a 503 rather than accepted.

Identifying the installation

VariableDefaultNotes
MANAGER_VERSIONunsetWhat Settings reports this installation as. The Docker image sets it from its build argument. A tarball or a clone has no way to know its own version - git archive leaves no .git behind - so it is normal for this to be unset, and the screen says "unreleased build" rather than guessing. Set it to the release you deployed if you want it on screen.

Sessions

VariableDefaultNotes
SESSION_DRIVERdatabaseNeeded to list and revoke individual sessions. A file or cache driver cannot answer "where am I signed in".
SESSION_LIFETIME120Minutes.
SESSION_SECURE_COOKIEautoDefaults to on whenever APP_URL is HTTPS, rather than off with a variable to remember.

Pairing and connectors

VariableDefaultNotes
MANAGER_ENROLMENT_TTL900Seconds an enrolment code stays valid. It is a bearer secret until consumed, so keep it short.
MANAGER_ENROLMENT_MAX_ATTEMPTS_IP10Enrolment attempts per source network before they are refused. A code is guessable in principle, so the rate limit is what makes guessing pointless in practice.
MANAGER_ENROLMENT_MAX_ATTEMPTS_SITE5The same, per site. Lower, because a site being paired repeatedly is stranger than an agency pairing several sites from one office.
MANAGER_ENROLMENT_DECAY900Seconds those two counters remember an attempt for.
MANAGER_NUDGE_ENABLEDtrueWhether Manager may ask a site to check in early, so requested work starts in seconds rather than at the next scheduled check-in. The request carries no instruction - a site that receives one makes the ordinary signed claim it would have made anyway - and the address is composed from the site's own expected domain rather than taken from the wire. Set it to false if this installation must make no outbound request to a managed site. Nothing depends on it: with it off, or against a connector too old to say where to knock, work starts on the site's own schedule exactly as before. Deliberately absent from .env.example, because a blank value reads as false and a copied-but-unfilled line would turn it off without anybody meaning to.
MANAGER_PROBE_ON_REFRESHtrueWhether pressing Refresh on a site also checks what that site serves to the public, rather than leaving it to the daily sweep. Same request the sweep makes, from the same place, to the site's own expected domain — the difference is only that somebody asked for it. A site is not looked at more than once every ten minutes however often the button is pressed, and Refresh all never does it. Set it to false if outbound requests to a managed site should happen only on a schedule — an operator whose own monitoring alerts on that pattern has a real reason to. The daily sweep and php artisan manager:web:check are unaffected. Deliberately absent from .env.example, for the reason MANAGER_NUDGE_ENABLED is.
MANAGER_TIMESTAMP_TOLERANCE120Seconds of accepted clock skew. Widening this widens the replay window the nonce store must remember; the two move together.
MANAGER_MAX_PAYLOAD_BYTES262144Enforced before parsing.
MANAGER_RATE_LIMIT_SITE60Requests per minute per site.
MANAGER_RATE_LIMIT_IP120Requests per minute per source network.
MANAGER_RATE_LIMIT_INGEST_SITE600Requests per minute per site, for artifact bytes only, counted separately from the row above. A backup arriving in parts is hundreds of requests in a few minutes and would otherwise be refused for doing what the platform asked; a separate budget also means an upload in progress cannot exhaust the one heartbeats and job claims depend on.
MANAGER_RATE_LIMIT_INGEST_IP1200The same, per source network. Every site behind one office NAT shares it.
MANAGER_SIGNING_PUBLIC_KEY-Generated by manager:keys:generate.
MANAGER_SIGNING_SECRET_KEY-As above. Losing it means re-pairing every site.

Accounts

VariableDefaultNotes
MANAGER_RECENT_AUTH_MINUTES15How long a password confirmation counts as recent for sensitive actions. Laravel's default is three hours, which suits a shopping site rather than a control plane.
MANAGER_MAX_LOGIN_ATTEMPTS5
MANAGER_LOGIN_DECAY900Seconds.

There is no variable to enable public registration. Accounts are created by an owner or through the one-time setup flow.

Network

VariableDefaultNotes
MANAGER_HTTP_PORT8080Host port the app binds on, on localhost only. Change it if something already holds 8080. The container port stays 8080, which is what the reverse-proxy examples name.
MANAGER_TRUSTED_PROXIESemptyComma-separated addresses or CIDR ranges. Never * - that lets any caller forge its apparent source address, defeating per-network rate limits and the addresses in the audit log. manager:doctor fails on it.
MANAGER_HSTS_SECONDS31536000max-age on the HSTS header, in seconds. One year. A blank value falls back to the default rather than to zero, because an empty variable is somebody who has not decided rather than somebody asking browsers to stop pinning HTTPS.

Health and telemetry

VariableDefaultNotes
MANAGER_HEARTBEAT_INTERVAL300Seconds a connector is expected to check in within. The screens describe the wait as a window rather than a countdown, because a site with no cron reports off ordinary web traffic instead.
MANAGER_HEARTBEAT_GRACE3How many intervals may pass before a site is treated as silent. Three, so one missed run is not an incident.
MANAGER_TELEMETRY_RETENTION_DAYS90Days of heartbeats, runtime reports and sign-in reports kept before manager:telemetry:prune removes them.
MANAGER_HEARTBEAT_RETENTION_DAYSunsetThe previous name for the row above, still honoured. It was the documented name before runtime and sign-in reports existed, and silently ignoring a setting an operator had already made would be the wrong way to rename one. Set either; the newer name wins.

Updates

VariableDefaultNotes
MANAGER_FETCH_CHANGELOGStrueWhether Manager fetches plugin release notes so an update can be read before it is applied. Outbound traffic to package registries, on by default and switchable off for an installation that must make none.

Setup

VariableDefaultNotes
MANAGER_SETUP_TTL3600Seconds the one-time setup flow stays open on a fresh installation. It creates the first owner, so it closes on its own rather than waiting to be remembered.

Mail

Standard Laravel MAIL_* variables. These are the fallback and the floor: what a fresh installation starts with, what a container sets, and what stands unless somebody overrides them.

Set this up. Password resets and invitations are how anybody other than the first account gets in, and the default writes them to the log instead of sending them. The failure mode is silence: somebody invites a colleague, nothing arrives, and nothing reports a problem. manager:doctor warns while this is unconfigured, and manager:user:password exists as the way in when it is.

Overriding them from the interface

An owner of a self-hosted installation can configure a relay under Settings → Mail without a shell. It is owner-only, and it is absent on a hosted installation, where the relay belongs to whoever runs the service rather than to the reader.

  • The override lives in the mail_settings table, one row, with the credential encrypted under APP_KEY - the same way a TOTP secret and a webhook signing secret are held. It is write-only: never rendered back into the form, and never displayed again once saved.
  • It is applied at send time, into the running config, rather than at boot. config:cache is therefore irrelevant to it, and nothing writes to .env.
  • "Use the environment configuration" on that screen discards it completely, and these variables take effect again immediately. That is the way out if a change stops mail working - which matters here more than anywhere else, because the usual way you would be told is email.
  • Rotating APP_KEY without re-entering the credential breaks the stored relay login, exactly as it breaks TOTP secrets and webhook signing secrets.
  • postmark and resend need a package that is not required by default (symfony/postmark-mailer and resend/resend-php). The screen says so and refuses to select one that is missing; setting MAIL_MAILER to either without installing it fails at send time instead.
VariableDefaultNotes
MAIL_MAILERlogThe transport. smtp, postmark, resend, ses, sendmail, or log to write messages to storage/logs instead of sending them.
MAIL_FROM_ADDRESSemptyThe envelope sender. Most relays reject a message without one, and most spam filters distrust one that does not match the sending domain.
MAIL_FROM_NAME${APP_NAME}What recipients see in the From line.

SMTP

Any relay - Fastmail, Mailgun, Postmark's SMTP interface, your own Postfix. Set MAIL_MAILER=smtp and:

VariableDefaultNotes
MAIL_HOST127.0.0.1The relay's hostname.
MAIL_PORT2525Usually 587 for submission with STARTTLS, or 465 for implicit TLS.
MAIL_USERNAMEempty
MAIL_PASSWORDempty
MAIL_SCHEMEemptysmtps forces implicit TLS. Leave unset for STARTTLS on 587.
MAIL_EHLO_DOMAINapp hostWhat this server calls itself when greeting the relay. Some relays check it against your SPF record.

API transports

No host or port; each takes one key, set in config/services.php's block for it.

TransportSetAlso needs
postmarkMAIL_MAILER=postmark, POSTMARK_API_KEYcomposer require symfony/postmark-mailer
resendMAIL_MAILER=resend, RESEND_API_KEYcomposer require resend/resend-php
sesMAIL_MAILER=ses, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGIONnothing - aws/aws-sdk-php is already required

Proving it works

bash
php artisan manager:mail-test [email protected]

Sends immediately rather than queueing - a queued test reports success as soon as the job is accepted, which is the one thing already known - and prints the transport's error in full if it fails. That is usually the useful part: 535 authentication failed and certificate verify failed want different fixes.

An owner can do the same from Settings → Mail, which sends to their own address and nowhere else. That path reports only the exception's class name, because it renders into a web page and a mail exception can carry the credentials it was using. Run the command when you need the detail. The command reads whatever is actually in force, so it exercises a relay configured on that screen rather than the variables above.

Either way, "sent without error" means the relay accepted the message, not that it arrived. Getting a message into an inbox is a DNS problem as much as a relay one - Settings → Mail carries a deliverability checklist covering SPF, DKIM, DMARC, reverse DNS and From-address alignment.

Site backups

Off until a keypair exists. See backup.md for what a backup contains and why this is not end-to-end encryption.

VariableDefaultNotes
MANAGER_BACKUP_PUBLIC_KEYemptyLegacy. Backups are now encrypted to keys the organisation holds, not to this platform - see Recovery keys. This keypair exists only to read artifacts taken before that change, and is not used for new ones. A fresh installation does not need it.
MANAGER_BACKUP_SECRET_KEYemptyLegacy. The other half. Whoever holds it can read backups taken before recovery keys existed, which is exactly why that arrangement was replaced. If you have no legacy artifacts, do not set it.
MANAGER_BACKUP_DISKbackupsWhich filesystem disk artifacts are written to.
MANAGER_BACKUP_DRIVERlocallocal or s3. The local default works but is a poor place for the only copy of a customer's database.
MANAGER_BACKUP_S3_BUCKETemptyThe bucket artifacts are written to when the driver is s3. Scope the credentials below to this bucket alone: a key with access to the backup store has access to every managed site's database.
MANAGER_BACKUP_S3_KEYemptyAccess key id.
MANAGER_BACKUP_S3_SECRETemptySecret access key.
MANAGER_BACKUP_S3_REGIONunsetNo default. AWS needs one; most S3-compatible services ignore it.
MANAGER_BACKUP_S3_ENDPOINTunsetSet it for a non-AWS S3-compatible service; see below.
MANAGER_BACKUP_S3_PATH_STYLEfalsePath-style addressing, which most non-AWS services need.
MANAGER_BACKUP_MAX_BYTESunsetLargest artifact accepted. Unset means no ceiling, which is the default: a limit nobody chose is an accident, not a policy. Set it in bytes to impose one, and the refusal names both the artifact's size and this variable. A policy statement rather than a buffer size - nothing is held in memory. With no ceiling here, your reverse proxy's body limit and PHP's post_max_size become the real one; manager:doctor reports what PHP allows, and cannot see the proxy.
MANAGER_BACKUP_UPLOAD_WINDOW21600Seconds a declared artifact may wait for its bytes before being written off. Six hours, because that is what an artifact of the size now permitted takes on a real uplink.
MANAGER_BACKUP_PART_BYTES268435456Bytes per part when an artifact goes straight to an object store and is too large for a single request. Sized by what the store will accept, not by anything on this machine. Lower it in tests so a small artifact exercises the multipart path.
MANAGER_BACKUP_INGEST_PART_BYTES8388608Bytes per part when an artifact arrives through this application. A different question from the row above and a different answer: this one is sized so that a single request finishes well inside a PHP-FPM request_terminate_timeout or an nginx fastcgi_read_timeout on a slow uplink, because neither of those can be seen or raised from here. Raising it makes each request longer and is the direction that breaks; the value is clamped to between 64 KB and 256 MB. Lower it in tests.
MANAGER_BACKUP_QUOTA_BYTESunsetTotal bytes one organisation may hold across every site. Unset means no limit - an operator who has not asked for one should not discover one. Worth setting where several sites share a volume: one site filling it takes backups down for all of them.

Object storage

Standard AWS_* variables, including AWS_ENDPOINT for any S3-compatible service, for anything other than backups. Backups have their own credentials above, deliberately.

Optional diagnostics

VariableDefaultNotes
MANAGER_DIAGNOSTICS_ENABLEDfalseOff by default and never mandatory. Carries no site content and no secrets, is visible in settings, and can be turned off again at any time.
MANAGER_DIAGNOSTICS_ENDPOINTunsetWhere diagnostics are sent when the row above is on. Unset means nowhere, which is why turning the switch on alone sends nothing.

AGPL-3.0-or-later. An independent product for Craft CMS, not affiliated with or endorsed by Pixel & Tonic.