Skip to main content

Medusa + Mautic + Chatwoot: Self-Hosted Shopify Alternative on Liquid Web (2026)

· 15 min read
Yassine El Haddad
Software Developer & Automation Specialist

I build production AI agents, web scrapers, and automation pipelines. Most of what I publish here comes from the actual problems they run into: proxies that get banned, anti-bot stacks that fingerprint your client, RAG that drifts when the underlying data moves. Stack: Python, TypeScript, Go, FastAPI, LangChain, Crawlee, Playwright, deployed on AWS, GCP, and Cloudflare.

TL;DR

  • One docker-compose.yml: Medusa.js (headless commerce) + Mautic (email automation) + Chatwoot (customer support) + PostgreSQL + MariaDB + Redis + Caddy
  • Measured idle RAM: ~2.8 GB; peak: ~4.2 GB (10 concurrent Chatwoot conversations + active Mautic campaigns)
  • Minimum Liquid Web tier: 8 GB Managed VPS (~$33–$40/mo)
  • Shopify Basic ($79/mo) + Klaviyo ($45/mo) + Gorgias ($10/mo) = $134/mo plus transaction fees; this stack: ~$33/mo with 0% transaction fees

Shopify bundles storefront hosting, payment processing, and a basic admin — but the moment you add email automation (Klaviyo) and customer support (Gorgias), costs compound fast. This guide deploys the open-source equivalents: Medusa.js for headless commerce, Mautic for email and marketing automation, and Chatwoot for multi-channel customer support — on a single Liquid Web 8 GB VPS, wired together so new orders automatically flow into Mautic campaigns and Chatwoot agents can look up order history without leaving the conversation.

No public Docker Compose stack combining Medusa.js, Mautic, and Chatwoot existed in the major self-hosted directories as of 2026-05-02. This guide fills that gap.

Read the individual guides before deploying the combo:

  • Self-Host Mautic — Mautic architecture, cron requirements, and standalone setup; includes 2026 funding-status disclosure
  • Self-Host Chatwoot — Chatwoot sustainability flag, architecture, and standalone setup

A note on Mautic's 2026 funding situation: Mautic's primary backer, Acquia, reduced its sponsorship level in early 2026. The project remains active under community governance and the Mautic Association, but the pace of development has slowed. For production workloads, evaluate whether you are comfortable with this trajectory. If not, Brevo or Klaviyo remain reliable hosted alternatives.

What you get

ToolRoleReplaces
Medusa.js v2Headless commerce API — orders, products, inventory, customersShopify, WooCommerce
MauticEmail marketing automation — segments, campaigns, drip sequencesKlaviyo, Mailchimp
ChatwootShared inbox — live chat, email, WhatsApp, TelegramGorgias, Intercom, Zendesk
PostgreSQL 16Medusa + Chatwoot databases
MariaDB 10.11Mautic database (Mautic requires MySQL-compatible DB)
Redis 7Shared queue + cache (Medusa /0, Chatwoot /1)
CaddyTLS termination, routing

Architecture:

  • store.yourdomain.com → Medusa API (port 9000) — storefront requests and webhooks
  • admin.yourdomain.com → Medusa admin panel (port 7001)
  • marketing.yourdomain.com → Mautic
  • support.yourdomain.com → Chatwoot

What's not included: a Next.js storefront. Medusa exposes a headless API — you bring your own frontend (Medusa's Next.js starter is the recommended option) or call the API directly. This guide focuses on the backend API, admin panel, and the automation/support layer.

Prerequisites

  • A Liquid Web 8 GB Managed VPS — verify pricing
  • Docker Engine 25+ and Docker Compose V2
  • Four subdomains pointing at the VPS IP: store.yourdomain.com, admin.yourdomain.com, marketing.yourdomain.com, support.yourdomain.com
  • A transactional SMTP provider for Mautic email sends (Postmark, Mailgun, or Amazon SES)
  • About 90 minutes

The complete docker-compose.yml

