Skip to main content

Install MailGen on a new machine

Install MailGen on a new macOS or Debian machine, then sign in and open a connected mailbox. This guide uses unified Docker Compose. BFF serves the browser workspace. Account handles login. Backend workers and .NET services process mailbox data.

The beginner route​

The sequence below configures a full-local installation with:

ChoiceThis route
InfrastructureECS_DEPLOYMENT_MODE=fulllocal, ECS_REMOTE_SIMULATION=false
Application loginMicrosoft through Account
MailboxesMicrosoft plus personal Gmail
Gmail updatesHistory polling: OAuth/sync enabled, watch disabled
ExecutionCompiled production through 0daysetup.sh
Extra applicationsFormGen and docs-mcp, selected by setup
Application memoryECS_MEMORY_GIB=44, plus host/build reserves
Source/configurationThis checkout (.), new root .env, fresh empty stores

Before selecting Microsoft, check Connect and provision one mailbox. Microsoft login and mailbox connection require an existing persisted tenant connection with granted consent and usable credentials. Environment credentials alone do not expose these actions in an empty database. This guide does not provide a complete Microsoft bootstrap for a blank database or a first-connection creation command. Proceed with the Microsoft route only when that connection already exists. The onboarding worker creates the mailbox later, after identity linking and an authorized start request.

Follow the numbered sequence with these choices. Complete the linked Microsoft and Gmail prerequisites before startup. Google application login is unnecessary for this route because users sign in with Microsoft.

Archive migration, Google-only, real remote infrastructure, Gmail push, and production source pins are alternatives. They are described in the relevant sections, not required intermediate steps. For retained data, use configuration restore and preserve original identities/keys instead of generating replacements.

Choose the same operating-system tab throughout the page. Copy commands into Terminal, press Enter, and wait for the prompt before continuing. Run later project commands from the t42-motherrepo root.

For an existing installation, use daily commands. Do not repeat a new-machine setup just to change infrastructure placement.

Understand the components​

Docker runs programs in isolated containers. Persistent volumes store their data. Restarting a program and deleting a volume have different effects.

ComponentPurposeReal partialRemote location
BFFBrowser UI and server-side browser requestsLocal
AccountApplication login and sessionsLocal
Backend and .NET servicesProcessing, synchronization, searchLocal
PostgreSQLUsers, settings, mailbox records, historyLocal
SeaweedFS/S3Bodies, attachments, other filesLocal
OpenSearchSearch indexesLocal
DragonflyCache and temporary coordinationLocal
Kafka, three brokersCommands and events between servicesRemote Coolify
ClamAVUploaded-file scanningRemote Coolify
CollaboraOffice document previewsRemote Coolify
Tika / knowledge-extractorDocument text extractionRemote Coolify
QdrantKnowledge-retrieval vectorsRemote Coolify

Coolify manages remote resources. Remote deployment here does not move the whole application. In fulllocal, the infrastructure in the last five rows also runs locally. See architecture overview.

0. Read setup without the full application​

With the repository downloaded and Docker running:

bash docsStarter.sh

Run it from the motherrepo or an independent Architecture checkout. It needs no root .env or application setup. Open the printed localhost URL. Ctrl+C removes its own disposable documentation container. Read standalone documentation for its static-build behavior.

1. Obtain access and preserve data​

Before installation, obtain:

A Coolify dashboard password does not replace service credentials. A .local hostname cannot receive cloud-provider webhooks.

For retained data, also obtain database and object-store backups plus the existing encryption/signing/grant keys. Keep the deployment's instance and tenant identities. A new machine and configuration do not restore old mailbox data.

Onboarding creates the initial local mailbox data, file objects, search data, and synchronization state. The provider mailbox remains at Gmail or Microsoft.

Machine capacity​

Selected workloadExplicit application budget
Standard full stack without Google worker profileECS_MEMORY_GIB=40
Full Microsoft + Google + FormGen stackECS_MEMORY_GIB=44

This guide follows the full 44 GiB selection. The application budget is not the whole machine's RAM. Runtime admission retains at least 3 GiB outside the requested application budget. Build admission also retains 3 GiB beyond the requested build/service memory. An initial 3 GiB build request therefore needs at least 6 GiB available. Production builds can need more.

