+91 80401 38000[email protected]24/7 Expert Support
[email protected]Client Portal →
ServerGurus
← All posts
HostingSelfhostedTutorialInfrastructureOpen Source

The Selfhosted Stack: Replace 12 SaaS Subscriptions with Your Own Server

By ServerGurus Team2 August 202610 min read
The Selfhosted Stack: Replace 12 SaaS Subscriptions with Your Own Server

The SaaS Tax

An average startup spends $380/month per employee on SaaS tools. A 10-person team burns $45,000/year on subscriptions that hold their data, send their email, sync their passwords, and store their files. Most of these tools are thin wrappers around open source software running on someone else's server.

For the price of a single bare metal server ($99/month on ServerGurus), you can self-host every one of them - retaining full control over your data, zero per-seat pricing, and an uptime SLA that depends on your skill, not a vendor's status page.

This guide builds the selfhosted stack - a complete replacement for 12 SaaS products on a single server. Docker Compose, one YAML file, everything behind a reverse proxy with automatic SSL.

The Stack at a Glance

SaaS You Pay For Selfhosted Replacement Monthly Saving
Google Workspace Stalwart Mail + SnappyMail $6/user
Notion / Confluence Outline Wiki $10/user
Slack / Teams Mattermost $8/user
GitHub / GitLab Gitea $4/user
1Password / Bitwarden Vaultwarden $4/user
Google Drive / Dropbox Nextcloud $10/user
Vercel / Netlify Coolify $20/project
Sentry / Datadog Sentry + Uptime Kuma $15/host
Google Analytics Plausible $9/site
Mailchimp Listmonk $30/mo
Auth0 / Clerk Authentik $0.02/user
Slack Workflows n8n $20/user

Total SaaS cost for 10 people: ~$3,800/month. Selfhosted stack: one $99 server. The math is not subtle.

Hardware Requirement

A dedicated server or VPS with:

Component Minimum Recommended
CPU 4 cores 8 cores (AMD EPYC / Intel Xeon)
RAM 8GB 16GB+ (ZFS ARC loves RAM)
Storage 100GB SSD 2 × 500GB NVMe (mirror)
Network 1Gbps 1Gbps with 5TB transfer

ServerGurus' Entry VPS ($19.99/month) runs the light stack. Our Bare Metal E-2488 ($99/month) runs everything comfortably with room to grow.

Step 1: Base System (Ubuntu 24.04)

# After provisioning your server
apt update && apt upgrade -y
apt install -y docker.io docker-compose-v2 nginx certbot python3-certbot-nginx ufw

# Enable firewall
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

# Create directory structure
mkdir -p /opt/selfhosted/{mail,gitea,nextcloud,vaultwarden,mattermost,coolify,outline,sentry,plausible,listmonk,n8n,authentik,uptime}
mkdir -p /opt/selfhosted/data/{postgres,redis,backups}

Step 2: Reverse Proxy with Automatic SSL

# Caddy is the simplest reverse proxy for selfhosted stacks
apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/caddy-stable-archive-keyring.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main" | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install -y caddy

# Caddyfile
cat > /etc/caddy/Caddyfile << 'EOF'
{
    email [email protected]
}

git.yourdomain.com {
    reverse_proxy localhost:3001
}

cloud.yourdomain.com {
    reverse_proxy localhost:8080
}

vault.yourdomain.com {
    reverse_proxy localhost:8081
}

chat.yourdomain.com {
    reverse_proxy localhost:8065
}

wiki.yourdomain.com {
    reverse_proxy localhost:3002
}

deploy.yourdomain.com {
    reverse_proxy localhost:8000
}

errors.yourdomain.com {
    reverse_proxy localhost:9000
}

analytics.yourdomain.com {
    reverse_proxy localhost:8001
}

newsletter.yourdomain.com {
    reverse_proxy localhost:9001
}

automation.yourdomain.com {
    reverse_proxy localhost:5678
}

auth.yourdomain.com {
    reverse_proxy localhost:9002
}

status.yourdomain.com {
    reverse_proxy localhost:3003
}
EOF

systemctl restart caddy

Caddy automatically obtains and renews Let's Encrypt certificates for every subdomain. No cron jobs, no certbot renewal scripts. Point your DNS A record at the server's IP, wildcard if you prefer:

@       A       165.140.164.100
*       CNAME   @

Step 3: Docker Compose - The Whole Stack

# /opt/selfhosted/docker-compose.yml
version: '3.8'

