How to Self-Host n8n for Free Forever on Oracle Cloud

Search for a command to run...

No comments yet. Be the first to comment.
Struggling with burnout and constant notifications? Here is a deep dive into my productivity setup, how I manage distractions with DigitalZen

Introduction In Kubernetes, Pod Priority and Preemption is a powerful scheduling feature that ensures critical workloads are placed and maintained on your cluster, even when resources are scarce. With this mechanism, Kubernetes can automatically pree...

🧠 Why Terraform for Proxmox? While Proxmox has a great web UI, infrastructure-as-code lets you: Automate repeatable VM deployments Keep configurations under version control Easily spin up multi-VM Setups Reduce human error 👋 Quick heads-up! T...

Whether you’re just dipping your toes into Terraform or you’ve been automating your infrastructure for years, Hooking up Terraform with Proxmox is a simple step that unlocks a lot of automation power. By the end of this guide, you’ll have learned how...

n8n stands out as one of the most powerful open-source low-code AI workflow automation tools available. While cloud-hosted n8n can get expensive quickly, Oracle Cloud's Always Free tier offers an incredible opportunity to run n8n completely free, forever.
In this comprehensive guide, I'll walk you through setting up n8n on Oracle Cloud Infrastructure (OCI) using their generous Always Free tier, which includes compute instances that never expire.
Oracle Cloud's Always Free tier is genuinely impressive:
2 AMD-based Compute VMs with 1/8 OCPU and 1 GB memory each
Up to 4 Arm-based Ampere A1 cores and 24 GB of memory (can be used as one VM or split)
200 GB total Block Volume storage
10 GB Object Storage
Always Free - no time limits, no credit expiration
The Arm-based instances are particularly powerful for running n8n, offering excellent performance for automation workflows.
For a more detailed overview of the Oracle Free Tier, refer to Oracle Free Tier & Always Free Resources.
Before we begin, you'll need:
Oracle Cloud account (free sign-up)
A registered Domain name (recommended for HTTPS)
This setup demonstrates how to run n8n securely inside an OCI Compute VM using Docker and Traefik as the reverse proxy.
A user accesses n8n.example.com, which resolves via DNS to the VM’s public IP.
Requests on ports 80/443 reach the Traefik container inside the VM.
Traefik forwards HTTPS traffic through the traefik-public network to the n8n container.
Traefik manages SSL/TLS certificates automatically with Let’s Encrypt CA.
Certificates are issued and renewed using the ACME TLS challenge.
All certificates are stored securely in ./letsencrypt/acme.json.
Traefik integrates with the Docker Socket to dynamically discover running containers.
This eliminates manual configuration whenever services are added or updated.
n8n container hosts the workflow automation platform.
Postgres container provides persistent database storage, connected via the n8n-network (port 5432).
All components (Traefik, n8n, Postgres) run as Docker containers inside a single OCI Compute VM.
Networking is logically separated using Docker networks (traefik-public and n8n-network).