The omitted budget defaults to 30 GiB. Neither 30 nor 35 GiB fits the full standard selection's current minimums. Do not lower service floors merely to pass admission. A smaller, deliberately selected workload requires its own allocator and host-capacity checks. Swap does not replace RAM.

On macOS, configure enough RAM in Docker Desktop → Settings → Resources for the budget and reserve. The smaller host/Docker VM capacity governs. On Debian, account for other host workloads.

Local simulation retains genuine local infrastructure and does not reduce capacity. Real remote mode changes the local selection, but requires measured local and remote capacity.

2. Install Terminal tools​

Open Terminal through Spotlight. Install Homebrew with its official command:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Run the installer's printed Next steps to make brew available. Then:

brew install git gh python openssl@3 unzip zip tmux
brew install --cask docker
open -a Docker

Wait for Docker Desktop to run. Configure sufficient resources, then apply its changes.

Check the tools:

git --version
python3 --version
docker version
docker compose version

Docker must show both Client and Server. A connection error means Docker is not yet available. Application services run inside containers, so this route does not require separate host Next.js or .NET installation.

Other Linux distributions can use the Docker route after replacing Debian package installation with their distribution's official instructions.

3. Download the application​

Use a GitHub account with access to all private repositories. gh auth login prints a one-time code and opens GitHub. Enter the code, sign in, approve GitHub CLI, and complete organization SSO if required. If the browser does not open, use the printed URL. Wait for Terminal to report success.

The submodule URLs use SSH. This new-machine route rewrites them to HTTPS so GitHub CLI can supply credentials without a separate SSH key. The global rewrite affects all GitHub repositories for this operating-system user. On a machine deliberately configured for SSH, agree on this change with its operator first.

Never put a personal access token or password in a command, clone URL, or shell history. GitHub CLI manages credentials. If it reports plaintext file storage, review credential protection with the operator. URL rewriting does not grant repository access.

gh auth login --hostname github.com --git-protocol https --web
gh auth setup-git --hostname github.com
git config --global url."https://github.com/".insteadOf 'git@github.com:'
mkdir -p "$HOME/dev"
cd "$HOME/dev"
git clone --recurse-submodules https://github.com/t42-hu/t42-motherrepo.git
cd t42-motherrepo

Run all remaining project commands from t42-motherrepo. In a new terminal, return with cd "$HOME/dev/t42-motherrepo". If a file is missing, check this directory before retrying.

4. Prepare the root configuration​

For this new-empty-installation route, create the root file from the template:

cp .env.example .env
chmod 600 .env

Do not run this copy over a working configuration. Open .env in a plain-text editor. It is a hidden text file, not a shell script. Do not append .txt or execute it with source.

Supply the mandatory blank values yourself: bootstrap administrator credentials with a 32–64-character password, unique ECS_INSTANCE_ID, PostgreSQL/S3/SeaweedFS credentials, Kafka identity/passwords, internal proxy/KeyStore tokens, Dragonfly/viewer passwords, Cloudflare management credentials, and active provider settings.

Setup calculates browser URLs, PostgreSQL connection strings, and default proxy paths. It preserves supplied auth/proxy/FormGen secrets or generates missing ones. It does not generate KAFKA_CLUSTER_ID, provider credentials, the explicit Graph delta key, or the Google grant/client-secret files. Complete those steps below before setup. Set ECS_MEMORY_GIB=44 for this guide's full selection. See the complete environment tables.

devbox is an example instance name. Replace it with your own lowercase name. Retain existing IDs and keys when moving data. Never run separate copies with the same instance identity.

Create the new empty Kafka cluster identity​

Only for a new cluster with no retained broker data, generate a UUID in Kafka's URL-safe Base64 representation:

python3 -c 'import base64, uuid; print(base64.urlsafe_b64encode(uuid.uuid4().bytes).decode("ascii").rstrip("="))'

Copy the printed 22-character value into root KAFKA_CLUSTER_ID. All three new brokers use this same value. Python is already installed in step 2. This uses the 16-byte UUID encoding accepted by Kafka's Uuid implementation. Kafka's own distribution also supplies bin/kafka-storage.sh random-uuid.

For a retained cluster, reuse its original ID exactly. Do not generate a replacement, format existing storage, or change the ID to clear an ownership error. Stop and recover the correct configuration if it is unknown.

Choose infrastructure placement​