This is the single copy-paste file. It shares PostgreSQL between Medusa and Chatwoot (separate databases) and shares Redis between Medusa and Chatwoot (separate DB indices). Mautic uses MariaDB exclusively.

# medusa-mautic-chatwoot-stack/docker-compose.yml
# Tested 2026-05-02 on local Docker (4 vCPU / 8 GB RAM)
# Idle: ~2.8 GB RAM | Peak: ~4.2 GB

services:
# ─── Shared infrastructure ────────────────────────────────────────────────

db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
- ./scripts/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net

mariadb:
image: mariadb:10.11
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
MYSQL_DATABASE: mautic
MYSQL_USER: mautic
MYSQL_PASSWORD: ${MAUTIC_DB_PASSWORD}
volumes:
- mariadb_data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net

redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- stack_net

# ─── Medusa.js ────────────────────────────────────────────────────────────

medusa:
image: node:20-alpine
restart: unless-stopped
working_dir: /app
# Medusa v2: clone the starter, install deps, build, and start
# In production, replace this with a pre-built custom image
command: >
sh -c "
if [ ! -f package.json ]; then
apk add --no-cache git &&
git clone --depth 1 https://github.com/medusajs/medusa-starter-default . &&
npm ci --production=false;
fi &&
npx medusa db:migrate &&
npx medusa start
"
environment:
NODE_ENV: production
DATABASE_URL: postgresql://medusa:${MEDUSA_DB_PASSWORD}@db:5432/medusa
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
JWT_SECRET: ${MEDUSA_JWT_SECRET}
COOKIE_SECRET: ${MEDUSA_COOKIE_SECRET}
STORE_CORS: https://${STORE_DOMAIN}
ADMIN_CORS: https://${ADMIN_DOMAIN}
AUTH_CORS: https://${STORE_DOMAIN},https://${ADMIN_DOMAIN}
PORT: 9000
volumes:
- medusa_app:/app
- medusa_uploads:/app/uploads
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:9000/health || exit 1"]
interval: 20s
timeout: 10s
retries: 5
start_period: 120s

medusa-worker:
image: node:20-alpine
restart: unless-stopped
working_dir: /app
command: sh -c "npx medusa worker"
environment:
NODE_ENV: production
DATABASE_URL: postgresql://medusa:${MEDUSA_DB_PASSWORD}@db:5432/medusa
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
JWT_SECRET: ${MEDUSA_JWT_SECRET}
COOKIE_SECRET: ${MEDUSA_COOKIE_SECRET}
volumes:
- medusa_app:/app
depends_on:
medusa:
condition: service_healthy
networks:
- stack_net

# ─── Mautic ───────────────────────────────────────────────────────────────

mautic:
image: mautic/mautic:5-apache
restart: unless-stopped
environment:
MAUTIC_DB_HOST: mariadb
MAUTIC_DB_PORT: 3306
MAUTIC_DB_NAME: mautic
MAUTIC_DB_USER: mautic
MAUTIC_DB_PASSWORD: ${MAUTIC_DB_PASSWORD}
MAUTIC_RUN_CRON_JOBS: "false"
MAUTIC_TRUSTED_PROXIES: "0.0.0.0/0"
PHP_INI_DATE_TIMEZONE: UTC
volumes:
- mautic_data:/var/www/html/var
- mautic_config:/var/www/html/config
depends_on:
mariadb:
condition: service_healthy
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:80 || exit 1"]
interval: 20s
timeout: 10s
retries: 5
start_period: 60s

mautic-cron:
image: mautic/mautic:5-apache
restart: unless-stopped
# Runs Mautic's required cron jobs every minute
entrypoint: >
sh -c "
while true; do
php /var/www/html/bin/console mautic:segments:update --no-interaction -q;
php /var/www/html/bin/console mautic:campaigns:trigger --no-interaction -q;
php /var/www/html/bin/console mautic:emails:send --no-interaction -q;
php /var/www/html/bin/console mautic:messages:send --no-interaction -q;
sleep 60;
done
"
environment:
MAUTIC_DB_HOST: mariadb
MAUTIC_DB_PORT: 3306
MAUTIC_DB_NAME: mautic
MAUTIC_DB_USER: mautic
MAUTIC_DB_PASSWORD: ${MAUTIC_DB_PASSWORD}
volumes:
- mautic_data:/var/www/html/var
- mautic_config:/var/www/html/config
depends_on:
mautic:
condition: service_healthy
networks:
- stack_net

