const tunnel = new cloudflare.ZeroTrustTunnelCloudflared("n8n-tunnel", { accountId: cfAccountId, name: "n8n-tunnel", configSrc: "cloudflare", tunnelSecret: crypto.randomBytes(32).toString("base64")
});
const tunnel = new cloudflare.ZeroTrustTunnelCloudflared("n8n-tunnel", { accountId: cfAccountId, name: "n8n-tunnel", configSrc: "cloudflare", tunnelSecret: crypto.randomBytes(32).toString("base64")
});
const tunnel = new cloudflare.ZeroTrustTunnelCloudflared("n8n-tunnel", { accountId: cfAccountId, name: "n8n-tunnel", configSrc: "cloudflare", tunnelSecret: crypto.randomBytes(32).toString("base64")
});
const tunnelToken = cloudflare.getZeroTrustTunnelCloudflaredTokenOutput({ accountId: cfAccountId, tunnelId: tunnel.id
});
const tunnelToken = cloudflare.getZeroTrustTunnelCloudflaredTokenOutput({ accountId: cfAccountId, tunnelId: tunnel.id
});
const tunnelToken = cloudflare.getZeroTrustTunnelCloudflaredTokenOutput({ accountId: cfAccountId, tunnelId: tunnel.id
});
const dnsRecord = new cloudflare.DnsRecord("n8n-dns", { zoneId: cfZoneId, name: domain, type: "CNAME", content: pulumi.interpolate`${tunnel.id}.cfargotunnel.com`, proxied: true
});
const dnsRecord = new cloudflare.DnsRecord("n8n-dns", { zoneId: cfZoneId, name: domain, type: "CNAME", content: pulumi.interpolate`${tunnel.id}.cfargotunnel.com`, proxied: true
});
const dnsRecord = new cloudflare.DnsRecord("n8n-dns", { zoneId: cfZoneId, name: domain, type: "CNAME", content: pulumi.interpolate`${tunnel.id}.cfargotunnel.com`, proxied: true
});
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
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
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
# 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
# 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
# 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
pulumi destroy
pulumi destroy
pulumi destroy - 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 - 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. - Installs Docker Engine
- Writes docker-compose.yml with all secrets baked in
- Writes the tunnel token to .env
- Runs docker compose up -d - 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+