Docker Compose Deployment

Deploying EmitMail API, background workers, PostgreSQL, and Redis with Docker Compose.

Deploy EmitMail with Docker Compose. The quickstart uses the repository's installer and the GHCR images configured by the compose file.


1. Prepare the VPS Firewall

Configure both your VPS provider's network firewall and the server firewall before starting EmitMail. Keep SSH reachable so you do not lock yourself out. If SSH uses a custom port, allow that port before enabling UFW.

For UFW, allow SSH, web traffic when you plan to use a reverse proxy, and the mail ports EmitMail needs:

sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow these only if you use a public HTTP/HTTPS reverse proxy.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Allow these mail ports if you use EmitMail's SMTP services.
sudo ufw allow 25/tcp
sudo ufw allow 2525/tcp
sudo ufw enable
sudo ufw status verbose

If you will access the dashboard only through an SSH tunnel, ports 80 and 443 are not needed. Keep dashboard 3000, API 4000, docs 3001, PostgreSQL 5432, Redis 6379, and the Docker socket proxy 2375 closed to the public internet. The web services bind to localhost; publish them through a reverse proxy after completing the first admin password change. Apply the same allowed-port policy in your VPS provider firewall.


2. Quickstart Installation (Automated)

After completing the firewall setup above, run the installer from a fresh checkout on a supported Linux VPS with Docker Compose installed. It creates or updates the environment configuration, checks host port availability, and starts the Compose stack. Review the script and environment file before exposing the service:

git clone https://github.com/omkarbhosale-dev/emitmail.git
cd emitmail
chmod +x install.sh
./install.sh

3. Manual Installation

If you prefer to configure and launch each component manually:

Clone the Repository

git clone https://github.com/omkarbhosale-dev/emitmail.git
cd emitmail

Create and Populate Environment File

cp .env.example .env

Configure Environment Variables

Open .env in your preferred editor (nano .env) and update the key host settings. Replace the secret markers below with actual generated values; do not leave marker text in the file. Generate random hex values with openssl rand -hex 24 or openssl rand -hex 32 as indicated.

# App Version & GitHub Repository
APP_VERSION=latest
GITHUB_REPO=omkarbhosale-dev/emitmail

# PostgreSQL Database Credentials
POSTGRES_USER=mailuser
POSTGRES_PASSWORD=replace_with_output_of_openssl_rand_hex_24
POSTGRES_DB=maildb

# Security Keys
# Must be at least 32 characters
JWT_SECRET=replace_with_output_of_openssl_rand_hex_32
# 64-character hex string (32 bytes) for AES-256-GCM DKIM encryption
ENCRYPTION_KEY=replace_with_output_of_openssl_rand_hex_32
# Internal secret for dashboard 1-click update trigger
UPDATER_SECRET=replace_with_output_of_openssl_rand_hex_32
CORS_ORIGINS=https://emitmail.yourdomain.com

# Public URLs & Host Port Bindings
APP_URL=https://emitmail.yourdomain.com
API_URL=https://emitmail.yourdomain.com
DASHBOARD_PORT=3000
API_PORT=4000
DOCS_PORT=3001
SMTP_SUBMISSION_PORT=2525
SMTP_RELAY_PORT=25

Automatic Onboarding Wizard

The installer generates required cryptographic and database secrets automatically. For manual setup, use unique secrets and configure CORS_ORIGINS to the dashboard origin. The initial admin login is admin / emitmail; change the temporary password in Settings before using protected features.

Protect the dashboard during first login

emitmail is a shared, publicly known temporary password. Keep the dashboard restricted to a trusted network until you sign in and change it. The API blocks protected actions until the password changes, but anyone who reaches the login page could claim the admin account first.

Configure the VPS Firewall

Before launching Compose, apply the firewall rules in Prepare the VPS Firewall. At minimum, preserve SSH access and allow the mail ports you plan to use. Do not expose dashboard, API, docs, database, Redis, or Docker proxy ports directly.

Pull Pre-Built GHCR Images and Launch

docker compose pull
docker compose up -d

Verify the Containers

docker compose ps
docker compose logs --tail=50 api worker

Confirm PostgreSQL and Redis are healthy and that the API and worker stay running before opening the dashboard.

Change the Temporary Admin Password

