How to Self-Host n8n for Free on Oracle Cloud (2026 Guide)
A step-by-step walkthrough for running your own n8n instance on Oracle's Always Free tier - ARM VM, Docker, Postgres, automatic SSL, and backups, for free.
N8N Chat UI Team

You've built a workflow in n8n and you're tired of the cloud plan's execution limits, or you just don't want to pay a monthly fee for something that can run comfortably on a free server. Good news: Oracle Cloud's Always Free tier gives you a real ARM VM — not a toy — that can run n8n, Postgres, and a reverse proxy with automatic SSL, for £0/month. You only pay for the domain.
This guide walks through the whole setup: account, VM, firewall (the part everyone gets stuck on), Docker, DNS, deployment, backups, and how to stop Oracle reclaiming your instance for looking idle.
- A free ARM VM running n8n behind Caddy, with automatic HTTPS on your own domain
- Postgres as the backing database instead of SQLite
- Nightly backups of your workflows and credentials
- A setup that survives Oracle's idle-instance reclamation policy
Time: 60–90 minutes, assuming you get server capacity on the first try (more on that below).
Cost: £0/month for the server. You only pay for the domain.
Before you start: the 2026 free tier change
Oracle halved the Always Free Ampere allocation in June 2026. If you've read an older guide that says "4 OCPU / 24 GB," that figure is out of date.
- Ampere A1 (ARM) compute: 2 OCPU / 12 GB RAM total, across all A1 instances
- AMD micro instances: 2 × VM.Standard.E2.1.Micro (1/8 OCPU, 1 GB each)
- Block storage: 200 GB total, minimum 50 GB per boot volume
- Outbound data transfer: 10 TB/month
- Load balancer: 1 flexible LB at 10 Mbps
2 OCPU / 12 GB is still comfortably more than enough for n8n — it'll handle hundreds of executions a day without breaking a sweat. The AMD micro instances are not enough to run this stack; you want the ARM shape.
1. Always Free resources only exist in your tenancy's home region. You pick the home region at signup and you cannot change it later. Choose the one closest to you.
2. Idle instances get reclaimed. Oracle may delete your VM if, over a 7-day window, CPU, network, and memory all sit under 20% at the 95th percentile. The last section of this guide covers how to avoid that.
Step 1: Create the Oracle Cloud account
- Go to Oracle's cloud free tier signup page and register.
- You'll need a card for identity verification. Oracle takes a small pre-auth (roughly £1) and reverses it — Always Free resources never actually charge the card.
- Set your home region carefully. This choice is permanent.
- Verification can take anywhere from minutes to a couple of days. If a card gets rejected for no clear reason, trying a different card usually fixes it.
You start on a 30-day trial with credit. When that expires, the account drops to Always Free only, and your instance keeps running as long as it's within the free limits.
Step 2: Generate an SSH key
On your machine:
ssh-keygen -t ed25519 -C "n8n-oracle" -f ~/.ssh/n8n_oracle
cat ~/.ssh/n8n_oracle.pubKeep that public key handy — you'll paste it into the console in the next step.
Step 3: Create the VM
In the OCI console: Menu → Compute → Instances → Create instance.
- Name: n8n-prod
- Placement: any availability domain (try each if one is out of capacity)
- Image: Ubuntu 24.04 (or Oracle Linux 9, if you prefer)
- Shape: VM.Standard.A1.Flex — 2 OCPU, 12 GB RAM
- Boot volume: 50 GB (default is fine)
- Networking: create a new VCN, assign a public IPv4 address
- SSH keys: paste the public key from Step 2
Click Create.
If the only free shape you can see is VM.Standard.E2.1.Micro
The Ampere shapes are on a separate tab. Click Change shape → Ampere → VM.Standard.A1.Flex, then set OCPUs to 2 and memory to 12 GB. The "Always Free-eligible" label only appears once you've dialled the allocation down inside the free limits — at the default setting it looks like a paid shape, which is why a lot of people assume ARM isn't available to them.
Also double check the region selector is on your home region — Always Free resources exist nowhere else. And don't settle for the E2.1.Micro: at 1/8 OCPU and 1 GB RAM, it can't run n8n, Postgres, Caddy, and a task runner together.
If you get "Out of host capacity"
This is the single most common blocker. ARM capacity in popular regions is frequently exhausted. In order of effort:
- Try a different availability domain. Regions with AD-1/2/3 often have capacity in only one of them.
- Retry on a loop. Capacity frees up constantly — most people succeed within a few hours to a few days of retrying.
- Switch to Pay As You Go. Counter-intuitively, PAYG accounts get priority for capacity, and Oracle still doesn't charge for anything inside the Always Free limits. Set a $0 budget alert so you'd know immediately if you ever crossed a line.
Step 4: Open the firewall — both layers
This is the step almost everyone gets wrong first time. Oracle blocks traffic in two separate places, and you have to open both.
Layer 1: the VCN security list
Menu → Networking → Virtual Cloud Networks → your VCN → Subnets → your subnet → Security Lists → default list → Add Ingress Rules. Add two rules:
- 0.0.0.0/0, TCP, port 80
- 0.0.0.0/0, TCP, port 443
Layer 2: iptables on the instance itself
Oracle's Ubuntu images ship with restrictive iptables rules that ignore whatever the security list says. SSH in first:
ssh -i ~/.ssh/n8n_oracle ubuntu@YOUR_SERVER_IPThen add the rules:
sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 80 -j ACCEPT
sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 443 -j ACCEPT
sudo netfilter-persistent saveVerify both ports are accepted:
sudo iptables -L INPUT -n --line-numbers | grep -E "dpt:(80|443)"If you skip layer 2, your site will time out and everything else will look correctly configured — the security list is open, DNS resolves, Caddy is running. Don't skip it.
Step 5: Basic hardening
sudo apt update && sudo apt upgrade -y
# Automatic security patches
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgradesDisable password logins so only your key works:
sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart sshOptional but worth doing — fail2ban for SSH:
sudo apt install -y fail2ban
sudo systemctl enable --now fail2banStep 6: Install Docker
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker
docker --version && docker compose versionThe official ARM64 images work fine here — no special handling needed for the Ampere architecture.
Step 7: Point your domain at the server
If you're using Cloudflare for DNS, add an A record:
- Type: A
- Name: n8n
- IPv4 address: your server's IP
- Proxy status: DNS only (grey cloud)
This is the Cloudflare equivalent of the iptables trap in Step 4. With the orange cloud on, DNS resolves to a Cloudflare edge IP, Let's Encrypt's HTTP-01 challenge reaches Cloudflare instead of your Caddy container, and certificate issuance fails with an error that looks nothing like a DNS problem.
You can put Cloudflare in front later for its WAF or caching — just do it after the stack is working, and set SSL/TLS mode to Full (strict) when you do. Anything less and Cloudflare will either refuse the origin cert or serve your site over an unencrypted hop to it.
Wait for the record to resolve before continuing:
dig +short n8n.your-domain.comDon't move on until that returns your server's actual IP. If you get something in the 104.x.x.x or 172.67.x.x range, the proxy is still on.
Step 8: Deploy n8n
mkdir -p ~/n8n && cd ~/n8n.env
cat > .env <<'EOF'
# --- Domain ---
DOMAIN_NAME=your-domain.com
SUBDOMAIN=n8n
GENERIC_TIMEZONE=Europe/London
# --- n8n version: pin it, don't use :latest ---
N8N_VERSION=2.32.7
# --- Postgres ---
POSTGRES_USER=postgres
POSTGRES_PASSWORD=CHANGE_ME_ROOT
POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=n8n
POSTGRES_NON_ROOT_PASSWORD=CHANGE_ME_APP
# --- Secrets ---
N8N_ENCRYPTION_KEY=CHANGE_ME_ENCRYPTION
RUNNERS_AUTH_TOKEN=CHANGE_ME_RUNNER
EOFGenerate real secrets and substitute them in:
for k in ROOT APP ENCRYPTION RUNNER; do
echo "$k: $(openssl rand -hex 32)"
doneEdit .env with nano .env, replacing each CHANGE_ME_* value, then lock the file down:
chmod 600 .envSave N8N_ENCRYPTION_KEY somewhere safe — a password manager, not a note file. It encrypts every credential you store in n8n. Lose it and every credential becomes unreadable, even with a full database backup.
init-data.sh
Creates the non-root Postgres user n8n connects as:
cat > init-data.sh <<'EOF'
#!/bin/bash
set -e;
if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
EOSQL
else
echo "SETUP INFO: No Environment variables given!"
fi
EOF
chmod +x init-data.shCaddyfile
mkdir -p caddy_config
cat > caddy_config/Caddyfile <<'EOF'
n8n.your-domain.com {
reverse_proxy n8n:5678 {
flush_interval -1
}
}
EOFflush_interval -1 turns off response buffering, which n8n needs for live execution streaming to work in the editor.
docker-compose.yml
volumes:
db_storage:
n8n_storage:
caddy_data:
caddy_config:
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy_config/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- n8n
postgres:
image: postgres:16
restart: always
environment:
- POSTGRES_USER
- POSTGRES_PASSWORD
- POSTGRES_DB
- POSTGRES_NON_ROOT_USER
- POSTGRES_NON_ROOT_PASSWORD
volumes:
- db_storage:/var/lib/postgresql/data
- ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh:ro
healthcheck:
test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 10
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
restart: always
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
- N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- NODE_ENV=production
- WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
- N8N_EDITOR_BASE_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_PROXY_HOPS=1
- N8N_RUNNERS_MODE=external
- N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
volumes:
- n8n_storage:/home/node/.n8n
- ./local_files:/files
depends_on:
postgres:
condition: service_healthy
n8n-runner:
image: n8nio/runners:${N8N_VERSION}
restart: always
environment:
- N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
depends_on:
- n8nNotice there's no ports: block on the n8n service. Only Caddy is exposed to the internet — n8n is only reachable through the reverse proxy, over the internal Docker network. Publishing port 5678 directly would bypass Caddy and expose n8n over plain HTTP.
N8N_PROXY_HOPS=1 tells n8n it's sitting behind one proxy, so it reads the real client IP from X-Forwarded-For — this matters for rate limiting and login security to behave correctly.
Start it
mkdir -p local_files
docker compose up -d
docker compose logs -f n8nWatch for Editor is now accessible via: https://n8n.your-domain.com. Caddy fetches a Let's Encrypt certificate automatically on the first request — no certbot, no cron renewal to manage.
Open the URL and create the owner account immediately. Until you do, anyone who finds the URL can claim it.
Step 9: Backups
The Docker volumes hold everything that matters. This script dumps Postgres and the n8n data directory, keeping 14 days of history:
cat > ~/n8n/backup.sh <<'EOF'
#!/bin/bash
set -e
BACKUP_DIR=/home/ubuntu/n8n-backups
STAMP=$(date +%F-%H%M)
mkdir -p "$BACKUP_DIR"
cd /home/ubuntu/n8n
# Postgres logical dump
docker compose exec -T postgres pg_dump -U postgres n8n | gzip > "$BACKUP_DIR/n8n-db-$STAMP.sql.gz"
# n8n data dir (encryption key, binary data)
docker run --rm \
-v n8n_n8n_storage:/data:ro \
-v "$BACKUP_DIR":/backup \
alpine tar czf "/backup/n8n-files-$STAMP.tar.gz" -C /data .
# Keep 14 days
find "$BACKUP_DIR" -name "n8n-*" -mtime +14 -delete
EOF
chmod +x ~/n8n/backup.shCheck the volume name matches your setup before relying on this:
docker volume ls | grep n8nSchedule it nightly at 03:00:
(crontab -l 2>/dev/null; echo "0 3 * * * /home/ubuntu/n8n/backup.sh >> /home/ubuntu/n8n-backup.log 2>&1") | crontab -Get the backups off the box. A local backup doesn't help if Oracle reclaims the instance. Either enable OCI block volume backups (five free volume backups are included) via Block Storage → Boot Volumes → Boot Volume Backups, or sync the folder to Object Storage, Backblaze, or Google Drive with rclone.
Test a restore at least once. An untested backup is a guess.
Step 10: Don't get reclaimed
Oracle reclaims Always Free instances that look idle. n8n ticking over quietly with a handful of workflows can genuinely fall below all three thresholds — CPU, network, and memory all under 20% at the 95th percentile, over 7 days.
This is a real policy, and a genuinely idle box is at real risk. A modest keep-warm job is the standard mitigation — a few minutes of load each hour is enough to stay clear of the threshold.
cat > ~/keepalive.sh <<'EOF'
#!/bin/bash
# ~3 min of load on one core, plus a little network traffic
timeout 180 dd if=/dev/urandom of=/dev/null bs=1M >/dev/null 2>&1
curl -s -o /dev/null https://www.google.com/generate_204
EOF
chmod +x ~/keepalive.sh
(crontab -l 2>/dev/null; echo "0 * * * * /home/ubuntu/keepalive.sh") | crontab -If your n8n workflows are already doing real work on a schedule, check actual usage first in Compute → Instances → your instance → Metrics — you may already clear the bar naturally and not need this at all.
And if your automations become genuinely important to the business, it's worth weighing the time cost of a surprise outage against the £4–5/month a small Hetzner box costs — one nobody can reclaim.
Day-to-day operations
cd ~/n8n
docker compose ps # status
docker compose logs -f n8n # follow logs
docker compose restart n8n # restart just n8n
docker compose down # stop everything
docker compose up -d # start everythingUpgrading n8n
cd ~/n8n
./backup.sh # always back up first
nano .env # bump N8N_VERSION
docker compose pull
docker compose up -d
docker compose logs -f n8nPin the version rather than using :latest — n8n ships breaking changes across majors, and you want upgrades to happen on your schedule, not the registry's.
Housekeeping
Execution history will fill the disk over months. In n8n's settings, set an execution data pruning policy (e.g. keep 14 days). Prune Docker periodically too:
docker system prune -af --volumes=falseTroubleshooting
- Site times out, no response: iptables rules missing on the instance (Step 4, layer 2)
- Caddy can't get a certificate: DNS not resolving yet, or port 80 blocked — Let's Encrypt validates over HTTP
- 502 Bad Gateway: n8n container not up — check
docker compose logs n8n - Postgres auth failures:
.envedited after first run — the DB was initialised with the old password - Webhooks return the wrong URL:
WEBHOOK_URLmismatch, or a trailing-slash issue - Executions hang or fail oddly: runner container down — check
docker compose logs n8n-runner - Instance vanished: reclaimed as idle, or the trial ended with resources over the free limits
Wrapping up
You've now got a free, self-hosted n8n instance with proper TLS, a real database, and backups — the same production setup you'd pay for on a managed plan, minus the monthly bill.
Now give your n8n workflow a proper front end.
Once your workflow's live, drop in a branded chat widget in minutes — no separate hosting, no extra infrastructure to maintain.