Self-Host Your Developer Stack: Replace SaaS with Open Source on One Droplet
Disclosure: Some links in this guide are affiliate links. If you sign up for DigitalOcean through them, ToolsHubKit earns a small commission — at no cost to you.
Running your own stack is the natural extension of ToolsHubKit's philosophy: your data, your compute, your rules. This guide shows you how to consolidate analytics, uptime monitoring, a password manager, file sync, and a notes app onto a single $12/mo Droplet — without losing the reliability you'd expect from SaaS.
We'll use Docker Compose as the management layer so each service is isolated, reproducible, and easy to update. Nginx handles routing to each subdomain. Everything gets automated backups.
1. The Case for Self-Hosting
| SaaS you pay for | Open-source replacement | Monthly saving |
|---|---|---|
| Google Analytics | Plausible / Umami | ~$9–$19 |
| Datadog (basic) | Uptime Kuma | ~$15–$25 |
| 1Password Teams | Vaultwarden (Bitwarden) | ~$5/seat |
| Dropbox Plus | Nextcloud | ~$11 |
| Notion | Outline | ~$8–$10 |
A 2 GB Droplet at $12/mo comfortably runs all five simultaneously. That's a payback in under a month versus even modest SaaS subscriptions.
2. Provision the Droplet
Create a new Droplet at DigitalOcean. Get $200 in free credits for 60 days when you sign up.
Recommended specs for a multi-service box:
| Services | RAM | vCPUs | Droplet |
|---|---|---|---|
| 2–3 lightweight services | 1 GB | 1 | Basic $6/mo |
| 4–6 services (this guide) | 2 GB | 2 | Basic $12/mo |
| 6+ services or Nextcloud | 4 GB | 2 | Basic $24/mo |
Choose Ubuntu 24.04 LTS x64, add your SSH key, select the datacenter region closest to your users.
3. Initial Server Setup
ssh root@YOUR_SERVER_IP
# Create admin user
adduser admin
usermod -aG sudo admin
# Copy SSH keys
cp -r /root/.ssh /home/admin/
chown -R admin:admin /home/admin/.ssh
# Harden SSH
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
systemctl restart ssh
# Firewall
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw enable
Log out and re-login as admin.
4. Install Docker and Docker Compose
sudo apt update && sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Run Docker without sudo
sudo usermod -aG docker $USER
newgrp docker
# Verify
docker --version && docker compose version
5. Directory Structure
/opt/selfhosted/
├── docker-compose.yml
├── .env
├── nginx/
│ ├── nginx.conf
│ └── conf.d/
│ ├── plausible.conf
│ ├── uptime-kuma.conf
│ ├── vaultwarden.conf
│ ├── nextcloud.conf
│ └── outline.conf
└── data/
├── plausible/
├── uptime-kuma/
├── vaultwarden/
├── nextcloud/
└── outline/
sudo mkdir -p /opt/selfhosted/{nginx/conf.d,data/{plausible,uptime-kuma,vaultwarden,nextcloud,outline}}
sudo chown -R admin:admin /opt/selfhosted
cd /opt/selfhosted
6. The Master docker-compose.yml
Create /opt/selfhosted/docker-compose.yml:
version: "3.9"
networks:
proxy:
external: true
internal:
internal: true
volumes:
plausible-db:
plausible-events:
vaultwarden-data:
nextcloud-data:
nextcloud-db:
outline-db:
uptime-kuma-data:
services:
# ── Plausible Analytics ──────────────────────────────
plausible-db:
image: postgres:16-alpine
restart: always
networks: [internal]
volumes: [plausible-db:/var/lib/postgresql/data]
environment:
POSTGRES_DB: plausible
POSTGRES_USER: plausible
POSTGRES_PASSWORD: ${PLAUSIBLE_DB_PASS}
plausible-events:
image: clickhouse/clickhouse-server:23.8-alpine
restart: always
networks: [internal]
volumes: [plausible-events:/var/lib/clickhouse]
ulimits:
nofile:
soft: 262144
hard: 262144
plausible:
image: ghcr.io/plausible/community-edition:v2
restart: always
networks: [proxy, internal]
depends_on: [plausible-db, plausible-events]
environment:
BASE_URL: https://stats.${DOMAIN}
SECRET_KEY_BASE: ${PLAUSIBLE_SECRET}
DATABASE_URL: postgres://plausible:${PLAUSIBLE_DB_PASS}@plausible-db/plausible
CLICKHOUSE_DATABASE_URL: http://plausible-events:8123/plausible_dev
# ── Uptime Kuma ──────────────────────────────────────
uptime-kuma:
image: louislam/uptime-kuma:1
restart: always
networks: [proxy]
volumes: [uptime-kuma-data:/app/data]
# ── Vaultwarden (Bitwarden) ──────────────────────────
vaultwarden:
image: vaultwarden/server:latest
restart: always
networks: [proxy]
volumes: [vaultwarden-data:/data]
environment:
DOMAIN: https://vault.${DOMAIN}
SIGNUPS_ALLOWED: "false"
ADMIN_TOKEN: ${VAULTWARDEN_ADMIN_TOKEN}
# ── Nextcloud ────────────────────────────────────────
nextcloud-db:
image: mariadb:11
restart: always
networks: [internal]
volumes: [nextcloud-db:/var/lib/mysql]
environment:
MYSQL_ROOT_PASSWORD: ${NEXTCLOUD_DB_ROOT_PASS}
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
MYSQL_PASSWORD: ${NEXTCLOUD_DB_PASS}
nextcloud:
image: nextcloud:28-apache
restart: always
networks: [proxy, internal]
depends_on: [nextcloud-db]
volumes: [nextcloud-data:/var/www/html]
environment:
MYSQL_HOST: nextcloud-db
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
MYSQL_PASSWORD: ${NEXTCLOUD_DB_PASS}
NEXTCLOUD_ADMIN_USER: admin
NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASS}
NEXTCLOUD_TRUSTED_DOMAINS: files.${DOMAIN}
# ── Outline (Knowledge Base) ─────────────────────────
outline-db:
image: postgres:16-alpine
restart: always
networks: [internal]
volumes: [outline-db:/var/lib/postgresql/data]
environment:
POSTGRES_DB: outline
POSTGRES_USER: outline
POSTGRES_PASSWORD: ${OUTLINE_DB_PASS}
outline:
image: outlinewiki/outline:latest
restart: always
networks: [proxy, internal]
depends_on: [outline-db]
environment:
DATABASE_URL: postgres://outline:${OUTLINE_DB_PASS}@outline-db/outline
SECRET_KEY: ${OUTLINE_SECRET}
UTILS_SECRET: ${OUTLINE_UTILS_SECRET}
URL: https://wiki.${DOMAIN}
PORT: 3000
Create /opt/selfhosted/.env:
DOMAIN=yourdomain.com
PLAUSIBLE_DB_PASS=change_me_long_random
PLAUSIBLE_SECRET=change_me_64_char_random
VAULTWARDEN_ADMIN_TOKEN=change_me_bcrypt_hash
NEXTCLOUD_DB_ROOT_PASS=change_me
NEXTCLOUD_DB_PASS=change_me
NEXTCLOUD_ADMIN_PASS=change_me
OUTLINE_DB_PASS=change_me
OUTLINE_SECRET=change_me_32_char
OUTLINE_UTILS_SECRET=change_me_32_char
Generate secrets:
# Random 64-char secret
openssl rand -hex 32
# Vaultwarden admin token (bcrypt hash of your chosen password)
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owasp
7. Nginx as a Reverse Proxy
Install Nginx on the host (not in Docker — easier to manage TLS):
sudo apt install -y nginx certbot python3-certbot-nginx
Create the shared Docker network:
docker network create proxy
Create /opt/selfhosted/nginx/conf.d/plausible.conf (repeat the pattern for each service):
server {
listen 80;
server_name stats.yourdomain.com;
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Port mapping for each service (add ports: to docker-compose or use a proxy network):
| Service | Internal port | Subdomain |
|---|---|---|
| Plausible | 8000 | stats.yourdomain.com |
| Uptime Kuma | 3001 | uptime.yourdomain.com |
| Vaultwarden | 80 | vault.yourdomain.com |
| Nextcloud | 80 | files.yourdomain.com |
| Outline | 3000 | wiki.yourdomain.com |
Issue TLS for all subdomains at once:
sudo certbot --nginx \
-d stats.yourdomain.com \
-d uptime.yourdomain.com \
-d vault.yourdomain.com \
-d files.yourdomain.com \
-d wiki.yourdomain.com
8. Start Everything
cd /opt/selfhosted
docker compose up -d
docker compose ps
Check logs for any startup errors:
docker compose logs -f plausible
docker compose logs -f vaultwarden
9. DNS Setup
Add A records pointing each subdomain to your Droplet IP:
stats.yourdomain.com A YOUR_DROPLET_IP
uptime.yourdomain.com A YOUR_DROPLET_IP
vault.yourdomain.com A YOUR_DROPLET_IP
files.yourdomain.com A YOUR_DROPLET_IP
wiki.yourdomain.com A YOUR_DROPLET_IP
TTL of 300 seconds during setup, bump to 3600 once confirmed working.
10. Automated Backups
Back up all service data nightly to DigitalOcean Spaces (S3-compatible):
sudo apt install -y s3cmd
s3cmd --configure # enter your Spaces credentials
Create /opt/selfhosted/backup.sh:
#!/bin/bash
set -e
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR=/tmp/selfhosted-backup-$BACKUP_DATE
SPACES_BUCKET=s3://your-bucket/backups
mkdir -p $BACKUP_DIR
# Dump all Postgres databases
docker exec selfhosted-plausible-db-1 pg_dump -U plausible plausible > $BACKUP_DIR/plausible.sql
docker exec selfhosted-outline-db-1 pg_dump -U outline outline > $BACKUP_DIR/outline.sql
# Dump MariaDB (Nextcloud)
docker exec selfhosted-nextcloud-db-1 mysqldump -uroot -p$NEXTCLOUD_DB_ROOT_PASS nextcloud > $BACKUP_DIR/nextcloud.sql
# Tar and upload
tar -czf /tmp/backup-$BACKUP_DATE.tar.gz -C /tmp selfhosted-backup-$BACKUP_DATE
s3cmd put /tmp/backup-$BACKUP_DATE.tar.gz $SPACES_BUCKET/
# Clean up local files
rm -rf $BACKUP_DIR /tmp/backup-$BACKUP_DATE.tar.gz
# Keep only 14 days in Spaces
s3cmd ls $SPACES_BUCKET/ | awk '{print $4}' | sort | head -n -14 | xargs -r s3cmd del
chmod +x /opt/selfhosted/backup.sh
# Run daily at 3am
echo "0 3 * * * admin /opt/selfhosted/backup.sh >> /var/log/selfhosted-backup.log 2>&1" | sudo tee /etc/cron.d/selfhosted-backup
11. Updates and Maintenance
Pull and rebuild all services with zero downtime:
cd /opt/selfhosted
# Pull latest images
docker compose pull
# Recreate containers one by one (Compose handles this)
docker compose up -d --remove-orphans
# Clean up old images
docker image prune -f
Set a monthly calendar reminder to run this. Most of these projects follow semantic versioning — check their GitHub releases before jumping major versions.
12. Monitoring the Stack Itself
Uptime Kuma (already running at uptime.yourdomain.com) can monitor all your other services:
- Open Uptime Kuma, go to Add New Monitor
- Type: HTTP(s)
- Add monitors for:
https://stats.yourdomain.com,https://vault.yourdomain.com,https://files.yourdomain.com,https://wiki.yourdomain.com - Set notification to email or Telegram
You now have self-hosted uptime monitoring watching your self-hosted services.
13. Honest Limits — When SaaS Still Wins
Self-hosting isn't for everyone in every situation:
| Scenario | Verdict |
|---|---|
| Solo developer, privacy-focused | ✅ Self-host wins |
| Small team, no DevOps person | ⚠️ Managed SaaS easier |
| Regulated industry (HIPAA, PCI) | ⚠️ Need compliance audit |
| 99.99% uptime required | ❌ Managed SaaS wins |
| You want to stop thinking about it | ❌ Managed SaaS wins |
A single Droplet has no automatic failover. If the server goes down, your services go down. For mission-critical workloads, use DigitalOcean's managed offerings or add a standby Droplet. For personal tools and small teams, a nightly backup + 5-minute restore is usually acceptable.
Related Guides
- GitHub Actions auto-deploy to DigitalOcean Droplet — automate deployments to this very server
- Deploy Spring Boot to DigitalOcean Droplet — add your own app to this stack
- Self-host Next.js on a $6 Droplet vs Serverless — add a frontend to the mix
Resources
| Resource | Link |
|---|---|
| DigitalOcean Droplets (+ $200 free credit) | Sign up here |
| Plausible Community Edition | https://github.com/plausible/community-edition |
| Vaultwarden | https://github.com/dani-garcia/vaultwarden |
| Uptime Kuma | https://github.com/louislam/uptime-kuma |
| Nextcloud | https://hub.docker.com/_/nextcloud |
| Outline | https://www.getoutline.com/self-hosted |