Self-Hosted Installation Guide

Deploy MailDesk on your own infrastructure. All data stays on your servers.

Prerequisites

RequirementMinimum
Docker Engine 24+
Docker Compose v2.20+
4 GB RAM
20 GB disk (+ attachment storage)
Any Linux with Docker support
License key from maildesk.cloud
The app listens on port 3000 by default. Place a reverse proxy (Caddy, Nginx, Traefik) in front of it for TLS.

1. Extract and load

Extract the package:

shell
tar xzf maildesk-selfhosted.tar.gz
cd maildesk

Load the Docker images:

shell
docker load < maildesk-images.tar.gz

2. Configure environment

shell
./setup.sh

Run ./setup.sh to auto-generate all secrets. Then edit .env to set your APP_URL and license key.

Open .env and set the following values:

Required variables

VariableDescription
APP_URLPublic URL of your instance (no trailing slash)
MAILDESK_LICENSE_KEYYour Ed25519-signed JWT license key

Auto-generated by setup.sh

These are generated automatically when you run setup.sh. Do not change unless migrating data.

VariableDescription
BETTER_AUTH_SECRETSession signing key, min 32 chars (auto-generated)
CREDENTIAL_ENCRYPTION_KEYAES-256-GCM key for IMAP/SMTP passwords (auto-generated)
POSTGRES_PASSWORDPostgreSQL database password (auto-generated)
REDIS_PASSWORDRedis cache password (auto-generated)
MAILDESK_MODESet to selfhosted (auto-set by setup.sh)

Optional variables

VariableDescription
MAILDESK_LICENSE_SERVER_URLLicense server URL for online verification (default: offline-only)
REGISTRATION_ENABLEDtrue (default) or false to disable self-signup
ALLOWED_REGISTER_IPSComma-separated IPs allowed to access /register
ENABLE_HSTStrue to send HSTS header (enable when behind TLS)
LOG_LEVELtrace, debug, info (default), warn, error, fatal
METRICS_AUTH_TOKENBearer token for /api/metrics endpoint (hidden if unset)
OPENAI_EMBEDDING_KEYOpenAI API key for KB semantic search (optional)
MAX_ATTACHMENT_SIZE_MBMax attachment size in MB (default: 25)

Example .env for self-hosted

env
# Only these 2 lines need manual editing after running ./setup.sh:
APP_URL=https://tickets.example.com
MAILDESK_LICENSE_KEY=eyJhbGciOiJFZERTQSIs...your-key-here

# Everything else is auto-generated by ./setup.sh

3. Start

shell
docker compose up -d

This starts 6 services:

ServicePurpose
migrateRuns database migrations, then exits
appNext.js web application (port 3000)
workerBackground worker (IMAP/SMTP, email processing, jobs)
postgresPostgreSQL 17 with pgvector extension
redisRedis 7 for job queues and pub/sub
clamavClamAV antivirus for attachment scanning

The migrate service runs automatically before app and worker start. It creates all tables idempotently — safe to run on both fresh and existing databases.

4. Verify the deployment

shell
# Check all services are healthy
docker compose ps

# Check app logs
docker compose logs app

# Check worker logs
docker compose logs worker

# Health check endpoint
curl http://localhost:3000/api/health

Once healthy, open your APP_URL in a browser. You should see the login/registration page.

5. Initial setup

  1. 1Register the first admin account at your-domain.com/register
  2. 2Create an organization — this is your workspace
  3. 3Add a mailbox under Settings > Mailboxes (IMAP/SMTP credentials)
  4. 4Incoming emails will automatically create tickets
Tip: Use ALLOWED_REGISTER_IPS to restrict who can create accounts, or set REGISTRATION_ENABLED=false after creating the admin account.

Email Verification (Optional)

Configure SMTP to enable email verification for new users. Without SMTP, new users are auto-verified and can log in immediately.

.env
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=you@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM=MailDesk <noreply@yourdomain.com>
Without SMTP: Email verification is skipped automatically. New users can register and log in without confirming their email. You can add SMTP later at any time.

