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 verboseIf 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.sh3. 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 emitmailCreate and Populate Environment File
cp .env.example .envConfigure 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=25Automatic 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 -dVerify the Containers
docker compose ps
docker compose logs --tail=50 api workerConfirm 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 Name | Image / Base | Internal Port | Host Port | Role |
|---|---|---|---|---|
emitmail-dashboard | GHCR Dashboard | 3000 | 127.0.0.1:3000 | Management Web Dashboard & Analytics UI |
emitmail-docs | GHCR Docs | 3001 | 127.0.0.1:3001 | EmitDocs developer documentation platform |
emitmail-api | GHCR API | 4000, 2525 | 127.0.0.1:4000, 0.0.0.0:25, 0.0.0.0:2525 | Express REST API & Inbound ESMTP server |
emitmail-worker | GHCR Worker | — | — | BullMQ queue worker, DKIM signer & direct MX dispatcher |
emitmail-updater | docker:27-cli | 9999 (internal) | — | Internal update sidecar; Docker API access is filtered by a private socket proxy |
emitmail-docker-socket-proxy | tecnativa/docker-socket-proxy | 2375 (private network) | — | Filters Docker API routes used by the updater; remains a trusted host-control component |
emitmail-postgres | PostgreSQL 16 Alpine | 5432 | Internal (mail_net) | Primary database (never exposed to host) |
emitmail-redis | Redis 7 Alpine | 6379 | Internal (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:
- Pulls new pre-built GHCR images.
- Synchronizes any new
.env.examplesettings non-destructively. - Recreates containers without touching user data or secrets.
Method B: Terminal Update Script
Run the non-destructive updater on your VPS:
./update.shTo pin to a specific version:
./update.sh v1.2.07. Verifying Container Health & Diagnostic Logs
To inspect real-time operational logs:
docker compose logs -f api worker updaterExpected 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 postgresAt 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 workerDo 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.