Complete guide to running Mautic 5.x in a single Docker container with nginx, PHP-FPM, and cron — designed for low-traffic marketing automation (< 50k contacts).
Reverse Proxy (Traefik/nginx) → Container (nginx:80 → PHP-FPM:9000)
↓
External MariaDB 11+
Single container runs three processes via supervisord:
- nginx — serves static assets + proxies PHP to FPM
- PHP-FPM — handles Mautic requests (TCP 9000)
- cron — runs Mautic scheduled tasks (segments, campaigns, emails)
Resource usage (< 50k contacts): ~100–150 MB RAM, < 0.1% CPU idle.
FROM php:8.3-fpm-bookworm
# System deps
RUN apt-get update && apt-get install -y --no-install-recommends \
nginx cron supervisor \
libpng-dev libjpeg62-turbo-dev libfreetype6-dev \
libzip-dev libicu-dev libbz2-dev libxml2-dev \
libc-client-dev libkrb5-dev libldap2-dev \
libmagickwand-dev libexif-dev \
unzip mariadb-client \
&& rm -rf /var/lib/apt/lists/*
# PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-configure imap --with-kerberos --with-imap-ssl \
&& docker-php-ext-install -j$(nproc) \
bcmath bz2 gd imap intl mysqli pdo_mysql \
opcache soap zip ldap sockets exif
# PECL extensions
RUN pecl install igbinary redis imagick \
&& docker-php-ext-enable igbinary redis imagick
# PHP config
RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
COPY php-overrides.ini /usr/local/etc/php/conf.d/99-mautic.ini
# PHP-FPM: TCP 9000, tuned for low-traffic (3 children × 256MB = 768MB ceiling)
RUN sed -i \
-e 's/pm.max_children = 5/pm.max_children = 3/' \
-e 's/pm.start_servers = 2/pm.start_servers = 1/' \
-e 's/pm.min_spare_servers = 1/pm.min_spare_servers = 1/' \
-e 's/pm.max_spare_servers = 3/pm.max_spare_servers = 2/' \
/usr/local/etc/php-fpm.d/www.conf
# Nginx config
RUN rm -f /etc/nginx/sites-enabled/default
COPY nginx.conf /etc/nginx/sites-enabled/mautic.conf
# Cron
COPY crontab /etc/cron.d/mautic
RUN chmod 0644 /etc/cron.d/mautic
# Supervisor
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Mautic source code
COPY html/ /var/www/html/
# Build frontend assets (CKEditor + compiled CSS with Remix Icons)
# Node is only needed at build time — removed after
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl gnupg \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& cd /var/www/html \
&& npm install --include=dev \
&& npx webpack --mode production --config webpack.config.js \
&& php -d memory_limit=512M bin/console mautic:assets:generate --env=prod --no-interaction \
&& rm -rf node_modules /root/.npm \
&& apt-get purge -y nodejs \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
RUN chown -R www-data:www-data /var/www/html
WORKDIR /var/www/html
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]Mautic 5 switched to CKEditor 5 (TypeScript, needs webpack) and Remix Icon (needs LESS compilation). Without running mautic:assets:generate:
- CKEditor returns 404 — only TypeScript source exists at
/app/assets/libraries/ckeditor/src/ - All sidebar icons are invisible — Remix Icon
@font-faceis missing from compiled/media/css/libraries.css
The command combines each bundle's Assets/css/* and Assets/js/* into production files in /media/css/ and /media/js/.
server {
listen 80;
server_name _;
root /var/www/html;
index index.php index.html;
client_max_body_size 64M;
# Mautic tracking pixel and JS — virtual URLs handled by PHP
location = /mtc.js { rewrite ^ /index.php last; }
location = /mtc { rewrite ^ /index.php last; }
location / {
try_files $uri /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_read_timeout 300;
}
# IMPORTANT: Allow font files from bundle Assets directories
# Compiled CSS references fonts via relative paths:
# url(../../app/bundles/CoreBundle/Assets/css/libraries/remixicon/fonts/remixicon.woff2)
# Without this rule, the deny block below returns 403 for all icon fonts
location ~* /app/bundles/.+/Assets/.+\.(woff2?|ttf|eot|svg|otf)$ {
expires 1y;
access_log off;
add_header Cache-Control "public, immutable";
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
access_log off;
add_header Cache-Control "public, immutable";
}
location ~ /\. { deny all; }
# Block sensitive directories — MUST anchor with ^/
# Without ^, routes like /s/config/edit match "config" → 403
location ~* ^/(app|bin|config|vendor|plugins|misc|node_modules)/ {
deny all;
}
}| Problem | Cause | Fix |
|---|---|---|
| Icons return 403 | Deny rule blocks /app/ path where fonts live |
Add font-allow rule before deny rule |
/s/config/edit returns 403 |
Unanchored /(config) matches Mautic routes |
Anchor with ^/ |
| Tracking pixel 404 | try_files looks for nonexistent /mtc file |
Add explicit rewrite rules |
| PHP-FPM connection refused | Unix socket fails under supervisord | Use TCP 127.0.0.1:9000 |
services:
mautic:
build:
context: .
dockerfile: Dockerfile
container_name: mautic
restart: unless-stopped
ports:
- "8085:80"
networks:
- default
- proxy # Traefik/reverse proxy network
- db-network # External MariaDB access
labels:
- "traefik.enable=true"
- "traefik.http.routers.mautic.rule=Host(`mautic.example.com`)"
- "traefik.http.routers.mautic.entrypoints=https"
- "traefik.http.routers.mautic.tls=true"
- "traefik.http.services.mautic.loadbalancer.server.port=80"
- "traefik.http.routers.mautic-http.rule=Host(`mautic.example.com`)"
- "traefik.http.routers.mautic-http.entrypoints=http"
- "traefik.http.routers.mautic-http.middlewares=redirect-to-https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
volumes:
- mautic-config:/var/www/html/config
- mautic-media:/var/www/html/media
- mautic-plugins:/var/www/html/plugins
- mautic-themes:/var/www/html/themes
- mautic-var:/var/www/html/var
- mautic-translations:/var/www/html/translations
environment:
- MAUTIC_DB_HOST=your-mariadb-container
- MAUTIC_DB_NAME=mautic
- MAUTIC_DB_USER=mautic
- MAUTIC_DB_PASSWORD=${MAUTIC_DB_PASSWORD}
networks:
proxy:
external: true
db-network:
external: true
volumes:
mautic-config:
mautic-media:
mautic-plugins:
mautic-themes:
mautic-var:
mautic-translations:- config —
local.phpwith DB/mailer/site settings - media — uploads + compiled assets (may need refresh after image rebuild)
- var — cache, logs, sessions
- themes — email/landing page templates
Caveat: The media volume persists old compiled CSS across rebuilds. After upgrading, either clear the volume or run mautic:assets:generate in the new container.
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
*/5 * * * * www-data php /var/www/html/bin/console mautic:segments:update --no-interaction >> /var/log/mautic-cron.log 2>&1
*/10 * * * * www-data php /var/www/html/bin/console mautic:campaigns:update --no-interaction >> /var/log/mautic-cron.log 2>&1
*/15 * * * * www-data php /var/www/html/bin/console mautic:campaigns:trigger --no-interaction >> /var/log/mautic-cron.log 2>&1
*/15 * * * * www-data php /var/www/html/bin/console mautic:messages:send --no-interaction >> /var/log/mautic-cron.log 2>&1
0 * * * * www-data php /var/www/html/bin/console mautic:broadcasts:send --no-interaction >> /var/log/mautic-cron.log 2>&1
Critical:
- PATH is required — Docker cron doesn't inherit the container's PATH
- Run as
www-data— running as root creates cache files PHP-FPM can't read mautic:messages:send— renamed frommautic:emails:sendin Mautic 5- Don't add
messenger:consume email— only for async transport; Mautic defaults to SyncTransport
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
[program:php-fpm]
command=/usr/local/sbin/php-fpm --nodaemonize
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:cron]
command=/usr/sbin/cron -f
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
date.timezone = UTC
; OPcache
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0For significant performance boost, add:
opcache.jit = 1255
opcache.jit_buffer_size = 64M
opcache.interned_strings_buffer = 32
opcache.enable_file_override = On
opcache.file_cache = /tmp/opcacheNote: jit_buffer_size=0 (default) disables JIT entirely, even with jit=tracing.
For databases > 1GB (Mautic's email_stats table grows fast):
innodb_buffer_pool_size = 512M # Default 128M
join_buffer_size = 4M # Default 256K — Mautic has complex joins
table_open_cache = 400 # Default 64 — Mautic has 109+ tables
tmp_table_size = 64M # Default 16M
max_heap_table_size = 64M # Must match tmp_table_size
innodb_log_file_size = 128M # Default 48M- Backup DB + files
- Upgrade incrementally: 3.x → 3.3.5 → 4.4.13 → 5.2.x
- Run migrations:
php bin/console doctrine:migrations:migrate --no-interaction - Replace themes — Mautic 5 requires
"builder": ["grapesjsbuilder"]inconfig.json - Clear cache as www-data:
docker exec -u www-data mautic php -d memory_limit=512M bin/console cache:clear --env=prod - Regenerate assets:
docker exec -u www-data mautic php bin/console mautic:assets:generate - Purge CDN cache if behind Cloudflare or similar
| Issue | Solution |
|---|---|
| Icons missing after deploy | Run mautic:assets:generate; purge CDN cache |
| CKEditor 404 | Check /media/libraries/ckeditor/ckeditor.js exists; run webpack if not |
| 403 on admin pages | Check nginx deny rule anchoring (^/) |
| Cache permission errors | Always clear cache as www-data, never root |
| Cron not running | Check PATH in crontab; verify cron -f in supervisord |
| Slow after upgrade | Restart container (clears OPcache); check MariaDB slow log |
If Mautic is behind Cloudflare:
- SSL mode must be "Full" — "Flexible" sends HTTP to origin, Traefik HTTPS router gets 504
- CF caches 404s — after fixing missing assets, purge zone cache
- ACME HTTP-01 certs fail behind CF proxy — use CF "Full" with self-signed, or DNS-01 challenge