Reverse Proxy Setup

MailDesk requires a reverse proxy for TLS termination. Here are examples for common setups:

Caddyfile
tickets.example.com {
    handle /ws* {
        reverse_proxy localhost:3002
    }
    handle {
        reverse_proxy localhost:3000
    }
}
WebSocket: MailDesk uses WebSocket for real-time updates (chat, notifications, ticket events). The /ws route must point to port 3002 (worker).

License Key

Your license key is an Ed25519-signed JWT that contains your plan, limits, and feature flags.

What the key controls:

  • Plan tier (Free, Pro, Business)
  • Limits: max agents, mailboxes, tickets/month, storage
  • Features: AI reply, knowledge base, auto-translation

Validation:

  • The key is validated offline by default using the embedded Ed25519 public key — no external calls needed
  • If MAILDESK_LICENSE_SERVER_URL is set, MailDesk will periodically verify the key online (optional)
  • Validated license data is cached in Redis for 24 hours
  • If Redis is down, the JWT is verified directly (Ed25519 needs no server)

Expired or invalid key:

  • The app enters read-only mode — existing data remains accessible
  • Creating tickets, sending replies, and AI features are disabled
  • A banner shows "License expired" with a renewal link

Updating

shell
# Load new images
docker load < maildesk-images.tar.gz

# Restart (migrations run automatically)
docker compose up -d

Migrations run automatically on every start — no manual migration step needed.

Backup & Restore

Backup

Database

shell
docker compose exec postgres pg_dump -U maildesk maildesk > backup_$(date +%Y%m%d).sql

Attachments

shell
docker compose cp worker:/data/attachments ./attachments-backup

Redis (optional — only caches, can be rebuilt)

shell
docker compose exec redis redis-cli BGSAVE

Restore

Database

shell
docker compose exec -T postgres psql -U maildesk maildesk < backup_20260317.sql

Attachments

shell
docker compose cp ./attachments-backup/. worker:/data/attachments

Troubleshooting

App won't start

shell
docker compose logs app
docker compose logs migrate

Common causes:

  • Database not ready — The migrate service waits for Postgres health check. Check docker compose ps for unhealthy services.
  • Invalid DATABASE_URL — Ensure the connection string matches the Postgres credentials.
  • Port conflict — If port 3000 is taken, change the port mapping in docker-compose.yml.

License errors

shell
docker compose logs app | grep -i license
  • "License invalid" — Verify MAILDESK_LICENSE_KEY is set correctly in .env (no line breaks, no quotes around the JWT).
  • "License expired" — Renew your license at maildesk.cloud.

ClamAV slow to start

ClamAV downloads virus definitions on first start, which can take 3–5 minutes.

shell
docker compose logs clamav

Worker not processing emails

shell
docker compose logs worker
  • Verify IMAP credentials are correct in the mailbox settings
  • Check that the worker service is healthy
  • Ensure Redis is reachable

Architecture Overview

        ┌─────────────┐
        │  Reverse    │
        │  Proxy      │
        │  (TLS)      │
        └──────┬──────┘
               │ :3000
        ┌──────▼──────┐
        │    App      │
        │  (Next.js)  │
        └──┬──────┬───┘
           │      │
  ┌────────▼─┐  ┌─▼────────┐
  │ Postgres │  │  Redis   │
  │ (pgvec)  │  │ (queue)  │
  └────────▲─┘  └─▲────────┘
           │      │
        ┌──┴──────┴───┐
        │   Worker    │
        │ (IMAP/SMTP) │
        └──────┬──────┘
               │
        ┌──────▼──────┐
        │   ClamAV    │
        │ (antivirus) │
        └─────────────┘
  • App App serves the web UI and API routes
  • Worker Worker connects to mailboxes via IMAP, sends replies via SMTP, processes background jobs
  • Postgres Postgres stores all application data (with pgvector for AI embeddings)
  • Redis Redis handles job queues (BullMQ) and real-time pub/sub (WebSocket)
  • ClamAV ClamAV scans email attachments for malware