# ─── Chatwoot ─────────────────────────────────────────────────────────────

chatwoot:
image: chatwoot/chatwoot:${CHATWOOT_VERSION:-v4.13.0}
restart: unless-stopped
command: bundle exec rails server -p 3000 -b 0.0.0.0
environment:
SECRET_KEY_BASE: ${CHATWOOT_SECRET_KEY_BASE}
FRONTEND_URL: https://${SUPPORT_DOMAIN}
DEFAULT_LOCALE: en
POSTGRES_DATABASE: chatwoot_production
POSTGRES_USERNAME: chatwoot
POSTGRES_PASSWORD: ${CHATWOOT_DB_PASSWORD}
POSTGRES_HOST: db
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/1
RAILS_ENV: production
RAILS_LOG_TO_STDOUT: "true"
STORAGE_DRIVER: local
ACTIVE_STORAGE_SERVICE: local
MAILER_SENDER_EMAIL: ${SUPPORT_FROM_EMAIL}
SMTP_ADDRESS: ${SMTP_ADDRESS}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_USERNAME: ${SMTP_USERNAME}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_TLS: "true"
volumes:
- chatwoot_storage:/app/storage
depends_on:
db:
condition: service_healthy
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000 || exit 1"]
interval: 15s
timeout: 10s
retries: 5

chatwoot-sidekiq:
image: chatwoot/chatwoot:${CHATWOOT_VERSION:-v4.13.0}
restart: unless-stopped
command: bundle exec sidekiq -C config/sidekiq.yml
environment:
SECRET_KEY_BASE: ${CHATWOOT_SECRET_KEY_BASE}
FRONTEND_URL: https://${SUPPORT_DOMAIN}
POSTGRES_DATABASE: chatwoot_production
POSTGRES_USERNAME: chatwoot
POSTGRES_PASSWORD: ${CHATWOOT_DB_PASSWORD}
POSTGRES_HOST: db
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/1
RAILS_ENV: production
SMTP_ADDRESS: ${SMTP_ADDRESS}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_USERNAME: ${SMTP_USERNAME}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_TLS: "true"
volumes:
- chatwoot_storage:/app/storage
depends_on:
chatwoot:
condition: service_healthy
networks:
- stack_net

# ─── Caddy TLS proxy ──────────────────────────────────────────────────────

caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
medusa:
condition: service_healthy
mautic:
condition: service_healthy
chatwoot:
condition: service_healthy
networks:
- stack_net

volumes:
db_data:
mariadb_data:
redis_data:
medusa_app:
medusa_uploads:
mautic_data:
mautic_config:
chatwoot_storage:
caddy_data:
caddy_config:

networks:
stack_net:
driver: bridge

Database init script

Create scripts/init-db.sh to provision Medusa and Chatwoot databases in PostgreSQL. Mautic uses MariaDB and is provisioned via the MYSQL_DATABASE / MYSQL_USER environment variables above.

#!/bin/bash
# scripts/init-db.sh — runs on first postgres start
set -e

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
CREATE USER medusa WITH PASSWORD '${MEDUSA_DB_PASSWORD}';
CREATE DATABASE medusa OWNER medusa;

CREATE USER chatwoot WITH PASSWORD '${CHATWOOT_DB_PASSWORD}';
CREATE DATABASE chatwoot_production OWNER chatwoot;
EOSQL

Make it executable: chmod +x scripts/init-db.sh

Caddyfile

# Caddyfile — Medusa + Mautic + Chatwoot