services:
  # ── DATABASES ──────────────────────────────────
  postgres:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    networks:
      - backend

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - ./data/redis:/data
    networks:
      - backend

  # ── GITEA (GitHub replacement) ──────────────────
  gitea:
    image: gitea/gitea:1.23
    restart: unless-stopped
    ports:
      - '127.0.0.1:3001:3000'
    environment:
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=postgres:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=${DB_PASSWORD}
    volumes:
      - ./gitea/data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    depends_on:
      - postgres
    networks:
      - backend

  # ── NEXTCLOUD (Google Drive replacement) ────────
  nextcloud:
    image: nextcloud:30-fpm-alpine
    restart: unless-stopped
    volumes:
      - ./nextcloud/html:/var/www/html
      - ./nextcloud/data:/var/www/html/data
      - ./nextcloud/config:/var/www/html/config
    environment:
      - POSTGRES_HOST=postgres
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=nextcloud
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - REDIS_HOST=redis
    depends_on:
      - postgres
      - redis
    networks:
      - backend

  nextcloud-web:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - '127.0.0.1:8080:80'
    volumes:
      - ./nextcloud/web.conf:/etc/nginx/conf.d/default.conf
      - ./nextcloud/html:/var/www/html
    depends_on:
      - nextcloud
    networks:
      - backend

  # ── VAULTWARDEN (1Password replacement) ─────────
  vaultwarden:
    image: vaultwarden/server:latest
    restart: unless-stopped
    ports:
      - '127.0.0.1:8081:80'
    environment:
      - DOMAIN=https://vault.yourdomain.com
      - SIGNUPS_ALLOWED=false
      - WEBSOCKET_ENABLED=true
    volumes:
      - ./vaultwarden/data:/data
    networks:
      - backend

  # ── MATTERMOST (Slack replacement) ──────────────
  mattermost:
    image: mattermost/mattermost-team-edition:10.5
    restart: unless-stopped
    ports:
      - '127.0.0.1:8065:8065'
    environment:
      - MM_SQLSETTINGS_DRIVERNAME=postgres
      - MM_SQLSETTINGS_DATASOURCE=postgres://mattermost:${DB_PASSWORD}@postgres:5432/mattermost?sslmode=disable
      - MM_SERVICESETTINGS_SITEURL=https://chat.yourdomain.com
    volumes:
      - ./mattermost/config:/mattermost/config
      - ./mattermost/data:/mattermost/data
      - ./mattermost/plugins:/mattermost/plugins
    depends_on:
      - postgres
    networks:
      - backend

  # ── OUTLINE (Notion replacement) ────────────────
  outline:
    image: outlinewiki/outline:latest
    restart: unless-stopped
    ports:
      - '127.0.0.1:3002:3000'
    environment:
      - DATABASE_URL=postgres://outline:${DB_PASSWORD}@postgres:5432/outline
      - REDIS_URL=redis://redis:6379
      - URL=https://wiki.yourdomain.com
      - SECRET_KEY=${OUTLINE_SECRET}
      - UTILS_SECRET=${OUTLINE_UTILS_SECRET}
      - OIDC_CLIENT_ID=outline
      - OIDC_CLIENT_SECRET=${OUTLINE_OIDC_SECRET}
      - OIDC_AUTH_URI=https://auth.yourdomain.com/application/o/authorize/
      - OIDC_TOKEN_URI=https://auth.yourdomain.com/application/o/token/
      - OIDC_USERINFO_URI=https://auth.yourdomain.com/application/o/userinfo/
    volumes:
      - ./outline/data:/var/lib/outline/data
    depends_on:
      - postgres
      - redis
    networks:
      - backend

  # ── COOLIFY (Vercel/Netlify replacement) ────────
  coolify:
    image: coollabsio/coolify:latest
    restart: unless-stopped
    ports:
      - '127.0.0.1:8000:3000'
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./coolify/data:/app/data
    environment:
      - APP_ID=coolify
      - APP_ENV=production
    networks:
      - backend

  # ── SENTRY (Error tracking) ────────────────────
  sentry:
    image: getsentry/sentry:24.12
    restart: unless-stopped
    ports:
      - '127.0.0.1:9000:9000'
    environment:
      - SENTRY_SECRET_KEY=${SENTRY_SECRET_KEY}
      - SENTRY_POSTGRES_HOST=postgres
      - SENTRY_DB_USER=sentry
      - SENTRY_DB_PASSWORD=${DB_PASSWORD}
      - SENTRY_REDIS_HOST=redis
    volumes:
      - ./sentry/data:/data
    depends_on:
      - postgres
      - redis
    networks:
      - backend

  # ── PLAUSIBLE (Google Analytics replacement) ────
  plausible:
    image: plausible/analytics:v3
    restart: unless-stopped
    ports:
      - '127.0.0.1:8001:8000'
    environment:
      - BASE_URL=https://analytics.yourdomain.com
      - SECRET_KEY_BASE=${PLAUSIBLE_SECRET}
      - DATABASE_URL=postgres://plausible:${DB_PASSWORD}@postgres:5432/plausible
    depends_on:
      - postgres
    networks:
      - backend

  # ── LISTMONK (Mailchimp replacement) ────────────
  listmonk:
    image: listmonk/listmonk:v4
    restart: unless-stopped
    ports:
      - '127.0.0.1:9001:9000'
    environment:
      - LISTMONK_db__host=postgres
      - LISTMONK_db__user=listmonk
      - LISTMONK_db__password=${DB_PASSWORD}
      - LISTMONK_db__database=listmonk
    volumes:
      - ./listmonk/config.toml:/listmonk/config.toml
    depends_on:
      - postgres
    networks:
      - backend

  # ── N8N (Zapier/Make replacement) ───────────────
  n8n:
    image: n8nio/n8n:latest
    restart: unless-stopped
    ports:
      - '127.0.0.1:5678:5678'
    environment:
      - N8N_HOST=automation.yourdomain.com
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=n8n
      - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD}
    volumes:
      - ./n8n/data:/home/node/.n8n
    depends_on:
      - postgres
    networks:
      - backend

  # ── AUTHENTIK (Auth0/Clerk replacement) ─────────
  authentik-server:
    image: ghcr.io/goauthentik/server:2024.12
    restart: unless-stopped
    ports:
      - '127.0.0.1:9002:9000'
      - '127.0.0.1:9443:9443'
    environment:
      - AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET}
      - AUTHENTIK_POSTGRESQL__HOST=postgres
      - AUTHENTIK_POSTGRESQL__NAME=authentik
      - AUTHENTIK_POSTGRESQL__USER=authentik
      - AUTHENTIK_POSTGRESQL__PASSWORD=${DB_PASSWORD}
      - AUTHENTIK_REDIS__HOST=redis
    volumes:
      - ./authentik/media:/media
      - ./authentik/custom-templates:/templates
    depends_on:
      - postgres
      - redis
    networks:
      - backend

  authentik-worker:
    image: ghcr.io/goauthentik/server:2024.12
    restart: unless-stopped
    command: worker
    environment:
      - AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET}
      - AUTHENTIK_POSTGRESQL__HOST=postgres
      - AUTHENTIK_POSTGRESQL__NAME=authentik
      - AUTHENTIK_POSTGRESQL__USER=authentik
      - AUTHENTIK_POSTGRESQL__PASSWORD=${DB_PASSWORD}
      - AUTHENTIK_REDIS__HOST=redis
    volumes:
      - ./authentik/media:/media
      - ./authentik/custom-templates:/templates
    depends_on:
      - postgres
      - redis
    networks:
      - backend

  # ── UPTIME KUMA (Monitoring) ────────────────────
  uptime-kuma:
    image: louislam/uptime-kuma:1
    restart: unless-stopped
    ports:
      - '127.0.0.1:3003:3001'
    volumes:
      - ./uptime/data:/app/data
    networks:
      - backend