Keep the beginner route full-local:

ECS_DEPLOYMENT_MODE=fulllocal
ECS_REMOTE_SIMULATION=false

Alternative: prepared real remote infrastructure. This is outside the required beginner sequence:

ECS_DEPLOYMENT_MODE=partialRemote
ECS_REMOTE_SIMULATION=false

Missing selectors preserve full-local operation. partialRemote / true is local simulation. It is optional, uses real local values, and does not save memory. fulllocal / true is invalid.

Complete the remote block and callback contract before real remote startup. All three Kafka advertised endpoints must work. Native Kafka is not an HTTPS URL. Authenticated scanner/extractor gateways require separate tokens. Collabora requires a restricted callback to the local BFF.

The setup selects connections, prepares local applications/proxy, and checks prerequisites. It neither provisions nor restarts Coolify resources. It does not silently fall back to local services. It also does not transfer Kafka offsets/events or Qdrant points. See partial remote deployment.

Preserve keys and source selection​

Do not regenerate delta, mailbox-grant, email, or service keys for existing encrypted data. Editing a database password assignment does not change an existing database password.

ECS_APPLICATION_SOURCE_ROOT=. uses this checkout. An absolute reviewed snapshot can pin production application code. The path is machine-specific and does not back up the snapshot. Do not remove an existing production pin outside an agreed release.

5. Configure login and mailbox providers​

Microsoft​

Follow Microsoft Graph setup for app-only backend access and exact permissions. Follow account setup for the separate login app.

Root settingSource
MICROSOFT_CONNECTOR_ENABLEDtrue for Microsoft, explicit false for Google-only. Omission defaults to enabled.
BFF_MICROSOFT_CLIENT_ID, BFF_MICROSOFT_CLIENT_SECRET, BFF_MICROSOFT_TENANT_IDLogin Entra application
BACKEND_CLIENT_ID, BACKEND_CLIENT_SECRET, BACKEND_TENANT_IDSeparate app-only Graph application with admin consent
GRAPH_WEBHOOK_URL, GRAPH_LIFECYCLE_WEBHOOK_URLYour public ingress's supported notification/lifecycle paths
GRAPH_WEBHOOK_CLIENT_STATEStable private validation secret
DELTA_LINK_ENCRYPTION_KEYNew 32-byte Base64 key for this empty deployment. Unified runtime uses reference local-dev-v1. Retained data needs its original compatible key/reference contract.

Register the exact Account Web callback, for example https://account.devbox.ecas.local:8443/api/auth/callback/microsoft. Keep the actual host/port of an existing deployment instead of replacing it with an example.

For a new empty installation only, generate the delta key:

openssl rand -base64 32

Keep the key private. Current root Compose forwards the key, not DELTA_LINK_ENCRYPTION_KEY_REFERENCE. Its runtime reference is local-dev-v1. An arbitrary reference assignment in root .env has no effect. Retained state created with another reference needs a checked compatible process configuration or explicit migration. Runtime initialization prepares Graph certificate files through certgen.sh. Compose paths under /runtime/graph/... are container paths, not macOS/Windows paths. See Graph cryptography.

Personal Gmail​

For this route, complete Google Cloud setup for the mailbox client, API access, and private files. Omit its optional app-login client and Pub/Sub sections. Keep GOOGLE_APP_LOGIN_ENABLED=false, GOOGLE_MAILBOX_OAUTH_ENABLED=true, GOOGLE_MAILBOX_SYNC_ENABLED=true, and GOOGLE_MAILBOX_WATCH_ENABLED=false. Set the three Google JSON lists below to []. Microsoft remains enabled.

Complete the Google prerequisites before enabling the workers. It explains API enablement, consent, separate login/mailbox Web clients, secret files, and Pub/Sub. Personal Gmail does not require Workspace administration or domain-wide delegation.

