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
| Variable | Default | Notes |
|---|---|---|
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_ENV | production | |
APP_DEBUG | false | The container refuses to start with this on in production. |
Database
| Variable | Default | Notes |
|---|---|---|
DB_CONNECTION | pgsql | PostgreSQL only. The audit log needs a trigger and privileges that are not portable. |
DB_HOST | postgres | |
DB_PORT | 5432 | |
DB_DATABASE | manager | |
DB_USERNAME | manager | See 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
| Variable | Default | Notes |
|---|---|---|
REDIS_HOST | redis | |
REDIS_PORT | 6379 | |
REDIS_PASSWORD | null | |
REDIS_CLIENT | predis | phpredis also works; the image has both. |
CACHE_STORE | redis | |
QUEUE_CONNECTION | redis | |
MANAGER_NONCE_STORE | redis | Must 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
| Variable | Default | Notes |
|---|---|---|
MANAGER_VERSION | unset | What 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
| Variable | Default | Notes |
|---|---|---|
SESSION_DRIVER | database | Needed to list and revoke individual sessions. A file or cache driver cannot answer "where am I signed in". |
SESSION_LIFETIME | 120 | Minutes. |
SESSION_SECURE_COOKIE | auto | Defaults to on whenever APP_URL is HTTPS, rather than off with a variable to remember. |
Pairing and connectors
| Variable | Default | Notes |
|---|---|---|
MANAGER_ENROLMENT_TTL | 900 | Seconds an enrolment code stays valid. It is a bearer secret until consumed, so keep it short. |
MANAGER_ENROLMENT_MAX_ATTEMPTS_IP | 10 | Enrolment 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_SITE | 5 | The same, per site. Lower, because a site being paired repeatedly is stranger than an agency pairing several sites from one office. |
MANAGER_ENROLMENT_DECAY | 900 | Seconds those two counters remember an attempt for. |
MANAGER_NUDGE_ENABLED | true | Whether 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_REFRESH | true | Whether 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_TOLERANCE | 120 | Seconds of accepted clock skew. Widening this widens the replay window the nonce store must remember; the two move together. |
MANAGER_MAX_PAYLOAD_BYTES | 262144 | Enforced before parsing. |
MANAGER_RATE_LIMIT_SITE | 60 | Requests per minute per site. |
MANAGER_RATE_LIMIT_IP | 120 | Requests per minute per source network. |
MANAGER_RATE_LIMIT_INGEST_SITE | 600 | Requests 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_IP | 1200 | The 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
| Variable | Default | Notes |
|---|---|---|
MANAGER_RECENT_AUTH_MINUTES | 15 | How 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_ATTEMPTS | 5 | |
MANAGER_LOGIN_DECAY | 900 | Seconds. |
There is no variable to enable public registration. Accounts are created by an owner or through the one-time setup flow.
Network
| Variable | Default | Notes |
|---|---|---|
MANAGER_HTTP_PORT | 8080 | Host 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_PROXIES | empty | Comma-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_SECONDS | 31536000 | max-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
| Variable | Default | Notes |
|---|---|---|
MANAGER_HEARTBEAT_INTERVAL | 300 | Seconds 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_GRACE | 3 | How many intervals may pass before a site is treated as silent. Three, so one missed run is not an incident. |
MANAGER_TELEMETRY_RETENTION_DAYS | 90 | Days of heartbeats, runtime reports and sign-in reports kept before manager:telemetry:prune removes them. |
MANAGER_HEARTBEAT_RETENTION_DAYS | unset | The 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
| Variable | Default | Notes |
|---|---|---|
MANAGER_FETCH_CHANGELOGS | true | Whether 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
| Variable | Default | Notes |
|---|---|---|
MANAGER_SETUP_TTL | 3600 | Seconds 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_settingstable, one row, with the credential encrypted underAPP_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:cacheis 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_KEYwithout re-entering the credential breaks the stored relay login, exactly as it breaks TOTP secrets and webhook signing secrets. postmarkandresendneed a package that is not required by default (symfony/postmark-mailerandresend/resend-php). The screen says so and refuses to select one that is missing; settingMAIL_MAILERto either without installing it fails at send time instead.
| Variable | Default | Notes |
|---|---|---|
MAIL_MAILER | log | The transport. smtp, postmark, resend, ses, sendmail, or log to write messages to storage/logs instead of sending them. |
MAIL_FROM_ADDRESS | empty | The 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:
| Variable | Default | Notes |
|---|---|---|
MAIL_HOST | 127.0.0.1 | The relay's hostname. |
MAIL_PORT | 2525 | Usually 587 for submission with STARTTLS, or 465 for implicit TLS. |
MAIL_USERNAME | empty | |
MAIL_PASSWORD | empty | |
MAIL_SCHEME | empty | smtps forces implicit TLS. Leave unset for STARTTLS on 587. |
MAIL_EHLO_DOMAIN | app host | What 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.
| Transport | Set | Also needs |
|---|---|---|
postmark | MAIL_MAILER=postmark, POSTMARK_API_KEY | composer require symfony/postmark-mailer |
resend | MAIL_MAILER=resend, RESEND_API_KEY | composer require resend/resend-php |
ses | MAIL_MAILER=ses, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION | nothing - aws/aws-sdk-php is already required |
Proving it works
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.
| Variable | Default | Notes |
|---|---|---|
MANAGER_BACKUP_PUBLIC_KEY | empty | Legacy. 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_KEY | empty | Legacy. 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_DISK | backups | Which filesystem disk artifacts are written to. |
MANAGER_BACKUP_DRIVER | local | local or s3. The local default works but is a poor place for the only copy of a customer's database. |
MANAGER_BACKUP_S3_BUCKET | empty | The 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_KEY | empty | Access key id. |
MANAGER_BACKUP_S3_SECRET | empty | Secret access key. |
MANAGER_BACKUP_S3_REGION | unset | No default. AWS needs one; most S3-compatible services ignore it. |
MANAGER_BACKUP_S3_ENDPOINT | unset | Set it for a non-AWS S3-compatible service; see below. |
MANAGER_BACKUP_S3_PATH_STYLE | false | Path-style addressing, which most non-AWS services need. |
MANAGER_BACKUP_MAX_BYTES | unset | Largest 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_WINDOW | 21600 | Seconds 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_BYTES | 268435456 | Bytes 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_BYTES | 8388608 | Bytes 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_BYTES | unset | Total 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
| Variable | Default | Notes |
|---|---|---|
MANAGER_DIAGNOSTICS_ENABLED | false | Off 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_ENDPOINT | unset | Where diagnostics are sent when the row above is on. Unset means nowhere, which is why turning the switch on alone sends nothing. |