Keep public access disabled for now. Open http://localhost:3000 through an SSH tunnel, sign in as admin with password emitmail, and change it in Settings. EmitMail blocks protected actions until the password changes. Afterward, add your sending domain and publish the DNS values shown by the dashboard.


4. Docker Services Overview

Container NameImage / BaseInternal PortHost PortRole
emitmail-dashboardGHCR Dashboard3000127.0.0.1:3000Management Web Dashboard & Analytics UI
emitmail-docsGHCR Docs3001127.0.0.1:3001EmitDocs developer documentation platform
emitmail-apiGHCR API4000, 2525127.0.0.1:4000, 0.0.0.0:25, 0.0.0.0:2525Express REST API & Inbound ESMTP server
emitmail-workerGHCR Worker——BullMQ queue worker, DKIM signer & direct MX dispatcher
emitmail-updaterdocker:27-cli9999 (internal)—Internal update sidecar; Docker API access is filtered by a private socket proxy
emitmail-docker-socket-proxytecnativa/docker-socket-proxy2375 (private network)—Filters Docker API routes used by the updater; remains a trusted host-control component
emitmail-postgresPostgreSQL 16 Alpine5432Internal (mail_net)Primary database (never exposed to host)
emitmail-redisRedis 7 Alpine6379Internal (mail_net)Distributed job broker & cache (never exposed)

5. Reverse Proxy & Domain Routing

After changing the temporary admin password, web services (dashboard, api, docs) remain bound to 127.0.0.1 by default. Place a reverse proxy (Nginx, Traefik, Caddy, or a tunnel) in front of them if they need public access:

Host Nginx Configuration Example

# EmitMail Dashboard (e.g. emitmail.yourdomain.com)
server {
    server_name emitmail.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:4000/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# EmitDocs Portal (e.g. emitdocs.yourdomain.com)
server {
    server_name emitdocs.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

6. Updates & Upgrades

The updater reaches the host Docker daemon through emitmail-docker-socket-proxy, which is attached only to a private internal network and enables only the API groups needed for Compose updates. This remains a trusted host-control path: keep the updater secret private and do not expose either service to the public network. If you do not want dashboard-triggered upgrades, disable both the updater and docker-socket-proxy services.

EmitMail supports two update paths:

Method A: 1-Click Update from Dashboard UI

When a new GitHub release is published, a notification badge appears in your Dashboard header. Click "Download & Install Update" → "Update Now". The internal emitmail-updater sidecar automatically:

  1. Pulls new pre-built GHCR images.
  2. Synchronizes any new .env.example settings non-destructively.
  3. Recreates containers without touching user data or secrets.

Method B: Terminal Update Script

Run the non-destructive updater on your VPS:

./update.sh

To pin to a specific version:

./update.sh v1.2.0

7. Verifying Container Health & Diagnostic Logs

To inspect real-time operational logs:

docker compose logs -f api worker updater

Expected healthy output:

emitmail-api     | 🚀 EmitMail API server running on port 4000 (production)
emitmail-api     | 📬 [Inbound SMTP] Server listening on 0.0.0.0:2525 (EHLO: mail.yourdomain.com)
emitmail-worker  | ⚙️ [BullMQ Worker] Connected to Redis at redis:6379
emitmail-worker  | 🔒 [Crypto] AES-256-GCM encryption verified for DKIM key storage
emitmail-updater | 🚀 [EmitMail Updater] Starting internal HTTP daemon on port 9999...

PostgreSQL P1000 authentication failure

If the API or worker logs show Prisma error P1000, the password in .env does not match the password stored for the PostgreSQL role. The Postgres image only uses POSTGRES_PASSWORD when it initializes an empty data volume; editing .env later does not change the existing database password. Keep the database volume intact.

To make the current .env password authoritative, connect through the local PostgreSQL socket inside the container and reset the role password interactively:

docker compose exec postgres psql -U mailuser -d postgres

At the psql prompt, run \password mailuser, enter the exact value of POSTGRES_PASSWORD from .env twice, then run \q. Replace mailuser with your configured POSTGRES_USER if you changed the default. Recreate the app containers so they retry their database connection:

docker compose up -d --force-recreate api worker
docker compose ps
docker compose logs --tail=50 api worker

Do not use docker compose down -v for this recovery; it deletes the database volume and its data. If the local psql connection is rejected, restore the original .env backup or use your PostgreSQL recovery procedure before restarting EmitMail.

On this page