Self-host n8n on a $6 server
Follow this on your own server. Start free, no card needed
Every workflow automation tool that bills by execution turns “connect these two APIs” into a recurring cost that grows with exactly the thing you automated it to stop thinking about. n8n does the same job as Zapier or Make, visual workflows that call APIs without you writing and maintaining glue code, as open-source software you run yourself. By the end of this guide you’ll have your own n8n instance running on a $6/month server, reachable over HTTPS at your own domain, backed by Postgres instead of a single SQLite file, with backups and an upgrade path that doesn’t break on you.
What you’ll need
- A server. DigitalOcean’s Basic Droplet at $6.00/month gets you 1 vCPU, 1 GB RAM and 25 GB SSD, which is enough for n8n, Postgres and a reverse proxy running together on a single box for personal or small-team use. Any Ubuntu 24.04 VPS from another provider works the same way.
- A domain name (or a subdomain of one you already own), with access to its DNS settings.
- Docker and the Docker Compose plugin installed on the server (covered in Step 2).
- A terminal and SSH access to the server.
Step 1: Create the server and point a domain at it
Spin up an Ubuntu 24.04 droplet on DigitalOcean’s $6/month Basic plan (1 vCPU, 1 GB RAM, 25 GB SSD). Note its public IP address.
In your domain’s DNS settings, add an A record pointing a subdomain, for example n8n.yourdomain.com, at that IP:
Type: A
Name: n8n
Value: <your server's IP>
TTL: 300
DNS propagation can take a few minutes to a few hours. You should see dig n8n.yourdomain.com +short return your server’s IP before moving on, since the next steps depend on it.
Step 2: Install Docker
SSH into the server and install Docker Engine plus the Compose plugin:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
Log out and back in for the group change to take effect, then check both pieces are present:
docker --version
docker compose version
You should see a Docker version and a Compose version printed, with no “command not found” errors.
Step 3: Lay out the project and its secrets
Create a directory for the stack and an .env file to hold everything that shouldn’t live in the compose file itself:
mkdir -p ~/n8n && cd ~/n8n
Generate a real encryption key. n8n uses this to encrypt every credential stored in its database, so losing it means losing access to every saved credential, not just this install:
openssl rand -base64 32
Create .env with that key, your domain, a timezone, and a database password:
# .env
DOMAIN=n8n.yourdomain.com
GENERIC_TIMEZONE=America/New_York
N8N_ENCRYPTION_KEY=<paste the key you just generated>
POSTGRES_USER=n8n
POSTGRES_PASSWORD=<a long random password>
POSTGRES_DB=n8n
Set its permissions so only your user can read it:
chmod 600 .env
Step 4: Write the Docker Compose file
n8n ships a default SQLite database, which is fine for trying things out but not for an instance that runs around the clock with more than a handful of workflows. Postgres is the database n8n itself recommends once you’re past the “just testing” stage, so this setup uses it from the start.
Create docker-compose.yml:
services:
n8n:
image: n8nio/n8n:1.68.0
restart: unless-stopped
environment:
- N8N_HOST=${DOMAIN}
- N8N_PROTOCOL=https
- N8N_PORT=5678
- N8N_WEBHOOK_URL=https://${DOMAIN}/
- N8N_PROXY_HOPS=1
- N8N_RUNNERS_ENABLED=true
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
networks:
- n8n-net
postgres:
image: postgres:16.4-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
networks:
- n8n-net
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
- DOMAIN=${DOMAIN}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- n8n
networks:
- n8n-net
networks:
n8n-net:
volumes:
n8n_data:
postgres_data:
caddy_data:
caddy_config:
A few choices worth explaining:
- The n8n image is pinned to
1.68.0rather thanlatest. An automation tool that silently upgrades itself on a server restart, taking your workflows with it into whatever changed, is not a good trade. You upgrade on your own schedule, covered in Step 7. N8N_PROXY_HOPS=1andN8N_WEBHOOK_URLmatter because n8n sits behind Caddy here rather than facing the internet directly. Without them, n8n doesn’t know its own public HTTPS address and registers the wrong webhook URLs with external services.N8N_RUNNERS_ENABLED=trueturns on n8n’s task runner, which executes Code node scripts in an isolated process instead of inside n8n’s own main process. Skipping this on a 1.x release means any script in a Code node runs with no isolation at all, which matters the moment you’re running workflows you didn’t write yourself.- Only Caddy publishes ports to the host. n8n and Postgres are reachable only on the internal Docker network, not from the internet directly.
Step 5: Write the Caddyfile and start everything
Caddy handles the TLS certificate from Let’s Encrypt automatically, as long as DNS is already pointing at the server and ports 80 and 443 are reachable from the internet. Create Caddyfile in the same directory:
{$DOMAIN} {
reverse_proxy n8n:5678
}
Start the stack:
docker compose up -d
Watch it come up and check Caddy actually got a certificate:
docker compose logs -f caddy
You should see a line mentioning a certificate obtained for your domain, then visiting https://n8n.yourdomain.com in a browser should load n8n’s setup screen with a valid padlock, no certificate warning. Create your owner account there; this is n8n’s own first-run setup, not something this stack configures for you.
Step 6: Back up workflows, credentials and the encryption key
Three things need backing up: the Postgres database (your workflows, executions and credentials), the n8n_data volume (n8n’s own local state, including anything not in the database), and the .env file (your encryption key, without which the credentials in that database are unreadable).
A daily Postgres dump, kept off the server:
docker compose exec -T postgres pg_dump -U n8n n8n > "n8n-backup-$(date +%F).sql"
Put that in a cron job and copy the result somewhere other than this server, an object storage bucket, another machine, anywhere that isn’t this droplet. A backup that lives only on the server it’s backing up doesn’t survive the failure mode that actually destroys a server.
For the volume and the .env file:
docker run --rm -v n8n_n8n_data:/data -v $(pwd):/backup alpine \
tar czf /backup/n8n-data-backup.tar.gz -C /data .
cp .env ~/n8n-env-backup-$(date +%F)
Keep that .env backup somewhere separate and access-controlled; it contains both your database password and your encryption key.
Step 7: Upgrade n8n without surprises
Check the version you’re currently running, read n8n’s release notes for anything between that version and the one you’re moving to, then bump the tag in docker-compose.yml:
image: n8nio/n8n:1.69.0
Pull and restart just that service:
docker compose pull n8n
docker compose up -d n8n
You should see n8n’s logs show a clean startup with no error about a failed database migration, and your existing workflows should still be there when you log back in. If a migration does fail, your Postgres backup from Step 6 is the way back to the previous version.
The shortcut: do it with Railyard
Everything above, a Postgres-backed n8n with a generated encryption key, a persistent data volume, and HTTPS already wired up, is also a one-click template:
https://app.railyard.run/deploy?template=n8n
That single-container template is the right fit for the same case the manual setup above covers: one user or a small team running internal automation. If you later outgrow it, running enough workflow volume that webhook delivery and execution start competing for the same process, n8n’s own queue mode architecture splits those two jobs apart, and that’s available as a separate stack: pick n8n (queue mode) in the templates catalog rather than deploying it directly from a link, since a stack template needs you to confirm its services first. It runs n8n’s web and worker processes against their own Postgres and a password-protected Redis for the job queue.
If you’d rather start from Railyard generally before deciding on n8n specifically, the quickstart covers the basics, and databases and backups explains how backups work across every template, not just this one. Deploy button covers the ?template= link format itself if you want to embed one elsewhere. For a wider comparison of what a $6 droplet actually gets you against a managed alternative, see hosting costs.
Troubleshooting
Caddy never gets a certificate, or the browser shows a certificate warning. This is almost always DNS, not Caddy. Run dig n8n.yourdomain.com +short from your own machine and confirm it returns the server’s IP, not an old one, and not nothing. Let’s Encrypt also needs port 80 reachable from the internet for the initial validation, so check your cloud provider’s firewall or security group allows inbound 80 and 443, not just 443.
docker compose up fails with “port is already allocated.” Something else on the box is already bound to 80 or 443, commonly a default Nginx or Apache install. Find it with sudo ss -tlnp | grep -E ':80|:443' and stop that service, or change which ports Caddy publishes if you genuinely need both running.
n8n logs show webhook URLs pointing at the wrong address, or at http instead of https. Check N8N_HOST, N8N_PROTOCOL and N8N_WEBHOOK_URL in the compose file match your real domain and https, and that N8N_PROXY_HOPS=1 is set. Without it, n8n doesn’t trust the forwarded headers Caddy is sending and falls back to guessing its own address.
Postgres container keeps restarting, or n8n can’t connect to the database. Check docker compose logs postgres for the actual error first. A mismatched POSTGRES_PASSWORD between the Postgres service and the DB_POSTGRESDB_PASSWORD n8n is using to connect is the most common cause, usually from editing .env after the Postgres volume was already initialized with the old password; Postgres only reads those environment variables on the very first startup with an empty data directory.
The container starts then immediately exits, or the volume shows permission errors. This usually shows up if you bind-mount a host directory instead of a named Docker volume for /home/node/.n8n. The n8n image runs as a non-root user, and a host directory created by root won’t be writable by it. Named volumes, as used in the compose file above, avoid this entirely because Docker manages their ownership.
The server runs out of memory under real load. A 1 GB droplet running n8n, Postgres and Caddy together has little headroom. Add a swap file as a buffer (fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile), and if you’re running workflows that process large payloads or many executions concurrently, move up to a 2 GB droplet rather than relying on swap as a permanent fix.
FAQ
Do I need Postgres, or is SQLite enough? SQLite, n8n’s default, works for trying it out, but it doesn’t handle more than a handful of concurrent workflows well and isn’t what n8n recommends once an instance is actually relied on. Postgres from the start, as in this guide, avoids a migration later.
Can I run this without a domain, just the server’s IP? Caddy’s automatic HTTPS needs a real domain name; Let’s Encrypt doesn’t issue certificates for bare IP addresses. You can run over plain HTTP on an IP for quick testing by pointing a browser straight at port 5678, but don’t store real credentials in an instance that isn’t behind HTTPS.
How much does this actually cost to run? The server is $6/month. n8n itself is free, open-source software with no execution limits of its own; you’re paying for the box it runs on, not for what runs on it.
What happens to my workflows if I lose the encryption key? Every credential n8n has stored becomes unreadable. Workflow structure survives, but anything that needed a saved API key or OAuth token will need those credentials re-entered. This is the entire reason Step 6 backs up .env separately from the database.
Is the single-container setup here the same as what Railyard’s n8n template runs? Close, with one real difference: Railyard’s template doesn’t include Postgres by default, since most single-user installs don’t need it from day one. The architecture, one n8n container with a generated encryption key and a persistent data volume, is the same idea; this guide adds Postgres because it’s the more honest default for anything you intend to keep running.
Either path gets you the same thing in the end: your own automation server, your own data, and a bill that doesn’t move when your workflows do more work.
Why people pick Railyard for this
- Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
- One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
- One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
- Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
- Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.