[Demo] Building a self-hosted automation stack
A temporary showcase post that exercises every block type: hero, rich text, code, media, gallery, video, tables, markdown, newsletter and ad space.
![[demo] Laptop on a wooden desk next to a notebook](/_next/image?url=https%3A%2F%2Fcdn.datontech.com%2Fphoto-0-1400x875.webp&w=3840&q=75)
![[demo] Close-up of a laptop keyboard](/_next/image?url=https%3A%2F%2Fcdn.datontech.com%2Fphoto-2-1400x875.webp&w=3840&q=75)
Building a self-hosted automation stack
From a blank VPS to scheduled workflows, tracing and backups in one weekend.
Why self-host at all?
Hosted automation tools are great until you hit rate limits, pricing tiers, or a workflow that needs to touch a private network. Owning the stack means you decide where the data lives. This post walks through the setup I use, with the config files inline.
The best automation is the one you forget exists, until the day it saves you an hour.
What we will build
- A Docker host with Caddy for TLS
- n8n for workflows, backed by Postgres
- OpenTelemetry traces shipped to a collector
- Nightly backups to S3-compatible storage
Inline code works too: run docker compose up -d and check the logs. Docs live at n8n.io.
![[demo] Developer workspace with a laptop and coffee](/_next/image?url=https%3A%2F%2Fcdn.datontech.com%2Fphoto-48-1400x875.webp&w=3840&q=75)
Step by step
- Provision the server and harden SSH
- Install Docker and the compose plugin
- Write the compose file below
- Point DNS at the box and let Caddy fetch certificates
Each step is small on purpose, so a failed step is easy to retry without starting over.
1services:2 n8n:3 image: n8nio/n8n:latest4 environment:5 - DB_TYPE=postgresdb6 - DB_POSTGRESDB_HOST=postgres7 ports:8 - "5678:5678"9 postgres:10 image: postgres:1611 volumes:12 - pgdata:/var/lib/postgresql/data13volumes:14 pgdata: {}
Minimal compose file for n8n and Postgres.
import { trace } from '@opentelemetry/api'const tracer = trace.getTracer('workflows')export async function runStep<T>(name: string, fn: () => Promise<T>): Promise<T> {return tracer.startActiveSpan(name, async (span) => {try {return await fn()} finally {span.end()}})}
| Option | Cost | Limits |
|---|---|---|
| Hosted plan | $50 | 10k executions |
| Self-hosted VPS | $8 | CPU-bound only |
| Home server | $3 power | Your uplink |
backup-checklist.md
Backup checklist
- Nightly
pg_dumpto object storage - Keep 7 daily and 4 weekly copies
- Test a restore once a month
A backup you have never restored is only a hope.
pg_dump -Fc "$DATABASE_URL" | aws s3 cp - s3://backups/n8n-$(date +%F).dump
| Workflow | Trigger | Runs on |
|---|---|---|
| RSS digest | Cron, 07:00 | n8n |
| Invoice export | Webhook | n8n |
| DB backup | Cron, 03:00 | Host cron |
Wrapping up
That is the whole stack. Start small, add tracing early, and test your restores. If you build something similar, I would love to hear about it.
![[demo] Hands typing on a laptop](/_next/image?url=https%3A%2F%2Fcdn.datontech.com%2Fphoto-201-1400x875.webp&w=3840&q=75)
Get the next post in your inbox
One email when something new ships.
![[demo] Minimal desk setup with a monitor](/_next/image?url=https%3A%2F%2Fcdn.datontech.com%2Fphoto-60-1400x875.webp&w=3840&q=75)
![[demo] Laptop and phone on a white desk](/_next/image?url=https%3A%2F%2Fcdn.datontech.com%2Fphoto-119-1400x875.webp&w=3840&q=75)
![[demo] Laptop with code on screen beside a plant](/_next/image?url=https%3A%2F%2Fcdn.datontech.com%2Fphoto-180-1400x875.webp&w=3840&q=75)