By LogicSystemIQ | May 24, 2026 | 9 min read
The host is a Dell OptiPlex 7060 Micro. It runs Windows 11 because I had it lying around. The CPU is an Intel i7-8700T, which is the 35-watt low-power variant of the i7-8700. Not the fastest chip on the planet, but it sips power and stays quiet under a desk.
| Component | Spec |
|---|---|
| Form factor | Dell OptiPlex 7060 Micro (1L footprint) |
| CPU | Intel i7-8700T (6 cores, 12 threads, 35W TDP) |
| RAM | 32GB DDR4 2666 MT/s |
| Storage | 2.73TB total, 1.25TB free |
| GPU | Intel UHD 630 integrated (no dedicated) |
| Network | 1GbE built in |
The host runs Windows because that is where I do client work, and the VM is just one window I open when I need to. If you have a Linux-only machine, this works even better because you skip the VM layer entirely and run Docker on the host.
| Setting | Value |
|---|---|
| Hypervisor | VMware Workstation Pro 17.x (free for commercial use, November 2024) |
| Guest OS | Ubuntu Server 24.04 LTS |
| vCPU | 1 socket, 4 cores |
| RAM | 12GB |
| Disk | 150GB on a single VMDK file |
| Networking | Bridged (gets a real LAN IP from my router) |
| VT-x passthrough | Disabled (Hyper-V holds the extensions for WSL2) |
| VMware Tools | Installed |
| Time sync | Enabled |
The 12GB allocation is intentional. The host has 32GB, but Windows + Edge + Slack + whatever else I am doing easily eats 16GB. Leaving 12GB to the VM and keeping a 4GB buffer for the host has worked smoothly.
Here is exactly what runs on the VM, grouped by deploy phase.
| Container | Image | Job |
|---|---|---|
| dockge | louislam/dockge:1 | Web UI for managing all the compose stacks |
| caddy | caddy:2-alpine | Reverse proxy, auto-HTTPS for LAN via internal CA, internal routing |
| cloudflared | cloudflare/cloudflared:latest | Outbound tunnel to Cloudflare edge for public hostnames |
| postgres | postgres:16-alpine | Shared Postgres for Vaultwarden, Twenty, GoTrue, n8n |
| dozzle | amir20/dozzle:latest | Real-time log viewer for every container |
| vaultwarden | vaultwarden/server:latest | Password manager (Bitwarden-compatible, but self-hosted) |
| beszel | henrygd/beszel:latest | System and container monitoring with ntfy alerts |
| beszel-agent | henrygd/beszel-agent:latest | Metric collector that reports to Beszel hub |
| diun | crazymax/diun:latest | Image update notifier (no auto-updates, just ntfy) |
| ntfy | binwiederhier/ntfy:latest | Self-hosted push notifications to my phone |
Wait, that is 10, not 8. Phase 1 added the ntfy + diun pair after the initial bring-up. Same idea, slightly bigger than the original spec.
| Container | Image | Job |
|---|---|---|
| redis | redis:7-alpine | Shared cache + queue for AppFlowy + Twenty |
| clickhouse | clickhouse/clickhouse-server:24-alpine | Analytics DB for Plausible |
| plausible | ghcr.io/plausible/community-edition:v3 | Self-hosted privacy-respecting web analytics |
| gotrue | appflowyinc/gotrue:latest | Auth service for AppFlowy (their fork of Supabase GoTrue) |
| minio | minio/minio:latest | S3-compatible storage for AppFlowy and Twenty file uploads |
| appflowy_postgres | pgvector/pgvector:pg16 | Dedicated Postgres for AppFlowy (needs pgvector extension) |
| appflowy_cloud | appflowyinc/appflowy_cloud:latest | AppFlowy backend (Notion alternative) |
| appflowy_web | appflowyinc/appflowy_web:latest | AppFlowy frontend |
| twenty_server | twentycrm/twenty:latest | Twenty CRM server (Pipedrive alternative) |
| twenty_worker | twentycrm/twenty:latest | Twenty CRM BullMQ background worker |
| Container | Image | Job |
|---|---|---|
| n8n | n8nio/n8n:1 | Workflow automation (greenfield home-stack instance) |
| firecrawl_api | ghcr.io/firecrawl/firecrawl:latest | Self-hosted web scraper API |
| firecrawl_playwright | ghcr.io/firecrawl/playwright-service:latest | Headless Chromium for scraping |
| firecrawl_redis | redis:alpine | Dedicated Redis for Firecrawl (upstream warns against sharing) |
| firecrawl_rabbitmq | rabbitmq:3-management | Job queue for Firecrawl |
| firecrawl_nuq_postgres | ghcr.io/firecrawl/nuq-postgres:latest | Crawl history database |
| openwebui | ghcr.io/open-webui/open-webui:main | Self-hosted Claude / ChatGPT replacement (via OpenRouter) |
Add it all up and you get 27.
Here is the actual breakdown from free -h right now:
total used free shared buff/cache available
Mem: 11Gi 7.0Gi 493Mi 171Mi 4.8Gi 4.8Gi
The big consumers:
| Tier | Approximate RAM |
|---|---|
| Postgres + ClickHouse + the 4 Redis-class processes | 1.2GB combined |
| AppFlowy stack (Cloud + Web + GoTrue + dedicated PG) | 1.5GB |
| Twenty CRM (server + worker) | 1.0GB |
| Plausible | 350MB |
| Firecrawl playwright headless Chromium pool | 600MB to 1.5GB depending on active scrapes |
| Open WebUI | 350MB |
| n8n | 250MB |
| Everything else (Caddy, cloudflared, Dockge, Dozzle, Beszel, Vaultwarden, ntfy, diun) | under 600MB combined |
Buffers and cache eat the rest, which is exactly what you want, that is Linux working as designed.
The biggest variable is Firecrawl during an active scrape. Each headless Chromium tab is roughly 200MB. The compose limits it to 3 concurrent jobs and 5 browser pool, which caps Firecrawl spend around 1.5GB. If a scrape kicks off while Twenty is indexing and AppFlowy is doing background sync, that is the load spike where I might hit 9GB used, 3GB free. Still fine.
Instead of running a dedicated Postgres per app, one shared postgres:16-alpine container hosts databases for Vaultwarden, Twenty CRM, GoTrue, and n8n. Each gets its own database and role, scoped with grants. The exception is AppFlowy, which needs pgvector and gets a dedicated pgvector/pgvector:pg16 container.
Single Postgres saves about 700MB of RAM versus running four of them. The trade-off is one upgrade hits all apps. Acceptable in exchange for the savings.
Every public-facing service routes through Caddy on a private Docker network. Caddy talks to cloudflared, which holds an outbound persistent tunnel to Cloudflare edge. No ports forwarded on my router. No public IP needed on the host. Cloudflare Access sits in front of admin tools with email OTP gating, and webhook-only endpoints (Stripe receivers, n8n triggers) get bypass policies so external services can hit them without an OTP prompt.
This setup costs zero dollars on Cloudflare's free tier and is more secure than anything I could have hand-rolled.
Rough monthly savings, based on what each replaced:
| Replaced SaaS | Self-hosted equivalent | Monthly savings |
|---|---|---|
| Last password manager subscription | Vaultwarden | $3 |
| Notion team plan | AppFlowy | $10 |
| Pipedrive starter | Twenty CRM | $14 |
| Google Analytics + privacy concerns | Plausible | $0 in dollars, big in privacy |
| Uptime monitoring SaaS | Beszel + ntfy | $7 |
| n8n cloud | Self-hosted n8n | $20 |
| ChatGPT Plus | Open WebUI via OpenRouter | $20 (replaced with ~$5 pay-as-you-go) |
| Firecrawl SaaS tier | Self-hosted Firecrawl | $20 to $50 |
| Total | ~$50 to $90 per month |
Annualized, somewhere between $600 and $1,080. Enough to fund the GMKtec K11 mini PC that this whole stack will migrate to once it arrives.
A few things this setup is honestly bad at:
The K11 (GMKtec, Ryzen 9 8945HS, 64GB DDR5, dual NVMe) is the planned upgrade. Once it lands, the migration is roughly:
The stack is designed to be portable. Everything is in git, every secret is in Vaultwarden, every data path is in the backup. The whole thing should migrate cleanly in an afternoon.
I will use Proxmox on the K11. On the Dell OptiPlex, VMware Workstation Pro was the path of least resistance because it ran on my existing Windows install. No bare metal swap needed. Free for commercial use since November 2024.
For 27 containers across 6 stacks on a single host, Docker Compose plus Dockge for the UI is the right tool. Kubernetes would be roughly 1.5GB of overhead for the control plane and zero practical benefit at this scale.
Kopia handles snapshots of /opt/stack/data and the Postgres dumps every night at 2 AM. Two destinations: local USB drive (when plugged in) and Backblaze B2 (always). Daily snapshots, 7 daily / 4 weekly / 6 monthly retention. Monthly test restore to a tmp directory to verify backups actually work.
Cloudflare Access in front of admin tools (email OTP for me, IP allowlist for service-to-service calls). Vaultwarden holds every credential, with Argon2id hashed admin token. Postgres is on a separate internal Docker network with no external port. UFW on the host blocks everything except SSH from LAN. Daily automatic security updates via unattended-upgrades.
Open to it. Drop me a comment or DM on LinkedIn and I will share the relevant compose snippets sanitized for public consumption.
You do not need a $3,000 server, a Kubernetes cluster, or a SaaS subscription per app to run a serious self-hosted stack. A used mini PC, a free hypervisor, and Docker Compose with discipline gets you 90 percent of the way there. The remaining 10 percent (HA, GPU inference, multi-host failover) is what real infrastructure budgets are for, and most of us do not need them.
If you are thinking about starting your own self-hosted stack, the question is not "what is the perfect architecture." It is "what is the smallest thing I can stand up this weekend that I will actually use." Pick one tool you are paying for, replace it, and let momentum carry you.
LogicSystemIQ is an IT managed services and SaaS studio based in Peabody, Massachusetts. We build DaycarePro (daycarepro.cloud), a trilingual SaaS for licensed home daycare providers. Reach us at (978) 815-1047 or Support@LogicSystemiq.com.