Visit oracle.com/cloud/free
Sign up for a free account
Complete the verification process (requires credit card for verification, but won't be charged)
Wait for account activation
Create a Virtual Cloud Network
Log into your OCI Console
Navigate to Networking → Virtual cloud networks


Create a /24 subnet inside the VCN where the VM instance will be connected
10.0.1.0/24
Create it as a Public Subnet

Create a Internet gateway for the VCN


Create the VM Instance
Navigate to Compute → Instances
Click Create Instance
Image and Shape:
Image: Ubuntu 22.04 LTS (Always Free-eligible)
Shape: VM.Standard.A1.Flex (Arm-based)
OCPU: 2 (or all 4 if you want maximum performance)
Memory: 12 GB (or up to 24 GB)


In the Networking tab, Select the VCN & subnet that we already created at the beginning

SSH Keys:
Generate a new key pair and download both public and private keys
Keep the private key secure - you'll need it to access your server
Click Create and wait for the instance to provision
Configure Network Security
Go to Networking → Virtual Cloud Networks
Click on your VCN
Click on Security Lists → Default Security List
Add ingress rules:
Port 22 (SSH): 0.0.0.0/0
Port 80 (HTTP): 0.0.0.0/0
Port 443 (HTTPS): 0.0.0.0/0
Port 5678 (n8n): 0.0.0.0/0 (temporary, we'll remove this later)
1. Note your instance's public IP address
2. Connect via SSH:
bash ssh -i /path/to/your/private-key ubuntu@YOUR_PUBLIC_IP
For Windows users with PuTTY, convert the private key to .ppk format first
Update the System
bash sudo apt update && sudo apt upgrade -y
Install Docker and Docker Compose
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
#install the latest version
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Add your user to docker group
sudo usermod -aG docker $USER
Reconnect to your server after logging out.
Prepare the project directories and files on the VM
~/n8n-oracle-cloud/
├── traefik/
│ └── acme.json # ACME storage for Let's Encrypt certificates
├── prod/
├── .env # Environment variables
└── docker-compose.yaml # Main Compose stack
🚀 All the code is available on my GitHub Repo!
👉 Clone this repository to your VM to get all the necessary code 🖥️💻
# Example: clone repo to home
cd ~
git clone https://github.com/sumitsaz23/n8n-docker-traefik-postgres.git n8n-oracle-cloud
cd n8n-oracle-cloud
Secure acme.json for Traefik
Traefik requires acme.json to be present and readable/writable by the Traefik container but with strict permissions (600)
# create acme file and set permissions
touch traefik/acme.json
chmod 600 traefik/acme.json
Create docker-compose.yml file:
services:
# -------------------------
# Postgres (self-managed)
# -------------------------
postgres:
image: postgres:16-alpine # Postgres 16 (lightweight alpine)
container_name: n8n_postgres
restart: unless-stopped
# Named volume for persistent DB files
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${DB_POSTGRESDB_USER}
- POSTGRES_DB=${DB_POSTGRESDB_DATABASE}
- POSTGRES_PASSWORD=${DB_POSTGRESDB_PASSWORD}
- PGDATA=/var/lib/postgresql/data/pgdata
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_POSTGRESDB_USER} -d ${DB_POSTGRESDB_DATABASE} || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
networks:
- n8n-network
mem_limit: 2g
cpus: 1.0
# -------------------------
# n8n main (web UI + webhooks)
# -------------------------
n8n:
image: n8nio/n8n:latest
container_name: n8n_main
restart: unless-stopped
depends_on:
- postgres
volumes:
# named volume for user-related files, credentials, workflows, logs, etc
- n8n_data:/home/node/.n8n
environment:
# Database (Postgres) - prefer file-based secret usage
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=${DB_POSTGRESDB_PORT}
- DB_POSTGRESDB_DATABASE=${DB_POSTGRESDB_DATABASE}
- DB_POSTGRESDB_USER=${DB_POSTGRESDB_USER}
- DB_POSTGRESDB_PASSWORD=${DB_POSTGRESDB_PASSWORD}
# n8n app settings
- N8N_PORT=5678
- N8N_PROTOCOL=https
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
- N8N_REINSTALL_MISSING_PACKAGES=true
- N8N_RUNNERS_ENABLED=true
- WEBHOOK_URL=https://${N8N_HOSTNAME} # actual public URL; override in .env
- GENERIC_TIMEZONE=${TZ} # e.g., "UTC" or "Asia/Kolkata"
# Basic auth for UI - use secret file variant
- N8N_BASIC_AUTH_ACTIVE=${N8N_BASIC_AUTH_ACTIVE}
- N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER}
- N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD}
networks:
- n8n-network
- traefik-public
ports:
# # bind to container local port only — Traefik will route external traffic
- 127.0.0.1:5678:5678
labels:
- "traefik.enable=true"
# Tell Traefik which network to use to connect to this service
- "traefik.docker.network=n8nstack_traefik-public"
# --- HTTPS Router ---
- "traefik.http.routers.n8n.rule=Host(`${N8N_HOSTNAME}`)"
- "traefik.http.routers.n8n.entrypoints=websecure"
- "traefik.http.routers.n8n.tls.certresolver=letsencrypt"
# Traefik headers middleware for better security
- traefik.http.routers.n8n.tls=true
- traefik.http.middlewares.n8n.headers.SSLRedirect=true
- traefik.http.middlewares.n8n.headers.STSSeconds=315360000
- traefik.http.middlewares.n8n.headers.browserXSSFilter=true
- traefik.http.middlewares.n8n.headers.contentTypeNosniff=true
- traefik.http.middlewares.n8n.headers.forceSTSHeader=true
- traefik.http.middlewares.n8n.headers.SSLHost=${N8N_HOSTNAME}
- traefik.http.middlewares.n8n.headers.STSIncludeSubdomains=true
- traefik.http.middlewares.n8n.headers.STSPreload=true
- traefik.http.routers.n8n.middlewares=n8n@docker
mem_limit: 4g
cpus: 2
# -------------------------
# Traefik (reverse proxy / TLS automation)
# -------------------------
traefik:
image: traefik:latest
container_name: traefik
restart: unless-stopped
command:
- --api.dashboard=true
- --api.insecure=false # Secure the dashboard
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=n8nstack_traefik-public # Specify network
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
# Let's Encrypt configuration
- --certificatesresolvers.letsencrypt.acme.httpchallenge=true
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
- --certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --log.level=INFO # Use INFO instead of DEBUG for production
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
#- traefik_data:/letsencrypt
- /home/ubuntu/traefik/acme.json:/letsencrypt/acme.json
- /home/ubuntu/traefik/log:/var/log/traefik
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- n8n-network
- traefik-public
mem_limit: 512m
cpus: 0.5
# -------------------------
# Networks & Volumes
# -------------------------
networks:
n8n-network:
driver: bridge
traefik-public:
driver: bridge
volumes:
pgdata:
name: n8n_pgdata
n8n_data:
name: n8n_data
create .env file
# -------------------------
# General (non-sensitive)
# -------------------------
COMPOSE_PROJECT_NAME=n8nstack #environment variable in Docker Compose used to define the project name for a set of Docker services
TZ=Asia/Kolkata # timezone for containers (set to your preferred timezone)
N8N_HOSTNAME=n8n.example.com # <-- Replace with your public domain (used for WEBHOOK_URL & Traefik rule)
LETSENCRYPT_EMAIL=email@example.com
# -------------------------
# Postgres settings
# -------------------------
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8nuser
DB_POSTGRESDB_PASSWORD=dbsupersecret # in Production , do not put passwords in .env
# -------------------------
# n8n auth / behavior
# -------------------------
N8N_BASIC_AUTH_PASSWORD=n8nsupersecret # in Production , do not put passwords in .env
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=admin
# Optional metrics/queue settings
N8N_METRICS=true
N8N_METRICS_INCLUDE_QUEUE_METRICS=true
# -------------------------
# Resource & tuning (example values)
# -------------------------
# For Postgres: max connections ≈ (typical) 100 (adjust in postgres.conf if needed)
DB_POOL_SIZE=20
Launch the docker compose config
docker-compose up -d
This will create the networks, volumes & the containers

