Marketing teams often outgrow lightweight email tools when they need owned data, clear consent records, and automation that connects cleanly to CRM and analytics systems. Mautic is a strong open-source option for that moment: it gives you contact segments, forms, campaigns, landing pages, email templates, and tracking without making every customer journey depend on a closed SaaS platform. This guide shows a production-oriented Mautic deployment on Ubuntu using Docker Compose, Caddy for HTTPS, MariaDB for durable storage, and Redis for cache/session support.
The goal is not only to make the first web page load. A reliable Mautic installation needs predictable background workers, safe secret handling, deliverability-friendly mail settings, backups that can actually restore, and health checks that detect broken cron jobs before your next campaign fails. The pattern below mirrors the practical SysBrix Guides style: simple components, explicit files, clear verification commands, and operational guardrails you can hand to a small platform team.
Architecture and flow overview
The deployment uses one Ubuntu host running Docker and Docker Compose. Caddy listens on ports 80 and 443, obtains and renews TLS certificates, and proxies traffic to the internal Mautic PHP/Apache container. Mautic stores application data in MariaDB and uses Redis for fast cache and lock handling. A dedicated worker container runs scheduled Mautic console commands so campaigns, email queues, webhooks, and segment rebuilds continue even when nobody is logged in.
The request flow is straightforward: a visitor reaches https://mautic.example.com, Caddy terminates TLS, Mautic serves the UI or tracking endpoint, and application state lands in MariaDB. Outbound email should normally go through a transactional provider such as Amazon SES, Postmark, Mailgun, or an internal SMTP relay. Avoid sending directly from the VPS unless you already manage SPF, DKIM, DMARC, bounce handling, and IP reputation.
Prerequisites
- Ubuntu 22.04 or 24.04 LTS server with at least 2 vCPU, 4 GB RAM, and 40 GB disk for small teams.
- A DNS record such as
mautic.example.compointing to the server. - Docker Engine and the Compose plugin installed.
- Firewall allowing SSH, HTTP, and HTTPS only.
- An SMTP account or relay already configured with verified sender domains.
- A backup destination outside the host, such as S3-compatible storage, rsync, or a managed snapshot system.
Step-by-step deployment
Start by creating a dedicated directory. Keeping all Mautic files under one path makes upgrades, backups, and incident response easier.
sudo mkdir -p /opt/mautic/{caddy,data,db,redis,backups}
sudo chown -R "$USER:$USER" /opt/mautic
cd /opt/mautic
If the copy button is unavailable, manually select the command text and copy it.
Create an environment file with secrets that are not committed to Git. Generate the passwords locally and store the real values in your password manager.
cat > .env.example <<'EOF'
MAUTIC_DOMAIN=mautic.example.com
MAUTIC_DB_NAME=mautic
MAUTIC_DB_USER=mautic
MAUTIC_DB_PASSWORD=replace_with_a_long_random_password
MYSQL_ROOT_PASSWORD=replace_with_a_different_long_random_password
MAUTIC_TRUSTED_PROXIES=0.0.0.0/0
PHP_MEMORY_LIMIT=512M
UPLOAD_MAX_FILESIZE=64M
POST_MAX_SIZE=64M
EOF
cp .env.example .env
chmod 600 .env
If the copy button is unavailable, manually select the command text and copy it.
Use this Compose file as a baseline. Pin major versions, define restart policies, isolate services on a private network, and mount persistent volumes explicitly.
cat > docker-compose.yml <<'EOF'
services:
caddy:
image: caddy:2.8
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/data:/data
- ./caddy/config:/config
depends_on:
- mautic
mautic:
image: mautic/mautic:5-apache
restart: unless-stopped
env_file: .env
environment:
MAUTIC_DB_HOST: db
MAUTIC_DB_PORT: 3306
MAUTIC_DB_NAME: ${MAUTIC_DB_NAME}
MAUTIC_DB_USER: ${MAUTIC_DB_USER}
MAUTIC_DB_PASSWORD: ${MAUTIC_DB_PASSWORD}
MAUTIC_RUN_CRON_JOBS: "false"
PHP_MEMORY_LIMIT: ${PHP_MEMORY_LIMIT}
UPLOAD_MAX_FILESIZE: ${UPLOAD_MAX_FILESIZE}
POST_MAX_SIZE: ${POST_MAX_SIZE}
volumes:
- ./data:/var/www/html
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
worker:
image: mautic/mautic:5-apache
restart: unless-stopped
env_file: .env
command: ["bash", "-lc", "while true; do php bin/console mautic:segments:update --no-interaction; php bin/console mautic:campaigns:update --no-interaction; php bin/console mautic:campaigns:trigger --no-interaction; php bin/console mautic:emails:send --no-interaction; sleep 300; done"]
volumes:
- ./data:/var/www/html
depends_on:
- mautic
- db
- redis
db:
image: mariadb:11.4
restart: unless-stopped
environment:
MYSQL_DATABASE: ${MAUTIC_DB_NAME}
MYSQL_USER: ${MAUTIC_DB_USER}
MYSQL_PASSWORD: ${MAUTIC_DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
- ./db:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "healthcheck.sh --connect --innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- ./redis:/data
EOF
If the copy button is unavailable, manually select the command text and copy it.
Create the Caddy reverse proxy configuration. Replace the domain and email address before launching.
cat > Caddyfile <<'EOF'
{
email [email protected]
}
{$MAUTIC_DOMAIN} {
encode zstd gzip
reverse_proxy mautic:80
header {
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
}
}
EOF
If the copy button is unavailable, manually select the command text and copy it.
Launch the stack and watch the first boot. The initial Mautic container may take a few minutes while it prepares application files and database migrations.
docker compose pull
docker compose up -d
docker compose ps
docker compose logs -f --tail=80 mautic caddy
If the copy button is unavailable, manually select the command text and copy it.
Configuration and secrets handling best practices
After the web installer opens, create the first administrator account, connect Mautic to MariaDB using the same database values from .env, and configure the site URL as the HTTPS domain. Immediately configure mail transport through a trusted SMTP provider, then send a test message to a mailbox you control. For production, enable bounce processing and unsubscribe handling before importing contacts.
Never store SMTP passwords, OAuth client secrets, or database passwords inside documentation repos. Keep the live .env on the server with mode 600, back it up through an encrypted secret store, and rotate it when an operator leaves. Limit admin accounts, use strong unique passwords, and place Mautic behind SSO or a VPN if only internal teams need dashboard access.
Plan the first production launch around consent and deliverability rather than only server uptime. Import contacts in small batches, preserve the source and timestamp for consent, and segment internal test users away from real prospects. Configure unsubscribe text, company address details, tracking domain alignment, and bounce processing before the first campaign. If marketing operations and sales operations both touch Mautic, define ownership for forms, lists, naming conventions, and archived campaigns so the instance does not become a shared junk drawer after a few months.
Operationally, treat campaign automation as a queueing system. Watch worker completion time, database growth, and failed email counts after every large send. Keep a short runbook that explains how to pause sending, disable a broken campaign, restore yesterday's database, and rotate SMTP credentials. These procedures matter most during incidents, when the team is under pressure and guessing from memory can create duplicate sends or data loss.
Use a small maintenance script for backups. Test restore quarterly; untested backups are only optimistic log files.
cat > backup.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd /opt/mautic
set -a
. ./.env
set +a
stamp=$(date -u +%Y%m%dT%H%M%SZ)
mkdir -p /opt/mautic/backups/$stamp
docker compose exec -T db mariadb-dump -uroot -p"$MYSQL_ROOT_PASSWORD" "$MAUTIC_DB_NAME" > /opt/mautic/backups/$stamp/mautic.sql
tar -C /opt/mautic -czf /opt/mautic/backups/$stamp/mautic-files.tgz data Caddyfile docker-compose.yml .env.example
find /opt/mautic/backups -mindepth 1 -maxdepth 1 -type d -mtime +14 -exec rm -rf {} +
EOF
chmod +x backup.sh
If the copy button is unavailable, manually select the command text and copy it.
Verification checklist
- Open the HTTPS URL and confirm the certificate is valid.
- Complete the Mautic installer and log in as the first administrator.
- Send a test email through your SMTP provider and confirm it reaches the inbox.
- Create a test segment, form, campaign, and email queue item.
- Confirm the worker container logs show campaign and email commands running every five minutes.
- Run a database dump and verify the backup file is not empty.
curl -I https://mautic.example.com
docker compose ps
docker compose logs --tail=120 worker
docker compose exec mautic php bin/console mautic:segments:update --no-interaction
./backup.sh
ls -lah backups/*/mautic.sql
If the copy button is unavailable, manually select the command text and copy it.
Common issues and fixes
Caddy cannot obtain a certificate. Check that the DNS record points to the server and ports 80 and 443 are reachable from the public internet. Cloud firewalls and stale AAAA records are common causes.
Campaigns do not trigger. Inspect the worker logs and run the console commands manually. If they fail, fix the underlying database, email, or permissions error instead of shortening the sleep interval.
Uploads fail or large emails cannot be saved. Increase UPLOAD_MAX_FILESIZE, POST_MAX_SIZE, and PHP memory, then recreate the Mautic container.
Email deliverability is poor. Verify SPF, DKIM, and DMARC, use a reputable SMTP relay, warm up lists gradually, and avoid importing contacts without consent evidence.
The application feels slow. Confirm Redis is reachable, reduce heavy dashboard queries during campaigns, and watch database CPU and disk latency during segment rebuilds.
FAQ
Can this run on one small VPS?
Yes for small teams and moderate contact lists, but monitor memory, database I/O, and worker duration. Upgrade before campaign jobs take longer than their schedule interval.
Should Mautic send email directly from the server?
Usually no. Use a transactional mail provider or managed SMTP relay so reputation, bounce handling, and domain authentication are easier to operate.
How often should the worker run?
Five minutes is a reasonable default. High-volume installations may split segment updates, campaign triggers, imports, and email sending into separate worker containers.
Where should uploaded assets live?
The default local volume is fine for a single host. For multi-node or large asset libraries, plan object storage and test backup/restore before launch.
How do we upgrade safely?
Take a database and file backup, read the Mautic release notes, pull the new image in staging, run migrations, and only then repeat the process in production.
What should be monitored?
Monitor HTTPS availability, container restarts, disk usage, MariaDB backups, worker log freshness, queue depth, SMTP failures, and campaign execution time.
Internal links
- Deploy Typesense with Docker Compose + Caddy + API Keys
- Deploy Linkwarden with Docker Compose + Caddy + PostgreSQL
- Deploy HedgeDoc with Docker Compose + Caddy + PostgreSQL
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.