Paying hundreds of dollars a month for cloud automation platforms just to run simple API workflows is a massive waste of money.
Today, we are going to spin up a production-ready, self-hosted n8n instance using Docker Compose, PostgreSQL, and Caddy for automatic SSL.
I have run this exact setup on a cheap $6 VPS for over a year, handling thousands of daily webhooks without a single crash. Here is exactly how to do it yourself.

Why SQLite is a Trap for Production n8n
By default, n8n runs on SQLite. It is fine for testing on your laptop, but do not use it on a live server.
When you start running multiple workflows concurrently, SQLite will throw “database is locked” errors. I learned this the hard way when three simultaneous webhooks from Stripe locked my database, causing n8n to drop the payloads entirely. If you want to build advanced integrations, you need a database that handles concurrent writes.
PostgreSQL handles high-throughput writes without breaking a sweat. If you are serious about reliability, pairing n8n with a dedicated Postgres container is the only way to go. It ensures your workflow executions are saved safely, even when your server gets slammed with traffic.
Preparing Your Server Environment
Before we write any configuration, you need a Linux VPS. I recommend Ubuntu 22.04 with at least 2GB of RAM. You can technically run n8n on 1GB of RAM, but Node.js is hungry, and it will crash if your workflows process large JSON payloads.
First, SSH into your server and update your system packages.
sudo apt update && sudo apt upgrade -y
Next, install Docker and the Docker Compose plugin. If you do not have them installed yet, run this official installation script:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
Verify that Docker is running and configured correctly by checking the version:
docker compose version
Now, let’s create a dedicated directory for our configuration files. Keeping everything in one place makes backups and migrations much easier. I always use /opt/n8n for my self-hosted setups, similar to how I organize my server when I self-host Umami Analytics.
sudo mkdir -p /opt/n8n
sudo chown -R $USER:$USER /opt/n8n
cd /opt/n8n
Writing the Docker Compose Configuration
We will use three services in our configuration: PostgreSQL for the database, Caddy as our reverse proxy for automatic SSL certificates, and n8n itself. Caddy is fantastic because it handles Let’s Encrypt certificates automatically without needing complex Certbot cron jobs.
Create a file named docker-compose.yml inside your /opt/n8n folder:
nano docker-compose.yml
Paste the following configuration into the file:
version: '3.8' volumes: db_storage: n8n_storage: caddy_data: caddy_config: services: db: image: postgres:16-alpine restart: always environment: - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB} volumes: - db_storage:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 5s timeout: 5s retries: 5 n8n: image: docker.n8n.io/n8nio/n8n:latest restart: always ports: - "127.0.0.1:5678:5678" environment: - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=db - DB_POSTGRESDB_PORT=5432 - DB_POSTGRESDB_DATABASE=${POSTGRES_DB} - DB_POSTGRESDB_USER=${POSTGRES_USER} - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD} - N8N_HOST=${N8N_HOST} - N8N_PORT=5678 - N8N_PROTOCOL=https - WEBHOOK_URL=https://${N8N_HOST}/ - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY} - EXECUTIONS_PROCESS=main volumes: - n8n_storage:/home/node/.n8n depends_on: db: condition: service_healthy caddy: image: caddy:2-alpine restart: always ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data - caddy_config:/config depends_on: - n8n
This setup uses named volumes so your workflow data and database records persist when you restart or update the containers. Note the healthcheck on the database: n8n will wait to start until Postgres is fully ready to accept connections.
Configuring Environment Variables (.env)
We need to create an .env file to store our passwords, domain name, and encryption keys. This keeps sensitive credentials out of our main configuration file.
First, generate a random 32-character string for your n8n encryption key. This key secures your API credentials inside the database. Run this command to generate one:
openssl rand -hex 32
Copy the output string. Now create your environment file:
nano .env
Add the following variables, replacing the placeholders with your actual values:
POSTGRES_USER=n8n_admin
POSTGRES_PASSWORD=your_super_secure_password_here
POSTGRES_DB=n8n_database
N8N_HOST=n8n.yourdomain.com
N8N_ENCRYPTION_KEY=paste_your_generated_openssl_key_here
Make sure your domain name’s A record points directly to your server’s public IP address before proceeding. If you do not set up your DNS first, Caddy will fail to generate your SSL certificate.
Setting Up Caddy for Automatic SSL
Caddy reads its routing rules from a file called Caddyfile. Let’s create it in the same directory:
nano Caddyfile
Add this simple block of configuration. Replace the domain with your actual subdomain:
n8n.yourdomain.com { reverse_proxy n8n:5678
}
This tells Caddy to listen on port 80 and 443 for your domain, fetch an SSL certificate from Let’s Encrypt, and forward all incoming traffic to our n8n container on port 5678.
Running the Stack and Verifying Logs
Now we are ready to launch our self-hosted automation server. Run the docker compose command in detached mode:
docker compose up -d
Docker will download the images and start the services. This usually takes about a minute. You can monitor the startup process by tailing the logs:
docker compose logs -f n8n
Look for a line that says: n8n ready on port 5678. Once you see that, open your browser and navigate to your domain (e.g., https://n8n.yourdomain.com). You will be greeted by the n8n setup screen where you can create your admin account.
The Gotcha: Fixing Webhook Timeout and Memory Limits
When I first set up n8n, my server crashed every time I ran a workflow that processed large files or loops. By default, n8n runs each execution in a separate process. On a small VPS, spawning these sub-processes quickly consumes all available RAM and triggers the Linux Out-Of-Memory (OOM) killer.
To fix this, we set EXECUTIONS_PROCESS=main in our docker-compose.yml environment variables. This forces n8n to run executions inside the main process instead of spawning new ones, reducing RAM usage by up to 60%.
Another common issue is webhooks failing because n8n does not know its own external URL. If you build workflows that wait for external webhooks, you must verify that your WEBHOOK_URL env variable is set correctly. If you are developing locally before pushing to production, you can use tools to test webhooks locally with Cloudflare Tunnels to verify your payloads.
If you process webhooks from external APIs, you should also secure them. Check out this guide on how to verify webhook signatures with HMAC to prevent unauthorized requests from triggering your self-hosted workflows.
Securing and Backing Up Your PostgreSQL Database
Since all your workflow configurations, credentials, and execution histories live inside PostgreSQL, you must back it up regularly. You do not want a corrupt server to wipe out weeks of building automations.
Here is a simple bash script to dump your database. Create a backup directory first:
mkdir -p ~/backups/n8n
You can run a one-off backup using this Docker command:
docker compose exec -t db pg_dumpall -U n8n_admin > ~/backups/n8n/db_backup_$(date +%F).sql
To automate this, add a cron job that runs this command every night at midnight. Your data will be safe, and you can easily restore it if you ever need to migrate servers.
Frequently Asked Questions
How do I update my self-hosted n8n instance?
To update n8n to the latest version, run these three commands in your /opt/n8n directory:
docker compose pull
docker compose up -d
docker image prune -f
This pulls the latest images, restarts the containers without losing your data, and cleans up the old, unused Docker images.
Can I run this on a $5 digital ocean droplet?
Yes, but you must configure a swap file. Node.js processes can spike in memory usage during updates or heavy workflows. A 1GB or 2GB swap file prevents the OS from killing your container during these spikes.
Why are my webhooks returning 404 errors?
This happens when the WEBHOOK_URL environment variable in your .env file does not match the exact URL you are using to access n8n. Double-check that it includes https:// and has a trailing slash.
Next Steps for Your Automation Stack
Now that your self-hosted n8n instance is running securely on Postgres, you can start building complex workflows without worrying about execution limits. If you want to compare other open-source automation platforms before committing fully, take a look at our guide to self-host Activepieces with Docker Compose to see how it fits your development needs.