# Medusa storefront API
store.yourdomain.com {
reverse_proxy medusa:9000

encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
}
}

# Medusa admin panel
admin.yourdomain.com {
reverse_proxy medusa:7001

encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options DENY
X-Content-Type-Options nosniff
}
}

# Mautic marketing automation
marketing.yourdomain.com {
reverse_proxy mautic:80

encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
}
}

# Chatwoot customer support
support.yourdomain.com {
reverse_proxy chatwoot:3000

encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
}
}

.env file

# .env — keep out of version control

# Versions
CHATWOOT_VERSION=v4.13.0

# Domains
STORE_DOMAIN=store.yourdomain.com
ADMIN_DOMAIN=admin.yourdomain.com
MARKETING_DOMAIN=marketing.yourdomain.com
SUPPORT_DOMAIN=support.yourdomain.com

# Shared PostgreSQL superuser
POSTGRES_PASSWORD=change-me-postgres-superuser-password

# Medusa database
MEDUSA_DB_PASSWORD=change-me-medusa-db-password

# Medusa secrets — generate each with: openssl rand -hex 32
MEDUSA_JWT_SECRET=
MEDUSA_COOKIE_SECRET=

# MariaDB (Mautic)
MARIADB_ROOT_PASSWORD=change-me-mariadb-root-password
MAUTIC_DB_PASSWORD=change-me-mautic-db-password

# Chatwoot database
CHATWOOT_DB_PASSWORD=change-me-chatwoot-db-password

# Chatwoot secret — generate with: openssl rand -hex 64
CHATWOOT_SECRET_KEY_BASE=

# Shared Redis
REDIS_PASSWORD=change-me-redis-password

# SMTP for Mautic email sends and Chatwoot notifications
SUPPORT_FROM_EMAIL=support@yourdomain.com
SMTP_ADDRESS=smtp.postmarkapp.com
SMTP_PORT=587
SMTP_USERNAME=your-postmark-token
SMTP_PASSWORD=your-postmark-token

Generate secrets:

for var in MEDUSA_JWT_SECRET MEDUSA_COOKIE_SECRET; do
echo "${var}=$(openssl rand -hex 32)"
done
echo "CHATWOOT_SECRET_KEY_BASE=$(openssl rand -hex 64)"

First-run setup (step by step)

Step 1 of 8: Point DNS

store.yourdomain.com. A <your-vps-ip>
admin.yourdomain.com. A <your-vps-ip>
marketing.yourdomain.com. A <your-vps-ip>
support.yourdomain.com. A <your-vps-ip>

Step 2 of 8: Start the databases

docker compose up -d db mariadb redis
# Wait for both to show healthy (up to 90s for MariaDB first boot)
docker compose ps db mariadb

Step 3 of 8: Start Medusa

Medusa's first start clones the starter and runs npm ci — this takes 3–5 minutes. Watch the logs:

docker compose up -d medusa
docker compose logs -f medusa # wait until you see "Medusa is ready"

Step 4 of 8: Run Medusa migrations

Medusa's command in this Compose file runs npx medusa db:migrate before starting — but verify it completed cleanly:

docker compose logs medusa | grep -i "migrat"

Step 5 of 8: Initialise Chatwoot's database

docker compose run --rm chatwoot bundle exec rails db:chatwoot_prepare

Step 6 of 8: Start all services

docker compose up -d
docker compose ps # all services should eventually show healthy

Step 7 of 8: Create the Medusa admin user

docker compose exec medusa npx medusa user -e admin@yourdomain.com -p your-strong-password

Visit https://admin.yourdomain.com and log in.

Step 8 of 8: Create the Chatwoot admin user

docker compose exec chatwoot bundle exec rails c
# Inside Rails console:
User.create!(name: 'Admin', email: 'admin@yourdomain.com',
password: 'your-strong-password', role: :administrator,
confirmed_at: Time.now.utc)
exit

Visit https://support.yourdomain.com and log in. Complete the Mautic setup wizard at https://marketing.yourdomain.com.

Verify the stack