Root settingRequired value
GOOGLE_APP_LOGIN_ENABLEDtrue only after the separate app-login client/public callback is ready
BFF_GOOGLE_LOGIN_CLIENT_ID, BFF_GOOGLE_LOGIN_CLIENT_SECRETComplete app-login client pair
GOOGLE_APP_LOGIN_CALLBACK_URL, GOOGLE_APP_LOGIN_PROXY_SECRETExact public HTTPS /api/auth/callback/google and separate internal secret
GOOGLE_MAILBOX_OAUTH_ENABLED, GOOGLE_MAILBOX_SYNC_ENABLEDBoth true for mailbox connection plus background sync
ECAS_DEPLOYMENT_TENANT_IDStable deployment UUID, not an email/domain/name
GOOGLE_MAILBOX_CLIENT_IDDedicated mailbox Web client ID
GOOGLE_MAILBOX_CALLBACK_URLRegistered public HTTPS /v1/oauth/callback
GOOGLE_MAILBOX_BFF_COMPLETION_URLBFF /api/google-mailbox/complete, not a Google redirect registration
GOOGLE_MAILBOX_SECRET_DIRECTORY./.docker-state/google
GOOGLE_MAILBOX_GRANT_KEY_REFERENCEStable reference such as google-mailbox-grant-v1
GOOGLE_MAILBOX_WATCH_ENABLEDfalse until authenticated Pub/Sub routing is complete
GOOGLE_PUBSUB_CONFIG_JSON[] without watch, complete routing array with watch
GOOGLE_WORKSPACE_CREDENTIALS_JSON, GOOGLE_DIRECTORY_POLICIES_JSON[] for personal Gmail

In the private Google directory, oauth-client-secret contains raw mailbox client-secret text, not exported OAuth JSON. mailbox-grant.key contains Base64 for 32 random bytes. Restore both original files for retained grants. They are separate from the personal email-key backup. Directory permissions are 700, file permissions 600.

Google app login can remain disabled if users sign in with Microsoft and connect Gmail separately. Login alone does not activate mailbox sync.

Gmail push needs one-time Cloud topic/subscription/IAM preparation. MailGen then creates and renews mailbox watches against that topic. The Cloud subscription and mailbox watch are separate resources. Workspace-managed accounts additionally need approved domain-wide delegation and explicit enrollment. See Workspace setup.

Cloudflare ingress prerequisite​

Before startup, create a management API token for the selected Cloudflare account and DNS zone. Scope it to account Cloudflare Tunnel Write and zone DNS Edit/Read plus Zone Read. Store it as CLOUDFLARE_TOKEN. Set CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_ZONE_ID to the selected 32-hexadecimal-character IDs.

All enabled Microsoft webhook and Google callback/push hosts must belong to that zone. Use hostnames covered by its Cloudflare edge certificate. Deeper subdomains can need additional coverage. Provider callback paths must match the configured ingress exactly.

During full setup/start, tunnel reconciliation creates or reuses remotely managed ecs-INSTANCE and its owned DNS records. Existing tunnel reuse requires matching .docker-state/tunnel/ownership.json; a matching tunnel name or restored .env alone is insufficient. Foreign DNS targets are refused before mutation.

For retained deployments, back up and restore ownership metadata as well as configuration, keys, and data. If metadata is lost, recover it from backup or have the operator explicitly resolve the old tunnel. Never delete foreign DNS or broaden token permissions to bypass ownership refusal. The local connector token remains private, and the management token is not sent to application containers. Normal stop/reset preserves tunnel ownership and does not delete cloud resources.

6. Run first setup​

Inspect existing load before cold bootstrap:

uptime
docker ps
tmux list-sessions
bash memmonitor.sh

On Linux/WSL, also use free -h. On macOS, inspect Docker Desktop resources and Activity Monitor. Choose one complete active stack. Preserve unrelated containers. Setup stops if another MailGen instance runs.

With configuration and provider prerequisites complete:

bash 0daysetup.sh
bash devstack.sh status

Setup prepares host tools/submodules, fills missing calculated settings and generated secrets, checks ownership/memory, prepares proxy/client routing, and starts compiled production services. It selects FormGen/docs-mcp and provider-dependent profiles. It preserves existing data.

Wait for Setup complete / Stack ready. If setup stops, correct the reported prerequisite and rerun. Do not reset volumes. No extra manual host backend or mandatory certgen.sh invocation is needed for this route.

7. Trust HTTPS and sign in​

Run bash configureNpmProxyHosts.sh --show to locate the selected public certificate. Follow client certificate instructions. Import only the public .crt. Never copy a private key to the browser machine.

WSL trust does not automatically configure Windows. Other browser hosts need their own name resolution and trust. Open the full Account URL, including :8443 where configured. A standard Force SSL redirect can lose that port. Do not disable TLS checks.