networks:
  backend:
    driver: bridge

Step 4: Email - The Hard Part

Email self-hosting has a bad reputation for good reason: deliverability is hard. Stalwart Mail solves this.

# Stalwart Mail - modern, all-in-one mail server with JMAP, IMAP, SMTP
mkdir /opt/selfhosted/stalwart
docker run -d --name stalwart-mail \
  -p 25:25 -p 143:143 -p 993:993 -p 587:587 -p 465:465 -p 4190:4190 \
  -v /opt/selfhosted/stalwart:/opt/stalwart-mail \
  stalwartlabs/mail-server:latest

Stalwart handles SPF, DKIM, DMARC, and TLS automatically. The critical deliverability steps:

  1. PTR record: Your server IP must reverse-resolve to your mail domain
  2. SPF: v=spf1 mx -all (only your server sends mail)
  3. DKIM: Stalwart generates the key; add the public key to DNS
  4. DMARC: v=DMARC1; p=quarantine; rua=mailto:[email protected]
  5. Warm-up: Send to yourself first, then to a few friends, then to your list - never blast 10,000 emails from a new IP

For webmail, SnappyMail is lightweight and fast:

docker run -d --name snappymail \
  -p 127.0.0.1:8082:8888 \
  -v /opt/selfhosted/snappymail:/var/lib/snappymail \
  djmaze/snappymail:latest

Add to Caddyfile:

mail.yourdomain.com {
    reverse_proxy localhost:8082
}

Step 5: Backups - Non-Negotiable

You are now responsible for your data. Automated, off-site, encrypted backups are mandatory:

# /opt/selfhosted/backup.sh
#!/bin/bash
set -e

BACKUP_DIR="/opt/selfhosted/data/backups/$(date +%Y-%m-%d_%H-%M)"
mkdir -p "$BACKUP_DIR"

