Medusa + Mautic + Chatwoot: Self-Hosted Shopify Alternative on Liquid Web (2026)
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
| Tool | Role | Replaces |
|---|---|---|
| Medusa.js v2 | Headless commerce API — orders, products, inventory, customers | Shopify, WooCommerce |
| Mautic | Email marketing automation — segments, campaigns, drip sequences | Klaviyo, Mailchimp |
| Chatwoot | Shared inbox — live chat, email, WhatsApp, Telegram | Gorgias, Intercom, Zendesk |
| PostgreSQL 16 | Medusa + Chatwoot databases | — |
| MariaDB 10.11 | Mautic database (Mautic requires MySQL-compatible DB) | — |
| Redis 7 | Shared queue + cache (Medusa /0, Chatwoot /1) | — |
| Caddy | TLS termination, routing | — |
Architecture:
store.yourdomain.com→ Medusa API (port 9000) — storefront requests and webhooksadmin.yourdomain.com→ Medusa admin panel (port 7001)marketing.yourdomain.com→ Mauticsupport.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.
| Service | Idle RAM | Peak RAM |
|---|---|---|
| medusa (Node.js API) | 380 MB | 700 MB |
| medusa-worker | 120 MB | 280 MB |
| mautic (PHP/Apache) | 420 MB | 700 MB |
| mautic-cron | 100 MB | 200 MB |
| chatwoot (Rails) | 600 MB | 1,200 MB |
| chatwoot-sidekiq | 380 MB | 700 MB |
| db (PostgreSQL 16) | 350 MB | 550 MB |
| mariadb (MariaDB 10.11) | 220 MB | 380 MB |
| redis (shared) | 20 MB | 35 MB |
| caddy | 15 MB | 20 MB |
| Total | 2,605 MB | 4,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 Basic | Klaviyo (5k contacts) | Gorgias Starter | This stack | |
|---|---|---|---|---|
| Monthly cost | $79/mo | $45/mo | $10/mo | ~$33/mo VPS |
| Transaction fees | 2% + gateway | — | — | 0% |
| Email sends | — | 15,000/mo | — | Unlimited |
| Support tickets | — | — | 150/mo | Unlimited |
| Custom checkout | Limited | — | — | Full 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.
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.

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.