Complete first login. Application identity, mailbox identity, and authorization actor are separate. A directory entry or successful login does not prove mailbox onboarding.

8. Daily start, stop, and restart​

GoalCommand
Resume unchanged prepared containers after shutdownbash quickStarter.sh
Stop this instance while retaining databash stopDevServer.sh
Apply configuration without rebuilding production artifactsbash restartDevServers.sh
Explicitly select compiled production executionbash restartDevServers.sh --prod
Choose a new full development startbash startDevServers.sh
Inspect status/logsbash devstack.sh status / bash devstack.sh logs bff

Quick start does not rebuild, regenerate Prisma, recreate missing containers, or reload a changed .env. A normal restart resumes saved service modes without rebuilding production artifacts. Neither normal nor --prod restart runs schema initialization. Publish changed production code with bash restartDevServers.sh --prod, after any separately required preparation. See scoped hotfix publication for one service. See execution mode reference.

Change infrastructure mode with existing data​

Preserve configuration/data/keys and agree on downtime. Stop the original instance with its original settings. Have the operator transfer and check Kafka/Qdrant state with writers stopped. Then change selectors/remote inputs, validate, and use ordinary restart.

Before returning to local mode, check newer remote state and any required reverse copy. Old local volumes can be stale. Changing a selector is not data migration or continuous replication. See the full change procedure.

Remote stop does not administer Coolify resources. Remote mode rejects reset/cleanup even in simulation. Project reset removes only its listed owned data volumes, while host configuration/keys and other named volumes survive. Bare down -v is a different, broader removal command. Do not substitute it. Never use them for routine troubleshooting or bypass remote protection by changing placement.

9. Inspect failures​

Ctrl+C exits log viewing. In devServerAttach.sh, it can stop the actual service. Ctrl+P, then Ctrl+Q, detaches. Do not start a duplicate process.

SymptomNext action
Docker socket permission deniedLog out/in after Debian group changes. Do not create another stack identity under sudo.
Missing root configurationComplete the named root field. Exclude secret values from reports.
Incomplete Pub/Sub routingCheck full JSON and Cloud OIDC settings. Keep watch false until ready. Polling can remain enabled.
Google SERVICE_DISABLEDEnable the API in the mailbox client's project and restart consent. Do not replay old OAuth state.
EMAIL_KEY_NOT_PROVISIONEDComplete the mailbox owner's key setup and backup acknowledgment. Keep the encryption gate.
502 / 503Inspect the affected service health/logs and proxy route.
Login returns to wrong host or 443Match actual origins/callback and preserve the HTTPS port.
Memory/build failureCheck selected 40/44 GiB budget, host/Docker reserve, and other stacks. Preserve service floors and data.
Gmail message absentCheck sync/history-poller readiness, then watch/Pub/Sub delivery. Push is a trigger, not the projection itself.
Quick start says unprepared/staleUse ordinary restart. Do not modify fingerprints or delete volumes.
Remote identity/endpoint/auth failureCheck all broker metadata endpoints and role credentials. There is no local fallback.
Empty Office preview/socket failureInspect BFF/Collabora health and restricted WOPI file reads. Keep callback restrictions.
Knowledge scan/extraction/vector failureInspect the file state and affected service. Recover normally without unscanned uploads or automatic Qdrant rebuild.

10. Check the installed application​

Use your own disposable content and approved recipients. Check mailbox switching, Inbox/Sent/Drafts, body loading, draft save/reopen, and permitted send/reply. Check read state, supported Archive/Trash/restore, labels/folders, and search within each provider's documented limits.

Compare downloaded attachment bytes when file integrity matters. Check that provider-created mail arrives locally. A mutation marked accepted or awaiting_confirmation is not confirmed. Wait for its confirmation and independent canonical readback. Do not delete existing mail to prove setup.

These checks apply to the actual machine and configuration. Container health alone is insufficient.

Preserve configuration after setup​

Use secrets backup after successful setup. Preserve Google credential/grant files and personal/service keys separately. The environment archive does not contain them automatically.

A reviewed source-root path is machine-specific. Keep the selected code revision available when moving an installation. New installations normally use .. Existing production source pins change only with an agreed release.

Official installation references: Docker macOS, Docker Debian, GitHub CLI login, Git credential helper, and Git configuration.