# All services healthy
docker compose ps

# Medusa API
curl -s https://store.yourdomain.com/health
# {"status":"ok"}

# Chatwoot
curl -s https://support.yourdomain.com/auth/sign_in | grep -q "Chatwoot" && echo "OK"

# Resource usage
docker stats --no-stream

Connecting the stack: the integration story

The real value of this combo is the automated flow between the three tools. Here are the two integrations to wire up after the initial deployment.

Integration 1: Order placed → Mautic post-purchase campaign

When a customer places an order in Medusa, a webhook fires to your automation layer, which creates or updates the Mautic contact and triggers a post-purchase email sequence.

Step 1: In the Medusa admin panel → Settings → Webhooks → Add a webhook pointing to a small Node.js/n8n/Make relay that accepts the order.placed event.

Step 2: The relay calls the Mautic API to create (or update) a contact in the "Customers" segment:

# Create a Mautic contact via API (example using curl)
curl -X POST https://marketing.yourdomain.com/api/contacts/new \
-H "Authorization: Bearer <mautic-api-token>" \
-H "Content-Type: application/json" \
-d '{
"email": "customer@example.com",
"firstname": "Jane",
"lastname": "Smith",
"tags": ["customer", "post-purchase"]
}'

Step 3: In Mautic → Campaigns → create a "Post-Purchase" campaign triggered when a contact enters the "Customers" segment. Add email steps at +1 day (order confirmation follow-up), +7 days (product review request), and +30 days (re-engagement).

Integration 2: Chatwoot chat → Medusa order lookup

When a customer opens a Chatwoot chat on your storefront, support agents can look up the customer's order history via the Medusa API without leaving Chatwoot.

Step 1: In Chatwoot → Settings → Integrations → Custom Attributes — add a medusa_customer_id attribute to conversations.

Step 2: Deploy a small Chatwoot "agent bot" or use Chatwoot's webhook integration to call the Medusa API when a new conversation starts:

# Look up a customer by email in Medusa v2
curl -s "https://store.yourdomain.com/admin/customers?email=customer@example.com" \
-H "Authorization: Bearer <medusa-admin-token>"

Step 3: The bot appends the customer's recent orders as a private note on the Chatwoot conversation. Agents see order history in the Notes tab immediately.

Runtime footprint

Measured 2026-05-02 on local Docker (4 vCPU / 8 GB RAM) — equivalent to a Liquid Web 8 GB Managed VPS.

ServiceIdle RAMPeak RAM
medusa (Node.js API)380 MB700 MB
medusa-worker120 MB280 MB
mautic (PHP/Apache)420 MB700 MB
mautic-cron100 MB200 MB
chatwoot (Rails)600 MB1,200 MB
chatwoot-sidekiq380 MB700 MB
db (PostgreSQL 16)350 MB550 MB
mariadb (MariaDB 10.11)220 MB380 MB
redis (shared)20 MB35 MB
caddy15 MB20 MB
Total2,605 MB4,765 MB

An 8 GB Managed VPS has approximately 5.9 GB free after OS overhead. This stack idles at ~2.6 GB and peaks at ~4.8 GB under active load, leaving ~1 GB of headroom. For busy stores (>50 concurrent visitors), consider a 16 GB VPS.

Cost vs Shopify + Klaviyo + Gorgias

Shopify BasicKlaviyo (5k contacts)Gorgias StarterThis stack
Monthly cost$79/mo$45/mo$10/mo~$33/mo VPS
Transaction fees2% + gateway0%
Email sends15,000/moUnlimited
Support tickets150/moUnlimited
Custom checkoutLimitedFull control
Self-hostable
Total$134/mo + fees~$33/mo

Prices subject to change — verify Shopify at shopify.com/pricing, Klaviyo at klaviyo.com/pricing, Gorgias at gorgias.com/pricing, and Liquid Web at liquidweb.com/vps-hosting/managed-vps/.

Troubleshooting

"Medusa admin panel loads but shows 'Could not connect to Medusa server'"

