DEV Community

Cover image for The $3/Month Enterprise-Ready Automation Stack: Zero-Trust n8n With One Command (n8n, Cloudflare, UpCloud, Pulumi)
Mihai Farcas
Mihai Farcas

Posted on • Originally published at linkedin.com

The $3/Month Enterprise-Ready Automation Stack: Zero-Trust n8n With One Command (n8n, Cloudflare, UpCloud, Pulumi)

I run one command and get a fully working n8n instance — HTTPS, zero-trust tunnel, PostgreSQL, the works. One more command and it's all gone. Total cost: $3/month.

No SSH. No open ports. No clicking around in dashboards.

Here's exactly how it works.


The Problem With Self-Hosted Automation

Every self-hosting tutorial I find tells me to do the same thing: open ports 80 and 443, set up Nginx, wrestle with Let's Encrypt, and pray nobody scans my server before I've hardened it.

Then there's the database. Most n8n guides default to SQLite. It works — until two workflows fire at the same time and the whole thing locks up.

I don't want to babysit a server. I want to deploy, use it, and tear it down when I'm done. No manual steps. No leftover infrastructure.

So I built exactly that.


What You Get

One pulumi up command creates:

  • A $3/month UpCloud server (1 CPU, 1 GB RAM, 10 GB storage, Ubuntu 24.04)
  • A Cloudflare Tunnel — zero-trust, outbound-only connection. No open ports.
  • Automatic HTTPS and DDoS protection via Cloudflare's edge
  • PostgreSQL 18 for concurrent workflow execution
  • n8n running behind the tunnel, accessible at your custom domain
  • DNS records pointed at the tunnel automatically

One pulumi destroy removes everything. Server, tunnel, DNS — gone.


Why Zero-Trust Matters Here

Traditional setups expose your server to the internet. You open ports, configure firewalls, manage certificates. Every open port is an attack surface.

Cloudflare Tunnels flip this model. The cloudflared daemon on your server creates an outbound-only connection to Cloudflare's network. Traffic flows in reverse:

User → Cloudflare (HTTPS + DDoS) → Tunnel → n8n container

Your server has zero open inbound ports. No firewall rules to manage. No certificates to renew. Cloudflare handles all of it at the edge.


The 6-Pillar Stack

Each tool has one job:

  • Pulumi — Infrastructure as Code. Provisions everything.
  • UpCloud — $3/month compute (Frankfurt, DE).
  • Cloudflare — Zero-trust tunnel + HTTPS + DDoS protection.
  • Docker — Container runtime via Docker Compose.
  • n8n — Workflow automation engine.
  • PostgreSQL 18 — Production database for concurrent execution.

How It Wires Together

The entire deployment is 180 lines of TypeScript. Here's what happens when you run pulumi up:

1. Pulumi Creates the Cloudflare Tunnel

const tunnel = new cloudflare.ZeroTrustTunnelCloudflared("n8n-tunnel", {
  accountId: cfAccountId,
  name: "n8n-tunnel",
  configSrc: "cloudflare",
  tunnelSecret: crypto.randomBytes(32).toString("base64")
});
Enter fullscreen mode Exit fullscreen mode

A 32-byte random secret gets generated. The tunnel routes traffic from your domain to http://n8n:5678 inside the Docker network.

2. The Tunnel Token Gets Fetched via API

const tunnelToken = cloudflare.getZeroTrustTunnelCloudflaredTokenOutput({
  accountId: cfAccountId,
  tunnelId: tunnel.id
});
Enter fullscreen mode Exit fullscreen mode

This is the key trick. Instead of SSHing into the server after boot to inject the token, Pulumi fetches it directly from Cloudflare's API. The token gets baked into the cloud-init script — zero post-deploy steps.

3. DNS Points to the Tunnel

const dnsRecord = new cloudflare.DnsRecord("n8n-dns", {
  zoneId: cfZoneId,
  name: domain,
  type: "CNAME",
  content: pulumi.interpolate`${tunnel.id}.cfargotunnel.com`,
  proxied: true
});
Enter fullscreen mode Exit fullscreen mode

