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:
| Choice | This route |
|---|---|
| Infrastructure | ECS_DEPLOYMENT_MODE=fulllocal, ECS_REMOTE_SIMULATION=false |
| Application login | Microsoft through Account |
| Mailboxes | Microsoft plus personal Gmail |
| Gmail updates | History polling: OAuth/sync enabled, watch disabled |
| Execution | Compiled production through 0daysetup.sh |
| Extra applications | FormGen and docs-mcp, selected by setup |
| Application memory | ECS_MEMORY_GIB=44, plus host/build reserves |
| Source/configuration | This 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.
| Component | Purpose | Real partialRemote location |
|---|---|---|
| BFF | Browser UI and server-side browser requests | Local |
| Account | Application login and sessions | Local |
| Backend and .NET services | Processing, synchronization, search | Local |
| PostgreSQL | Users, settings, mailbox records, history | Local |
| SeaweedFS/S3 | Bodies, attachments, other files | Local |
| OpenSearch | Search indexes | Local |
| Dragonfly | Cache and temporary coordination | Local |
| Kafka, three brokers | Commands and events between services | Remote Coolify |
| ClamAV | Uploaded-file scanning | Remote Coolify |
| Collabora | Office document previews | Remote Coolify |
| Tika / knowledge-extractor | Document text extraction | Remote Coolify |
| Qdrant | Knowledge-retrieval vectors | Remote 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:
- GitHub access to
t42-hu/t42-motherrepoand every required private submodule repository. - Values for a new empty root configuration. An archive is an alternative for retained deployments, with its password delivered separately.
- Account-login credentials and callback registration.
- Microsoft Graph application permissions and admin consent, if Microsoft is selected.
- Google OAuth clients and optional Pub/Sub routing, if Google is selected.
- Your own public HTTPS webhook domain and Cloudflare management-token/ownership setup.
- For real remote services, checked endpoints, separate tokens/passwords, Kafka cluster identity, and Tailscale access.
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 workload | Explicit application budget |
|---|---|
| Standard full stack without Google worker profile | ECS_MEMORY_GIB=40 |
| Full Microsoft + Google + FormGen stack | ECS_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
- macOS
- Debian
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.
Use Debian 12 or 13 with sudo access:
sudo apt-get update
sudo apt-get install -y ca-certificates curl git python3 openssl unzip zip tmux
The next block adds Docker's official Debian repository. On an existing Docker machine, inspect the vendor's conflicting-package instructions first.
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF_DOCKER
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF_DOCKER
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
Log out of the machine and log in again to apply Docker group membership. The Docker group gives administrative access. Grant it only to the operator account.
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.
- macOS
- Debian
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
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /etc/apt/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null
sudo apt-get update
sudo apt-get install -y gh
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 setting | Source |
|---|---|
MICROSOFT_CONNECTOR_ENABLED | true for Microsoft, explicit false for Google-only. Omission defaults to enabled. |
BFF_MICROSOFT_CLIENT_ID, BFF_MICROSOFT_CLIENT_SECRET, BFF_MICROSOFT_TENANT_ID | Login Entra application |
BACKEND_CLIENT_ID, BACKEND_CLIENT_SECRET, BACKEND_TENANT_ID | Separate app-only Graph application with admin consent |
GRAPH_WEBHOOK_URL, GRAPH_LIFECYCLE_WEBHOOK_URL | Your public ingress's supported notification/lifecycle paths |
GRAPH_WEBHOOK_CLIENT_STATE | Stable private validation secret |
DELTA_LINK_ENCRYPTION_KEY | New 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 setting | Required value |
|---|---|
GOOGLE_APP_LOGIN_ENABLED | true only after the separate app-login client/public callback is ready |
BFF_GOOGLE_LOGIN_CLIENT_ID, BFF_GOOGLE_LOGIN_CLIENT_SECRET | Complete app-login client pair |
GOOGLE_APP_LOGIN_CALLBACK_URL, GOOGLE_APP_LOGIN_PROXY_SECRET | Exact public HTTPS /api/auth/callback/google and separate internal secret |
GOOGLE_MAILBOX_OAUTH_ENABLED, GOOGLE_MAILBOX_SYNC_ENABLED | Both true for mailbox connection plus background sync |
ECAS_DEPLOYMENT_TENANT_ID | Stable deployment UUID, not an email/domain/name |
GOOGLE_MAILBOX_CLIENT_ID | Dedicated mailbox Web client ID |
GOOGLE_MAILBOX_CALLBACK_URL | Registered public HTTPS /v1/oauth/callback |
GOOGLE_MAILBOX_BFF_COMPLETION_URL | BFF /api/google-mailbox/complete, not a Google redirect registration |
GOOGLE_MAILBOX_SECRET_DIRECTORY | ./.docker-state/google |
GOOGLE_MAILBOX_GRANT_KEY_REFERENCE | Stable reference such as google-mailbox-grant-v1 |
GOOGLE_MAILBOX_WATCH_ENABLED | false 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
| Goal | Command |
|---|---|
| Resume unchanged prepared containers after shutdown | bash quickStarter.sh |
| Stop this instance while retaining data | bash stopDevServer.sh |
| Apply configuration without rebuilding production artifacts | bash restartDevServers.sh |
| Explicitly select compiled production execution | bash restartDevServers.sh --prod |
| Choose a new full development start | bash startDevServers.sh |
| Inspect status/logs | bash 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.
| Symptom | Next action |
|---|---|
| Docker socket permission denied | Log out/in after Debian group changes. Do not create another stack identity under sudo. |
| Missing root configuration | Complete the named root field. Exclude secret values from reports. |
| Incomplete Pub/Sub routing | Check full JSON and Cloud OIDC settings. Keep watch false until ready. Polling can remain enabled. |
Google SERVICE_DISABLED | Enable the API in the mailbox client's project and restart consent. Do not replay old OAuth state. |
EMAIL_KEY_NOT_PROVISIONED | Complete the mailbox owner's key setup and backup acknowledgment. Keep the encryption gate. |
502 / 503 | Inspect the affected service health/logs and proxy route. |
Login returns to wrong host or 443 | Match actual origins/callback and preserve the HTTPS port. |
| Memory/build failure | Check selected 40/44 GiB budget, host/Docker reserve, and other stacks. Preserve service floors and data. |
| Gmail message absent | Check sync/history-poller readiness, then watch/Pub/Sub delivery. Push is a trigger, not the projection itself. |
| Quick start says unprepared/stale | Use ordinary restart. Do not modify fingerprints or delete volumes. |
| Remote identity/endpoint/auth failure | Check all broker metadata endpoints and role credentials. There is no local fallback. |
| Empty Office preview/socket failure | Inspect BFF/Collabora health and restricted WOPI file reads. Keep callback restrictions. |
| Knowledge scan/extraction/vector failure | Inspect 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.