Self-hosting n8n on a $5 DigitalOcean VPS: complete 2026 setup walkthrough
A 1 GB Ubuntu droplet, Docker Compose with n8n + Caddy, and the WEBHOOK_URL setting most tutorials skip - run in under 15 minutes.
TL;DR: Self-hosting n8n on a small DigitalOcean droplet takes three pieces: a 1 GB Ubuntu droplet, Docker with a two-service compose file (n8n plus Caddy), and a domain pointed at the droplet's IP.
The "$5 VPS" framing is a holdover - DigitalOcean's actual basic tiers are $4 (512 MB) and $6 (1 GB) as of 2026, and the $4 box will OOM the first time you import a non-trivial workflow. Use the $6 droplet, add a swap file, and the full setup below runs in under fifteen minutes.
What you'll build
A reachable n8n instance at https://n8n.example.com with HTTPS auto-renewed by Caddy, workflow data in SQLite on the droplet's disk, and webhook URLs that resolve correctly for inbound triggers. No Kubernetes, no managed Postgres, no load balancer. One droplet, one Docker network, one config file each for Caddy and Compose.
Prerequisites
- A DigitalOcean account and an SSH key uploaded to it.
- A domain (or subdomain) you can edit DNS for. Caddy needs an
Arecord pointing at the droplet's public IP before you start the stack, otherwise the ACME challenge fails. - About $6 a month for the droplet, plus a few dollars a year for the domain.
Step 1 - Create the droplet
From the DigitalOcean control panel, create a new droplet with these settings:
- Image: Ubuntu 24.04 (LTS) x64.
- Plan: Basic, Regular SSD, $6/month (1 GB RAM, 1 vCPU, 25 GB SSD, 1 TB transfer).
- Region: closest to you for SSH latency; closest to your webhook senders for trigger latency. Pick one.
- Authentication: SSH key. Skip the password option.
While the droplet provisions, point an A record (for example n8n.example.com) at the IPv4 address shown in the droplet panel. DNS propagation is usually under a minute on a fresh record but can take an hour. Confirm with dig +short n8n.example.com before continuing - Caddy's automatic certificate issuance fails fast and noisy if the hostname does not yet resolve to the droplet.

Step 2 - Harden the box and add swap
SSH in as root, then create a non-root user, install updates, and add a 1 GB swap file. The swap is the difference between n8n surviving a workflow import and the kernel killing the Node process.
adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
apt update && apt -y upgrade
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enableReconnect as deploy and confirm free -h shows the 1 GB of swap. You can now disable root SSH login by editing /etc/ssh/sshd_config if you want; for a single-user box, the firewall is the load-bearing control.
Step 3 - Install Docker
The official Docker apt repo is the only install path n8n's docs guarantee. Snap and the distro's docker.io package both lag versions and have caused webhook routing weirdness in the past.
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker
docker --versionConfirm docker run --rm hello-world prints the welcome message before continuing.
Step 4 - Write the Compose file and Caddyfile
Create ~/n8n/docker-compose.yml:
services:
n8n:
image: docker.n8n.io/n8nio/n8n:latest
restart: unless-stopped
environment:
- N8N_HOST=n8n.example.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://n8n.example.com/
- GENERIC_TIMEZONE=UTC
- N8N_RUNNERS_ENABLED=true
volumes:
- n8n_data:/home/node/.n8n
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
volumes:
n8n_data:
caddy_data:
caddy_config:And ~/n8n/Caddyfile:
n8n.example.com {
reverse_proxy n8n:5678
}Two things to flag. First, WEBHOOK_URL is the single setting that decides whether webhook trigger nodes show a usable public URL or a useless http://localhost one. Get it wrong and external services calling your trigger return 404 even though n8n is healthy. Second, N8N_RUNNERS_ENABLED=true opts in to the new task-runner architecture; it will become the default in a coming release per the n8n task runners docs, so set it now to avoid a deprecation warning later.

Step 5 - Start the stack
cd ~/n8n
docker compose up -d
docker compose logs -f caddyCaddy logs should print certificate obtained successfully within thirty seconds. If you see no such host or connection refused, your DNS record is not yet pointing at the droplet - fix DNS, then run docker compose restart caddy.
Open https://n8n.example.com. You will see n8n's owner-account setup screen. Create the first user; this account is the admin and is stored in SQLite at /home/node/.n8n/database.sqlite inside the n8n volume. Save the credentials before clicking through.
Step 6 - Verify webhooks resolve
Create a Webhook trigger node in any new workflow, set HTTP method to GET, and copy the Production URL. It should start with https://n8n.example.com/webhook/. From a separate machine:
curl -i https://n8n.example.com/webhook/<your-test-id>A 200 response with {"message":"Workflow was started"} confirms the trigger fires and that WEBHOOK_URL is set correctly. A 404 here almost always means the workflow is not active yet - flip the toggle in the top right of the editor.
How to back up and update
The entire instance state lives in the n8n_data Docker volume. Back it up with:
docker run --rm -v n8n_n8n_data:/data -v $PWD:/backup alpine \
tar czf /backup/n8n-$(date +%F).tar.gz -C /data .Schedule that with a cron job and rsync the tarball off the droplet (S3, Backblaze B2, anywhere not on the same machine). Updates are docker compose pull && docker compose up -d; pin the image tag to a specific version like n8nio/n8n:1.74.0 if you want to control when n8n upgrades.
FAQ
Will n8n run on a $4 DigitalOcean droplet?
Technically yes, but the 512 MB box runs out of memory the first time you import a workflow with more than a few dozen nodes, or when the n8n editor builds a complex expression preview. The OOM killer takes Node down and Docker restarts it, which looks identical to a webhook outage. Save yourself the debugging session and use the $6 / 1 GB tier with a 1 GB swap file.
SQLite or Postgres for self-hosted n8n?
SQLite is the default and is fine up to a few hundred thousand executions per month on a single-droplet setup. Switch to Postgres when you hit concurrency limits (multiple writes blocking the editor) or when you want point-in-time backups. The migration is documented in the n8n supported databases reference.
Do I need Docker, or can I install n8n with npm?
You can npm install -g n8n on the droplet directly, but you give up reproducible upgrades and lose the Caddy reverse-proxy pattern shown above. The Docker setup is also the path n8n's own docs treat as the supported one, which matters when you open a GitHub issue.
How do I update n8n without losing workflows?
Workflows live in the n8n_data volume, which survives docker compose pull and docker compose up -d. Take the volume backup above before any update so you can roll back by re-importing the tarball.
Is self-hosted n8n actually free?
The Community edition is source-available under the n8n Sustainable Use License and has no per-execution fee. You pay for the droplet ($6/mo) and the domain. Some advanced features (SSO, external secrets, log streaming) are gated to the paid Enterprise tier - the n8n community vs enterprise comparison lists the split.