A proxied CNAME record. Cloudflare's edge handles TLS termination and DDoS filtering before traffic ever reaches your server.

4. Cloud-Init Bootstraps the Server

Pulumi builds a cloud-init script that:

  1. Installs Docker Engine
  2. Writes docker-compose.yml with all secrets baked in
  3. Writes the tunnel token to .env
  4. Runs docker compose up -d

The server boots, runs this script, and 60 seconds later n8n is live. No SSH required.

5. Three Containers, One Compose File

services:
  postgres:
    image: postgres:18-alpine
    # Health check ensures n8n waits for DB

  n8n:
    image: n8nio/n8n:latest
    depends_on:
      postgres:
        condition: service_healthy
    # Full PostgreSQL config, HTTPS via tunnel

  cloudflared:
    image: cloudflare/cloudflared:latest
    command: tunnel run
    # Token from .env file
Enter fullscreen mode Exit fullscreen mode

PostgreSQL starts first. n8n waits for the health check. cloudflared connects the tunnel. The Docker network handles service discovery — n8n:5678 just works.


The Setup (5 Minutes)

Prerequisites

  • An UpCloud account (API credentials)
  • A Cloudflare account with a domain (API token with Zone:DNS:Edit + Account:Tunnels:Edit)
  • Pulumi CLI installed
  • Node.js 18+

Deploy

# Clone and install
cd infra && npm install

# Create .env from the template (in the project root)
cp ../.env.example ../.env
# Edit .env — fill in your API tokens, domain, and Cloudflare IDs
# SSH key is auto-detected from ~/.ssh/ (no config needed)

# Load env vars and set secrets
set -a && source ../.env && set +a
pulumi config set --secret postgresPassword "$(openssl rand -hex 16)"
pulumi config set --secret n8nBasicAuthUser "admin"
pulumi config set --secret n8nBasicAuthPassword "$(openssl rand -hex 16)"
pulumi config set --secret n8nEncryptionKey "$(openssl rand -hex 32)"

# Deploy everything
pulumi up
Enter fullscreen mode Exit fullscreen mode

That's it. Visit https://n8n.yourdomain.com and log in.

Tear Down

pulumi destroy
Enter fullscreen mode Exit fullscreen mode

Server, tunnel, DNS — all removed. Nothing left running. Nothing left billing.


What About Persistence?

PostgreSQL data is saved as a Docker volume on the server itself. Your workflows, credentials, and execution history survive container restarts and docker compose down / up cycles.

What they don't survive is pulumi destroy — that deletes the server and everything on it.

For production, consider switching to a managed PostgreSQL service (UpCloud, Aiven, Neon) or mounting resilient block storage for the database volume. Either option decouples your data from the server lifecycle.


What About n8n Cloud?

n8n Cloud is the easiest way to get started. Managed hosting, automatic updates, built-in auth, and zero infrastructure to think about. If you don't want to manage servers, it's the right choice.

This self-hosted stack is a different tradeoff. You get full control over the environment — custom domains, your own database, and the ability to spin up or tear down the whole thing in seconds. You also manage the infrastructure.

Both are valid paths. Pick the one that fits how you work.


The Full Lifecycle

Here's the entire flow, start to finish:

You (one-time): Fill in a .env file (API tokens, domain, Cloudflare IDs). Run 4 secret config commands. SSH key is auto-detected.

Pulumi (automated): Creates tunnel → fetches token → configures routes → creates DNS → builds cloud-init → provisions server.

Server (automated): Boots → installs Docker → writes configs → starts containers.

Result: n8n live at your domain with HTTPS, zero-trust networking, and PostgreSQL. No SSH. No open ports. $3/month.

Done? pulumi destroy. Everything gone.


Want to try it? The full source code is on GitHub. Clone the repo, set your API keys, and deploy in 5 minutes.

Need help with your setup? I build and consult on automation infrastructure professionally. Reach out and let's talk about your use case.

Top comments (0)