# Stop services briefly for consistent dumps
docker compose -f /opt/selfhosted/docker-compose.yml stop

# Dump PostgreSQL
docker compose -f /opt/selfhosted/docker-compose.yml run --rm postgres \
  pg_dumpall -U postgres > "$BACKUP_DIR/postgres-all.sql"

# Copy persistent volumes
rsync -a /opt/selfhosted/gitea/data "$BACKUP_DIR/gitea"
rsync -a /opt/selfhosted/nextcloud/data "$BACKUP_DIR/nextcloud"
rsync -a /opt/selfhosted/vaultwarden/data "$BACKUP_DIR/vaultwarden"

# Restart
docker compose -f /opt/selfhosted/docker-compose.yml up -d

# Encrypt and ship to remote storage
tar czf - "$BACKUP_DIR" | gpg --encrypt --recipient [email protected] \
  | rclone rcat remote:backups/selfhosted/$(date +%Y-%m-%d).tar.gz.gpg

# Keep 7 days locally
find /opt/selfhosted/data/backups -maxdepth 1 -mtime +7 -exec rm -rf {} \;

Schedule with cron:

# Daily at 3 AM
echo '0 3 * * * root /opt/selfhosted/backup.sh >> /var/log/backup.log 2>&1' > /etc/cron.d/selfhosted-backup

For remote storage, rclone supports S3-compatible, Backblaze B2, and rsync.net. ServerGurus includes S3-compatible object storage with every bare metal plan.

Step 6: Unified Auth with Authentik

Once Authentik is running, every application authenticates through it:

┌────────────────────────────────────────────┐
│              Authentik (auth.yourdomain.com)│
│         SAML / OIDC / LDAP / Proxy          │
└────┬───────┬───────┬───────┬──────┬─────────┘
     │       │       │       │      │
  Gitea   Outline  Nextcloud  n8n  Mattermost

One account, one password, one MFA method. When someone leaves, you disable them in Authentik and they lose access to everything instantly. No hunting through 12 admin panels.

Add Authentik's OIDC provider details to each app's SSO settings. It takes 30 minutes to wire up the whole stack once.

Monitoring: Uptime Kuma

Uptime Kuma pings every service every 60 seconds and sends alerts via Telegram, Discord, email, or webhook. Add a status page:

status.yourdomain.com → Uptime Kuma public status page

Now when someone asks "is the wiki down?", send them to the status page instead of checking 12 services manually.

What This Stack Costs vs SaaS

Service SaaS (monthly, 10 users) Selfhosted
Email $60 (Google Workspace) $0
File storage $100 (Google Drive) $0
Git hosting $40 (GitHub Team) $0
Password manager $40 (1Password) $0
Wiki $100 (Notion) $0
Chat $80 (Slack) $0
CI/CD $200 (Vercel/GitHub Actions) $0
Error tracking $150 (Sentry) $0
Analytics $90 (Plausible + GA) $0
Newsletter $300 (Mailchimp) $0
Auth $100 (Auth0) $0
Automation $200 (Zapier) $0
Total $1,460/mo $0 + server cost

ServerGurus Bare Metal E-2488: $99/month. You keep $1,361/month - $16,332/year - that previously went to SaaS vendors.

When Not to Self-Host

Self-hosting is not always right:

  • You have no one who understands Docker/PostgreSQL at all. The stack needs basic sysadmin knowledge. If your team is entirely non-technical, stay on SaaS.
  • Compliance requires SOC 2 Type II from every service. You can self-host with compliance, but it requires documented processes, access controls, and audit logs. Factor that time in.
  • Email deliverability is mission-critical and you cannot afford warm-up time. Use a transactional email service (SendGrid, SES) for critical mail and self-host everything else.

Getting Started with ServerGurus

We offer the selfhosted stack as a one-click deployment on our bare metal servers:

  1. Provision a Bare Metal E-2488 or above
  2. Select "Selfhosted Stack" from our app marketplace
  3. Our deployment script runs, Caddy gets SSL certificates, the Docker Compose stack starts
  4. You get a dashboard with login URLs and initial admin credentials for each service
  5. Point your DNS A record at the server IP

From zero to a fully functional selfhosted stack in under 30 minutes. Existing data migrations (Google Drive → Nextcloud, GitHub → Gitea, 1Password → Vaultwarden) are included.

Self-hosting is not about rejecting SaaS - it is about reclaiming control over the tools your team depends on every day. The open source ecosystem in 2026 has caught up. The SaaS wrapping paper is getting thinner. Take it off.

Ready to build your infrastructure?

Get a quote from our Hyderabad-based team - Tier IV datacenter, real support, INR or USD billing.

View pricingRequest a quoteWhatsApp sales