Skip to main content

Coolify + Authentik + Plausible + Postal: The Self-Hosted Meta Stack (2026)

· 19 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 deploys Authentik (SSO) + Plausible (analytics) + Postal (SMTP) + shared PostgreSQL + MySQL + ClickHouse + RabbitMQ + Redis + Caddy; Coolify installs separately via its own script
  • Measured idle RAM: ~4.6 GB total across the host (our Compose stack + Coolify's stack)
  • Minimum Liquid Web tier: 16 GB Managed VPS (~$50/mo) — the 16 GB gives comfortable room for apps you deploy through Coolify
  • Vercel Pro ($20/mo) + Auth0 Essentials ($23/mo) + Postmark ($15/mo) = $58/mo just for foundations; this stack: ~$50/mo with unlimited apps, SSO, pageviews, and email

This is the "meta stack" — the infrastructure layer you deploy before anything else. Every other self-hosted app you run needs a place to live (Coolify), a way to log users in (Authentik), a way to measure traffic (Plausible), and a way to send email (Postal, the open-source SMTP platform). This guide wires all four together on a single Liquid Web 16 GB VPS as a reusable foundation.

Deploy this stack once. Then every new app you spin up via Coolify inherits SSO through Authentik, analytics through a Plausible snippet, and transactional email through Postal's SMTP endpoint — without paying per-seat, per-pageview, or per-email fees again.

Read the individual guides before deploying the combo:

A note on Coolify and Caddy port conflicts: Coolify installs its own Traefik reverse proxy that binds ports 80 and 443 on the host. Our Compose file uses Caddy for Authentik, Plausible, and Postal. The simplest solution — and the one this guide uses — is to register Authentik, Plausible, and Postal as applications inside Coolify's UI, letting Coolify's Traefik handle TLS for all four tools. See the Port conflict troubleshooting entry below for the full explanation.

What you get

ToolRoleReplaces
CoolifySelf-hosted PaaS — deploy apps from Git, manage databases, env varsVercel, Railway, Render, Heroku
AuthentikIdentity provider — SSO, OIDC, SAML, MFA, user managementAuth0, Okta, Keycloak
PlausiblePrivacy-first web analytics — pageviews, sessions, funnelsGoogle Analytics 4
PostalOpen-source mail server — transactional SMTP, delivery tracking, webhooksPostmark, Mailgun, SendGrid
PostgreSQL 16Plausible + Authentik databases
MySQL 8.0Postal database
ClickHousePlausible event store (required)
RabbitMQPostal message queue (required)
Redis 7Shared queue + cache (Plausible /0, Authentik /1, Postal /2)
CaddyOptional TLS proxy — or use Coolify's built-in Traefik

Architecture:

  • deploy.yourdomain.com → Coolify UI (port 8000) — managed by Coolify's Traefik
  • sso.yourdomain.com → Authentik (port 9000)
  • analytics.yourdomain.com → Plausible (port 8000 internal)
  • mail.yourdomain.com → Postal web UI (port 5000 internal)

What's not included: a storefront, wiki, CRM, or any of the apps that will run on top of this foundation. This stack is the platform layer only.

Prerequisites

  • A Liquid Web 16 GB Managed VPS — verify pricing
  • Docker Engine 25+ and Docker Compose V2
  • Four subdomains pointing at the VPS IP: deploy.yourdomain.com, sso.yourdomain.com, analytics.yourdomain.com, mail.yourdomain.com
  • Postal requires proper email DNS: MX record, SPF, DKIM, and PTR (rDNS) — your VPS provider must allow PTR record configuration
  • About 2 hours (Postal DNS propagation adds time)

Installation order

The order matters. Coolify must be running first because it manages the Traefik reverse proxy that all other services will route through.

  1. Install Coolify — via its own script; it manages its own Docker environment and Traefik
  2. Deploy Authentik, Plausible, Postal — via our docker-compose.yml
  3. Register the three apps in Coolify — so Traefik picks up their domains and issues TLS certificates
  4. Initialize Postal — run the postal initialize command and configure DNS
  5. Configure Authentik OIDC — connect Coolify login to Authentik SSO

Step 1: Install Coolify

Coolify uses its own installer — it sets up its own Docker Compose stack, internal databases, and Traefik reverse proxy. Run as root on your VPS:

curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash

The installer takes 2–5 minutes. Verify it is running:

systemctl status coolify
curl -s http://localhost:8000/api/health

Complete the Coolify onboarding wizard at http://<your-vps-ip>:8000. At this point Traefik is managing ports 80 and 443 on the host. Do not run a second reverse proxy on those ports — we will register our services through Coolify's UI instead.

Step 2: Deploy Authentik + Plausible + Postal

Create your project directory and place the files below into it.

meta-stack/
├── docker-compose.yml
├── .env
└── scripts/
└── init-pg.sh

The complete docker-compose.yml

This file manages Authentik, Plausible, Postal, and all their dependencies. It does not include Caddy — Coolify's Traefik handles TLS routing. Coolify is not in this file.

# meta-stack/docker-compose.yml
# Tested 2026-05-02 on local Docker (4 vCPU / 16 GB RAM)
# Idle (this stack): ~3.8 GB | Total host idle with Coolify: ~4.6 GB
# Coolify installs separately via its own installer script

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-pg.sh:/docker-entrypoint-initdb.d/init-pg.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net

mysql:
image: mysql:8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: postal
MYSQL_USER: postal
MYSQL_PASSWORD: ${POSTAL_DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
networks:
- stack_net

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

clickhouse:
image: clickhouse/clickhouse-server:24.3-alpine
restart: unless-stopped
environment:
CLICKHOUSE_DB: plausible_events_db
CLICKHOUSE_USER: plausible
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1"
CLICKHOUSE_MAX_SERVER_MEMORY_USAGE: "4000000000"
volumes:
- clickhouse_data:/var/lib/clickhouse
- clickhouse_logs:/var/log/clickhouse-server
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8123/ping || exit 1"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net

rabbitmq:
image: rabbitmq:3.12-management-alpine
restart: unless-stopped
environment:
RABBITMQ_DEFAULT_USER: postal
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
RABBITMQ_DEFAULT_VHOST: postal
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net

# ─── Authentik ────────────────────────────────────────────────────────────

authentik-server:
image: ghcr.io/goauthentik/server:${AUTHENTIK_VERSION:-2024.12.3}
restart: unless-stopped
command: server
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_REDIS__PASSWORD: ${REDIS_PASSWORD}
AUTHENTIK_REDIS__DB: "1"
AUTHENTIK_POSTGRESQL__HOST: db
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
AUTHENTIK_EMAIL__HOST: ${SMTP_ADDRESS}
AUTHENTIK_EMAIL__PORT: ${SMTP_PORT:-587}
AUTHENTIK_EMAIL__USERNAME: ${SMTP_USERNAME}
AUTHENTIK_EMAIL__PASSWORD: ${SMTP_PASSWORD}
AUTHENTIK_EMAIL__USE_TLS: "true"
AUTHENTIK_EMAIL__FROM: ${AUTHENTIK_FROM_EMAIL}
volumes:
- authentik_media:/media
- authentik_templates:/templates
depends_on:
db:
condition: service_healthy
networks:
- stack_net
ports:
- "9000:9000"
healthcheck:
test: ["CMD-SHELL", "ak healthcheck"]
interval: 20s
timeout: 10s
retries: 5

authentik-worker:
image: ghcr.io/goauthentik/server:${AUTHENTIK_VERSION:-2024.12.3}
restart: unless-stopped
command: worker
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_REDIS__PASSWORD: ${REDIS_PASSWORD}
AUTHENTIK_REDIS__DB: "1"
AUTHENTIK_POSTGRESQL__HOST: db
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_EMAIL__HOST: ${SMTP_ADDRESS}
AUTHENTIK_EMAIL__PORT: ${SMTP_PORT:-587}
AUTHENTIK_EMAIL__USERNAME: ${SMTP_USERNAME}
AUTHENTIK_EMAIL__PASSWORD: ${SMTP_PASSWORD}
AUTHENTIK_EMAIL__USE_TLS: "true"
AUTHENTIK_EMAIL__FROM: ${AUTHENTIK_FROM_EMAIL}
volumes:
- authentik_media:/media
depends_on:
authentik-server:
condition: service_healthy
networks:
- stack_net

# ─── Plausible Analytics ──────────────────────────────────────────────────

plausible:
image: ghcr.io/plausible/community-edition:${PLAUSIBLE_VERSION:-v2.1.4}
restart: unless-stopped
command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
environment:
BASE_URL: https://${ANALYTICS_DOMAIN}
SECRET_KEY_BASE: ${PLAUSIBLE_SECRET_KEY_BASE}
DATABASE_URL: postgresql://plausible:${PLAUSIBLE_DB_PASSWORD}@db:5432/plausible
CLICKHOUSE_DATABASE_URL: http://plausible:${CLICKHOUSE_PASSWORD}@clickhouse:8123/plausible_events_db
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
DISABLE_REGISTRATION: "true"
# Remove DISABLE_REGISTRATION once you have created your admin account
depends_on:
db:
condition: service_healthy
clickhouse:
condition: service_healthy
networks:
- stack_net
ports:
- "8888:8000"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8000/api/health || exit 1"]
interval: 20s
timeout: 10s
retries: 5
start_period: 30s

# ─── Postal ───────────────────────────────────────────────────────────────

postal:
image: ghcr.io/postalserver/postal:${POSTAL_VERSION:-3.3.4}
restart: unless-stopped
command: postal web-server
environment:
POSTAL_DB_HOST: mysql
POSTAL_DB_PORT: 3306
POSTAL_DB_NAME: postal
POSTAL_DB_USERNAME: postal
POSTAL_DB_PASSWORD: ${POSTAL_DB_PASSWORD}
POSTAL_RABBITMQ_HOST: rabbitmq
POSTAL_RABBITMQ_USER: postal
POSTAL_RABBITMQ_PASS: ${RABBITMQ_PASSWORD}
POSTAL_RABBITMQ_VHOST: postal
POSTAL_REDIS_HOST: redis
POSTAL_REDIS_PASSWORD: ${REDIS_PASSWORD}
POSTAL_REDIS_DB: "2"
POSTAL_SECRET_KEY: ${POSTAL_SECRET_KEY}
RAILS_ENV: production
volumes:
- postal_config:/config
depends_on:
mysql:
condition: service_healthy
rabbitmq:
condition: service_healthy
networks:
- stack_net
ports:
- "5000:5000"

postal-worker:
image: ghcr.io/postalserver/postal:${POSTAL_VERSION:-3.3.4}
restart: unless-stopped
command: postal worker
environment:
POSTAL_DB_HOST: mysql
POSTAL_DB_PORT: 3306
POSTAL_DB_NAME: postal
POSTAL_DB_USERNAME: postal
POSTAL_DB_PASSWORD: ${POSTAL_DB_PASSWORD}
POSTAL_RABBITMQ_HOST: rabbitmq
POSTAL_RABBITMQ_USER: postal
POSTAL_RABBITMQ_PASS: ${RABBITMQ_PASSWORD}
POSTAL_RABBITMQ_VHOST: postal
POSTAL_REDIS_HOST: redis
POSTAL_REDIS_PASSWORD: ${REDIS_PASSWORD}
POSTAL_REDIS_DB: "2"
POSTAL_SECRET_KEY: ${POSTAL_SECRET_KEY}
RAILS_ENV: production
volumes:
- postal_config:/config
depends_on:
postal:
condition: service_started
mysql:
condition: service_healthy
networks:
- stack_net

postal-smtp:
image: ghcr.io/postalserver/postal:${POSTAL_VERSION:-3.3.4}
restart: unless-stopped
command: postal smtp-server
ports:
- "25:25"
- "587:587"
environment:
POSTAL_DB_HOST: mysql
POSTAL_DB_PORT: 3306
POSTAL_DB_NAME: postal
POSTAL_DB_USERNAME: postal
POSTAL_DB_PASSWORD: ${POSTAL_DB_PASSWORD}
POSTAL_RABBITMQ_HOST: rabbitmq
POSTAL_RABBITMQ_USER: postal
POSTAL_RABBITMQ_PASS: ${RABBITMQ_PASSWORD}
POSTAL_RABBITMQ_VHOST: postal
POSTAL_REDIS_HOST: redis
POSTAL_REDIS_PASSWORD: ${REDIS_PASSWORD}
POSTAL_REDIS_DB: "2"
POSTAL_SECRET_KEY: ${POSTAL_SECRET_KEY}
RAILS_ENV: production
volumes:
- postal_config:/config
depends_on:
postal:
condition: service_started
networks:
- stack_net

volumes:
db_data:
mysql_data:
redis_data:
clickhouse_data:
clickhouse_logs:
rabbitmq_data:
authentik_media:
authentik_templates:
postal_config:

networks:
stack_net:
driver: bridge

PostgreSQL init script

Create scripts/init-pg.sh to provision Authentik and Plausible databases. MySQL handles Postal via the MYSQL_DATABASE / MYSQL_USER environment variables above.

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

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

CREATE USER plausible WITH PASSWORD '${PLAUSIBLE_DB_PASSWORD}';
CREATE DATABASE plausible OWNER plausible;
EOSQL

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

.env file

# .env — keep out of version control

# Versions
AUTHENTIK_VERSION=2024.12.3
PLAUSIBLE_VERSION=v2.1.4
POSTAL_VERSION=3.3.4

# Domains
SSO_DOMAIN=sso.yourdomain.com
ANALYTICS_DOMAIN=analytics.yourdomain.com
MAIL_DOMAIN=mail.yourdomain.com

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

# Authentik
AUTHENTIK_DB_PASSWORD=change-me-authentik-db-password
# Generate with: openssl rand -hex 32
AUTHENTIK_SECRET_KEY=

# SMTP for Authentik password resets (use an external relay before Postal is ready)
AUTHENTIK_FROM_EMAIL=sso@yourdomain.com
SMTP_ADDRESS=smtp.postmarkapp.com
SMTP_PORT=587
SMTP_USERNAME=your-postmark-token
SMTP_PASSWORD=your-postmark-token

# Plausible
PLAUSIBLE_DB_PASSWORD=change-me-plausible-db-password
# Generate with: openssl rand -base64 64 | tr -d '\n'
PLAUSIBLE_SECRET_KEY_BASE=

# ClickHouse
CLICKHOUSE_PASSWORD=change-me-clickhouse-password

# MySQL (Postal)
MYSQL_ROOT_PASSWORD=change-me-mysql-root-password
POSTAL_DB_PASSWORD=change-me-postal-db-password

# Postal
# Generate with: openssl rand -hex 32
POSTAL_SECRET_KEY=

# RabbitMQ
RABBITMQ_PASSWORD=change-me-rabbitmq-password

# Redis
REDIS_PASSWORD=change-me-redis-password

Generate secrets:

echo "AUTHENTIK_SECRET_KEY=$(openssl rand -hex 32)"
echo "PLAUSIBLE_SECRET_KEY_BASE=$(openssl rand -base64 64 | tr -d '\n')"
echo "POSTAL_SECRET_KEY=$(openssl rand -hex 32)"

Step 3: Start the infrastructure layer

Step 3a: Point DNS

deploy.yourdomain.com. A <your-vps-ip>
sso.yourdomain.com. A <your-vps-ip>
analytics.yourdomain.com. A <your-vps-ip>
mail.yourdomain.com. A <your-vps-ip>

Postal also needs additional DNS records (see Step 5).

Step 3b: Start databases and queues

docker compose up -d db mysql redis clickhouse rabbitmq
# Wait for all to show healthy (ClickHouse and MySQL take up to 90s on first boot)
docker compose ps

Step 3c: Start Authentik

docker compose up -d authentik-server authentik-worker
docker compose logs -f authentik-server # wait until you see "Starting server"

Step 3d: Start Plausible

docker compose up -d plausible
docker compose logs -f plausible # wait for migrations to complete and server to start

Step 3e: Initialize and start Postal

Postal requires a one-time postal initialize command to create its database schema and generate signing keys. Run this before starting the Postal services:

docker compose run --rm postal postal initialize

If this fails with a MySQL connection error, MySQL has not finished its initial setup. Wait 30 seconds and retry. Once postal initialize completes:

docker compose up -d postal postal-worker postal-smtp

Create the first Postal admin user:

docker compose exec postal postal make-user
# Follow the prompts to set email and password

Rather than running a second reverse proxy on ports 80/443 alongside Coolify's Traefik, register Authentik, Plausible, and Postal as applications within Coolify's UI. Coolify's Traefik will handle TLS certificate issuance via Let's Encrypt for all domains.

For each service (Authentik, Plausible, Postal):

  1. In Coolify → ProjectsNew ApplicationExisting Docker Compose
  2. Point to your meta-stack/ directory or paste the service definition
  3. Set the Domain to the relevant subdomain (sso.yourdomain.com, etc.)
  4. Set the Port to the service's exposed host port (9000 for Authentik, 8888 for Plausible, 5000 for Postal web UI)
  5. Enable SSL — Coolify automatically issues Let's Encrypt certificates via Traefik
  6. Save and deploy

Coolify will configure Traefik routing rules for each domain and renew TLS certificates automatically.

Alternative: If you prefer to manage TLS yourself, keep Caddy as a standalone container but move it to a different host port (e.g., 8080/8443) and proxy Coolify's Traefik to it. Option (b) above (using Coolify's Traefik directly) is simpler and the approach this guide recommends.

Step 5: Configure Postal DNS

Postal requires proper email DNS to achieve good deliverability. In your DNS provider:

Required records:

# MX record — point to your Postal SMTP server
yourdomain.com. MX 10 mail.yourdomain.com.

# SPF — authorize your VPS IP to send mail
yourdomain.com. TXT "v=spf1 ip4:<your-vps-ip> ~all"

# PTR / rDNS — set in your VPS provider's control panel
<your-vps-ip> PTR mail.yourdomain.com.

# DKIM — Postal generates this key after postal initialize
# In Postal web UI → Mail Servers → [your server] → Domains → [your domain] → DKIM
# Copy the DKIM TXT record and add it to your DNS

In Postal web UI (https://mail.yourdomain.com):

  1. Log in with the admin user created in Step 3e
  2. Create a Mail Server for your organization
  3. Add your domain under the mail server → Domains
  4. Copy and add the DKIM TXT record to your DNS
  5. Click Check DNS — all records must show green before Postal will send mail

Step 6: Configure Authentik OIDC for Coolify

Once Authentik is running, connect Coolify's login to it so your team logs into the PaaS with their SSO credentials.

Step 6a: Create the Coolify OIDC provider in Authentik

  1. In Authentik → Admin InterfaceApplicationsProvidersCreate
  2. Select OAuth2/OpenID Provider
  3. Configure:
    • Name: coolify-provider
    • Client type: Confidential
    • Redirect URIs: https://deploy.yourdomain.com/auth/source/oauth/callback/authentik
    • Scopes: openid, profile, email
  4. Click Finish — copy the Client ID and Client Secret
  5. Go to ApplicationsCreate:
    • Name: Coolify PaaS
    • Slug: coolify
    • Provider: coolify-provider

Step 6b: Enable OIDC in Coolify

  1. Open https://deploy.yourdomain.comSettingsAuthenticationOAuth / OIDC
  2. Enable the provider and fill in:
    • Client ID: (from step 6a)
    • Client Secret: (from step 6a)
    • Issuer URL: https://sso.yourdomain.com/application/o/coolify/
    • Redirect URI: https://deploy.yourdomain.com/auth/source/oauth/callback/authentik
  3. Save. The Coolify login page now shows a Sign in with Authentik button.

Step 7: Add Plausible tracking and Postal SMTP to your apps

Adding Plausible to any app deployed via Coolify:

Add this snippet to each app's HTML <head>:

<script defer data-domain="yourappdomain.com"
src="https://analytics.yourdomain.com/js/script.js"></script>

That's it. Plausible is cookieless and GDPR-compliant by default — no consent banner required.

Using Postal as your SMTP relay:

In any app that sends transactional email, set:

SMTP_HOST=mail.yourdomain.com
SMTP_PORT=587
SMTP_USERNAME=<credential-from-postal-web-ui>
SMTP_PASSWORD=<credential-from-postal-web-ui>
SMTP_FROM=noreply@yourdomain.com

Generate SMTP credentials in Postal web UI → Mail Servers → [your server] → Credentials → New API Credential (type: SMTP).

Verify the stack

# All Compose services
docker compose ps

# Authentik
curl -s https://sso.yourdomain.com/api/v3/root/config/ | grep -q "authentik" && echo "Authentik OK"

# Plausible health
curl -s https://analytics.yourdomain.com/api/health
# {"postgres":"ok","clickhouse":"ok"}

# Postal web UI
curl -s https://mail.yourdomain.com | grep -q "Postal" && echo "Postal OK"

# Coolify health
curl -s https://deploy.yourdomain.com/api/health

# Full host resource usage
docker stats --no-stream

Runtime footprint

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

ServiceIdle RAMPeak RAMManaged by
authentik-server350 MB600 MBour Compose
authentik-worker250 MB400 MBour Compose
plausible200 MB400 MBour Compose
clickhouse1,000 MB2,000 MBour Compose
postal (web)300 MB550 MBour Compose
postal-worker280 MB500 MBour Compose
postal-smtp250 MB450 MBour Compose
db (PostgreSQL 16)400 MB650 MBour Compose
mysql (MySQL 8.0)512 MB800 MBour Compose
rabbitmq256 MB400 MBour Compose
redis (shared)50 MB80 MBour Compose
Subtotal (our stack)3,848 MB6,830 MB
Coolify stack (server + db + Traefik)~770 MB~1,200 MBCoolify installer
Total host~4,618 MB~8,030 MB

A 16 GB Managed VPS has approximately 12 GB free after OS overhead. This meta stack idles at ~4.6 GB, leaving ~7 GB of headroom for apps you deploy through Coolify. If you plan to run many always-on services, a 32 GB VPS gives comfortable room to grow.

Cost vs Vercel + Auth0 + Google Analytics + Postmark

Vercel ProAuth0 EssentialsGoogle AnalyticsPostmarkThis stack
Monthly cost$20/mo$23/mo$0$15/mo~$50/mo VPS
Apps hostedUnlimitedUnlimited (Coolify)
Users (SSO)1,000 MAUUnlimited
PageviewsUnlimitedUnlimited
Emails/mo10,000Unlimited
PrivacyTracks usersCookieless (Plausible)
Self-hostable
Total$58/mo~$50/mo

Prices subject to change — verify Vercel at vercel.com/pricing, Auth0 at auth0.com/pricing, Postmark at postmarkapp.com/pricing, and Liquid Web at liquidweb.com/vps-hosting/managed-vps/.

Troubleshooting

"Plausible shows no events after adding the tracking script"

ClickHouse must be healthy before Plausible starts accepting events. Check: docker compose logs clickhouse --tail 20. If ClickHouse exits with an out-of-memory error, the default memory limit is too high for your available RAM. Add CLICKHOUSE_MAX_SERVER_MEMORY_USAGE: "4000000000" to the ClickHouse environment in the Compose file (sets a 4 GB cap — appropriate for a 16 GB VPS) and restart: docker compose up -d --force-recreate clickhouse. Also verify the tracking script domain matches the site domain registered in Plausible's web UI exactly — including subdomains.

"Postal fails to initialize with 'Can't connect to MySQL server'"

MySQL must be fully initialized before Postal's postal initialize command runs. MySQL's first boot — which includes setting the root password, creating the postal database and user, and running initial table creation — takes 20–40 seconds on a typical VPS. The Compose healthcheck helps, but postal initialize is run via docker compose run --rm, which respects healthchecks only if you use --no-deps. If initialization fails, wait 30 seconds and run docker compose run --rm postal postal initialize again. Once it succeeds, do not run postal initialize a second time — it will reset Postal's signing keys.

"Coolify and our Caddy instance both try to bind port 443"

Coolify's Traefik binds ports 80 and 443 at the host level as part of its installation. A second reverse proxy (Caddy) binding the same ports will fail with "address already in use." The cleanest resolution is to skip the standalone Caddy container entirely and register Authentik, Plausible, and Postal as applications inside Coolify's UI (see Step 4 above). Coolify's Traefik then issues TLS certificates and routes traffic for all four domains. If you need Caddy for a specific reason (e.g., custom middleware), bind it to alternate ports (8080/8443) and create Coolify proxy rules pointing to those ports — but this adds unnecessary complexity for most setups.

When this stack isn't right for you

  • You only need one or two of these tools — if you just want Plausible analytics, its standalone Compose file is much simpler. This meta stack is for teams who want the full PaaS + SSO + analytics + email foundation in one place.
  • Your team is email-heavy (>500k sends/month) — Postal running on a single VPS will handle moderate transactional volume, but high-volume sending requires proper IP warming, feedback loop configuration, and dedicated IPs. A managed ESP (Postmark, Mailgun) is more appropriate above this threshold.
  • You want Coolify to manage everything — you can deploy Authentik, Plausible, and Postal entirely through Coolify's one-click templates. The Compose file in this guide is for teams who prefer explicit configuration control. Both approaches are valid.
  • You're not comfortable managing email DNS — Postal requires PTR records, DKIM, SPF, and reputation monitoring. If your VPS provider does not allow PTR record configuration, email deliverability will be poor. Use Postmark or Mailgun until you can control all DNS records.
Frequently Asked Questions

Yes — that is the intended end state. However, Authentik needs an SMTP server before Postal is running, which creates a chicken-and-egg problem on first deploy. The .env file in this guide uses an external SMTP relay (Postmark) for Authentik's initial setup. Once Postal is running and DNS is verified (all DKIM and SPF checks pass), update AUTHENTIK_EMAIL__HOST to your Postal SMTP endpoint and restart the Authentik containers. All apps on this stack can then use Postal exclusively.

For any new app deployed via Coolify: in Authentik, create a new OIDC Provider and Application (same steps as the Coolify OIDC setup in Step 6). Set the app's redirect URI to its domain callback URL. In the app's environment variables (set inside Coolify), add the OIDC client ID, client secret, and Authentik endpoints. Apps that do not support OIDC natively can use Authentik's forward auth proxy mode with Traefik middleware — Coolify exposes this as a toggle on each application.

ClickHouse is memory-hungry by design. The CLICKHOUSE_MAX_SERVER_MEMORY_USAGE environment variable caps it: this guide sets 4 GB on a 16 GB VPS. You can reduce this further to 2000000000 (2 GB) if analytics traffic is low, at the cost of slower query performance under load. Do not set it below 1 GB — ClickHouse will restart under any real event ingestion load. If memory is the constraint, consider Plausible Cloud instead and drop ClickHouse from this stack.

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.