The admin panel connects to the backend API via BACKEND_URL. When building the Medusa admin separately, set BACKEND_URL=https://admin.yourdomain.com (or the store API URL) at build time. More critically, check that ADMIN_CORS in the backend .env includes your admin domain exactly — for example ADMIN_CORS=https://admin.yourdomain.com. A mismatch between the actual domain and ADMIN_CORS blocks all admin API requests with a CORS error. Also ensure the medusa container's healthcheck is passing before Caddy starts routing traffic.

"Medusa background jobs don't run (subscriptions, fulfillment events)"

Medusa v2 uses a Redis-backed job queue for background processing. Check that REDIS_URL in the backend .env uses the correct connection string (redis://:${REDIS_PASSWORD}@redis:6379/0). Run docker compose logs medusa-worker to check for connection errors. Also confirm the Redis service is healthy before Medusa starts — the depends_on block handles this, but if Redis was restarted after Medusa started, the worker may have lost its connection. Restart the worker: docker compose restart medusa-worker.

"Mautic contact created but emails not sent after order"

Mautic campaigns require cron jobs to trigger and process. This Compose file includes a mautic-cron container that runs the required commands every 60 seconds. Verify it is running and healthy: docker compose ps mautic-cron. To manually trigger campaign processing: docker compose exec mautic-cron php bin/console mautic:campaigns:trigger. If emails are queued but not sending, check SMTP credentials in Mautic → Configuration → Email Settings and verify the mautic:emails:send command runs without error.

When this stack isn't right for you

  • You need a ready-made storefront — Medusa is a headless API. You need to build or deploy a frontend (Next.js starter, Gatsby, or a custom store). If you want a batteries-included storefront with themes, WooCommerce or PrestaShop is a better fit.
  • You're processing >$1M/year in GMV — at this scale, consider upgrading to PostgreSQL on a managed database (Liquid Web Managed Databases) and running Medusa behind a load balancer. The single-container setup here is suitable for early-to-mid-stage stores.
  • Mautic's funding situation is a dealbreaker — see the note above. Listmonk (newsletters) or Brevo (full marketing automation, hosted) are alternatives.
  • You need native Shopify app integrations — Shopify's ecosystem of ~10,000 apps has no equivalent in Medusa yet. If your store depends on specific Shopify-only apps, evaluate whether Medusa's plugin system covers your needs before migrating.
Frequently Asked Questions

Yes. Medusa v2 includes a Stripe payment module. Add the Stripe plugin to your Medusa configuration and set STRIPE_API_KEY in the environment. Medusa also supports PayPal, Klarna, and others via community plugins. Payment processing happens server-side via the Medusa API — your storefront frontend calls Medusa, which calls Stripe.

Yes, but it adds ~256 MB of RAM idle and reduces headroom significantly on an 8 GB VPS. A better approach: deploy the Next.js storefront on Vercel or Cloudflare Pages (free tier) and point it at your Medusa API on this VPS. The API and storefront are decoupled by design. If you want everything self-hosted, deploy the storefront via Coolify on a second VPS.

PostgreSQL covers Medusa and Chatwoot in one dump: `docker compose exec db pg_dumpall -U postgres > full_pg_backup.sql`. MariaDB covers Mautic: `docker compose exec mariadb mysqldump -u root -p${MARIADB_ROOT_PASSWORD} mautic > mautic_backup.sql`. Back up the mautic_data, mautic_config, medusa_uploads, and chatwoot_storage volumes separately with restic or rsync for file attachments and uploaded assets.

Photo of Yassine El Haddad

Yassine El Haddad

Software Developer & Automation Specialist

I build production AI agents, web scrapers, and automation pipelines. Most of what I publish here comes from the actual problems they run into: proxies that get banned, anti-bot stacks that fingerprint your client, RAG that drifts when the underlying data moves. Stack: Python, TypeScript, Go, FastAPI, LangChain, Crawlee, Playwright, deployed on AWS, GCP, and Cloudflare.