Verify if n8n has successfully initiated
docker compose logs n8n

#Verify if traefik ACME is able to successfully get a certificate
docker compose logs -f traefik

Now check the n8n portal using your browser

Symptom: When you open https://n8n.example.com, your browser shows the Traefik default certificate instead of a valid Let’s Encrypt cert.
Causes & Fixes:
Resolver name mismatch: The resolver name in your labels must match the resolver defined in Traefik’s command args.
Traefik command:
--certificatesresolvers.myresolver.acme.email=you@example.com
--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
--certificatesresolvers.myresolver.acme.tlschallenge=true
Label must match:
- "traefik.http.routers.n8n.tls.certresolver=myresolver"
acme.json permissions: Ensure the file exists and is writable by Traefik:
touch ./traefik/acme.json
chmod 600 ./traefik/acme.json
Firewall/DNS: Ports 80/443 must be open, and n8n.example.com must resolve to your VPS IP.
N8N_EDITOR_BASE_URL is setSymptom: Traefik logs show certificate request failures, and n8n only loads behind the default cert. The issue appears right after setting N8N_EDITOR_BASE_URL=https://n8n.example.com/.
Cause: With TLS‑ALPN challenge, Traefik passes the ACME validation request through to the backend. If n8n enforces HTTPS at this point, the validation breaks.
Fixes:
Option A: Deploy without N8N_EDITOR_BASE_URL until the cert is issued, then set it and restart n8n.
Option B (better): Switch to HTTP‑01 challenge, which bypasses n8n entirely during validation.
--certificatesresolvers.le.acme.httpchallenge.entrypoint=web
Option C: Use DNS‑01 challenge if your DNS provider supports it (best for Cloudflare/Route53).