
Leads arrive through a web form, someone copies them into a spreadsheet, and the follow-up email goes out when somebody remembers. Instead of a paid SaaS tool for every step, you want one server that runs the workflows and keeps the customer list. This guide builds it with n8n, Baserow, PostgreSQL and Caddy, sizes it from each project's docs, and ends with the part most setups skip: who keeps it running. Commands are for Ubuntu 24.04 and Debian 12.
Quick answer: Run four containers from one Compose file: n8n for workflows and forms, Baserow as the customer table, PostgreSQL 17 with a database and login per app, and Caddy, the only container with open ports, which handles the HTTPS certificates. Ubuntu needs 1 GB and Baserow 2 GB before n8n loads anything, so 4 GB is the floor and 8 GB the comfortable size. Claim both admin accounts at first start, copy nightly dumps and volume archives off the server, and watch both sites and the backup from a monitor elsewhere. Then name one person to update it monthly, and a deputy.
What Runs in the Stack
| Job | Tool | Why this one |
|---|---|---|
| Workflows | n8n | Free Community edition for internal business use |
| Web forms | n8n Form Trigger or a Baserow form | No extra app to run |
| Customer table | Baserow | Open Source Edition under the MIT licence |
| Database | PostgreSQL 17 | One server, a database and login per app |
| HTTPS | Caddy | Gets and renews certificates by itself |
| Uptime checks | Uptime Kuma | Runs on another machine |
| Backups | pg_dump, tar, restic | Copies leave the server nightly |
n8n's Form Trigger "generates the form web page for you to use" and runs a workflow per submission. In a Baserow form view, "their responses create new rows in your table", and visitors with the share link need no account. n8n's Baserow node creates, reads, updates and deletes rows.
Need deals and a sales pipeline instead of a table? Twenty, an open source CRM mostly under AGPLv3, runs a server, a worker, PostgreSQL and Redis, and its docs ask for "at least 2GB of RAM". This guide uses Baserow, which runs as one container.
How Big the VPS Must Be
| Part | What its docs say |
|---|---|
| Ubuntu Server 24.04 | 1 GB RAM minimum for cloud images, 3 GB or more suggested; 25 GB disk or more suggested |
| Baserow image | "at minimum 2GB of RAM", according to Baserow's community FAQ (2023) |
| n8n | No minimum; memory follows the data your workflows load |
| PostgreSQL 17 | shared_buffers is "typically 128 megabytes" by default |
| Caddy | No figure published |
n8n's Compose guide asks for at least 4 GB of RAM and 2 vCPUs, but that covers its AI sandbox, a Docker-in-Docker container this stack leaves out. n8n itself "doesn't restrict the amount of data each node can fetch and process", so one large export can need more RAM than the rest.
The system and Baserow alone take 3 GB, leaving 1 GB of a 4 GB plan for n8n and PostgreSQL. Our RAM guide explains why to stay under about three quarters of the RAM, which points to 8 GB. On 4 GB, add BASEROW_RUN_MINIMAL: "1" to the baserow environment, a setting Baserow offers "to lower the memory usage of Baserow in expense of performance". For disk, add your data, images and a few nights of backups to Ubuntu's 25 GB. On Space-Node, a Standard VPS-M has 4 vCores, 8 GB RAM and 80 GB NVMe; see the VPS plans for n8n and AI agents, and our guide on one VPS or several for when an app needs its own server.
Step 1: Prepare the Server
Log in with an SSH key and turn off password logins, as our SSH hardening guide shows. Ubuntu ships with ufw, and its unattended-upgrades is "installed by default" and installs security updates from the start. Debian may lack both; install them and switch the updates on:
sudo apt install ufw unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades
Then let SSH through the firewall before you turn it on:
sudo ufw allow 22/tcp
sudo ufw enable
Install Docker Engine and the Compose plugin with Docker's guide for Ubuntu or Debian. Then point two names at the server with A and AAAA records; this guide uses n8n.example.com and crm.example.com. Caddy needs those records and ports 80 and 443, "both required for the ACME HTTP challenge", before it can get certificates.
Step 2: Write the Compose File
Create the project folder and a .env file with a random value for every secret:
sudo mkdir -p /srv/automation/conf
cd /srv/automation
for v in POSTGRES_PASSWORD N8N_DB_PASSWORD BASEROW_DB_PASSWORD \
N8N_ENCRYPTION_KEY BASEROW_SECRET_KEY BASEROW_JWT_SIGNING_KEY; do
echo "$v=$(openssl rand -hex 32)"
done | sudo tee .env > /dev/null
sudo chmod 600 .env
Compose fills the ${...} values below from .env, and :? stops it when one is missing. Copy the file to your password manager now: n8n encrypts saved credentials with N8N_ENCRYPTION_KEY, and a restore without that key can't decrypt them, as our n8n backup guide shows. Then create /srv/automation/compose.yaml as root:
services:
postgres:
image: pgvector/pgvector:0.8.7-pg17
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h localhost -U postgres"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: n8nio/n8n:2.45.7
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${N8N_DB_PASSWORD:?}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY:?}
N8N_HOST: n8n.example.com
N8N_PROTOCOL: https
N8N_PROXY_HOPS: "1"
N8N_WEBHOOK_URL: https://n8n.example.com/
GENERIC_TIMEZONE: Europe/Amsterdam
volumes:
- n8n-data:/home/node/.n8n
baserow:
image: baserow/baserow:2.4.1
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
BASEROW_PUBLIC_URL: https://crm.example.com
DATABASE_HOST: postgres
DATABASE_NAME: baserow
DATABASE_USER: baserow
DATABASE_PASSWORD: ${BASEROW_DB_PASSWORD:?}
SECRET_KEY: ${BASEROW_SECRET_KEY:?}
BASEROW_JWT_SIGNING_KEY: ${BASEROW_JWT_SIGNING_KEY:?}
volumes:
- baserow-data:/baserow/data
caddy:
image: caddy:2.11.7
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- caddy_data:/data
- caddy_config:/config
volumes:
pgdata:
n8n-data:
baserow-data:
caddy_data:
caddy_config:
Put the routing in /srv/automation/conf/Caddyfile; Caddy's image docs say to mount the folder, not the file:
n8n.example.com {
reverse_proxy n8n:5678
}
crm.example.com {
reverse_proxy baserow:80
}
Why it looks like this:
- Only Caddy publishes ports. The others have no
ports:entry, so only containers on the project network reach them, each "discoverable by its service name". Docker's published ports skip UFW, as our Docker and UFW guide explains, but here that only concerns 80 and 443. Caddy "keeps all managed certificates renewed" and redirects HTTP to HTTPS. - One PostgreSQL, two databases. n8n supports PostgreSQL 16 to 18. Baserow supports 14 or newer and lists the pgvector extension among its dependencies; its own Compose file uses the
pgvector/pgvectorimage, which "adds pgvector to the Postgres image". On 17, mount the volume at/var/lib/postgresql/data: the image docs warn that a mount at/var/lib/postgresqlloses the data when the container is recreated. - Baserow on the shared database. The
DATABASE_variables switch off the PostgreSQL inside the Baserow image, which its docs call "highly recommended for any production deployments". Inside the container it keeps serving plain HTTP on port 80 for Caddy.BASEROW_PUBLIC_URLmust match the address in your browser. - n8n behind Caddy.
N8N_PROXY_HOPSsays one proxy sits in front,N8N_WEBHOOK_URLsets the public webhook address, and Caddy sends the threeX-Forwarded-headers n8n asks for.GENERIC_TIMEZONEsets the time zone for Schedule triggers.
Step 3: Create the Databases and Claim the Admin Accounts
Start PostgreSQL on its own, show the two database passwords and open a shell as the superuser:
cd /srv/automation
sudo docker compose up -d postgres
sudo grep DB_PASSWORD .env
sudo docker compose exec postgres psql -U postgres
Create a login and a database per app with those passwords, and enable pgvector in Baserow's database, which the pgvector README says to do "once in each database where you want to use it":
CREATE ROLE n8n WITH LOGIN PASSWORD 'paste N8N_DB_PASSWORD here';
CREATE DATABASE n8n OWNER n8n;
CREATE ROLE baserow WITH LOGIN PASSWORD 'paste BASEROW_DB_PASSWORD here';
CREATE DATABASE baserow OWNER baserow;
\c baserow
CREATE EXTENSION vector;
\q
Owning its database lets each app create tables, because PostgreSQL 15 and newer give a database's owner its public schema; the superuser creates the extension because many extensions require one. Start the rest and follow Baserow's first start until it logs Baserow is now available at, then press Ctrl+C:
sudo docker compose up -d
sudo docker compose logs -f baserow
Open both addresses straight away: n8n shows a signup screen for its owner account, and Baserow gives staff rights to the first user who signs up, so whoever comes first runs the instance. In Baserow's admin Settings, turn off Allow creating new accounts; people you invite to a workspace can still join. For n8n logins, two-factor authentication and webhook protection, follow our n8n security guide.
Step 4: Connect n8n and Baserow
Connect them over their public HTTPS addresses:
- n8n to Baserow: in Baserow, create a token under My Settings > Database tokens. In n8n, add a Baserow credential with that token and set Host to
https://crm.example.com. - Baserow to n8n: start an n8n workflow with a Webhook node, set HTTP Method to POST and Authentication to Header auth with a header name and a long random value, and publish it. In Baserow, open the ••• menu of the table, choose Webhooks, select POST and the event for created rows, paste the production URL and add the same header.
The public addresses avoid two guards against internal requests: Baserow refuses webhooks to private addresses unless you set BASEROW_WEBHOOKS_ALLOW_PRIVATE_ADDRESS, which its docs call "a security risk", and n8n's SSRF protection, once turned on, blocks private ranges too. A first workflow: Form Trigger, Baserow Create a row, then an email to the lead.
Step 5: Back Up Every Night
Save this as /usr/local/sbin/automation-backup, with the Push URL from Step 6:
#!/bin/bash
set -euo pipefail
cd /srv/automation
B=/var/backups/automation
mkdir -p "$B" && chmod 700 "$B"
docker compose exec -T postgres pg_dumpall -U postgres --globals-only > "$B/globals.sql"
for db in n8n baserow; do
docker compose exec -T postgres pg_dump -U postgres -Fc "$db" > "$B/$db.dump"
done
trap 'docker compose start n8n baserow' EXIT
docker compose stop n8n baserow
for v in n8n-data baserow-data; do
docker run --rm -v "automation_$v:/data:ro" -v "$B:/backup" ubuntu:24.04 \
tar czf "/backup/$v.tar.gz" -C /data .
done
trap - EXIT
docker compose start n8n baserow
tar czf "$B/project.tar.gz" -C /srv automation
curl -fsS -m 10 "https://status.example.net/api/push/YOUR_TOKEN?status=up&msg=OK" > /dev/null
pg_dump "makes consistent backups even if the database is being used concurrently", and pg_dumpall --globals-only adds the logins it leaves out. n8n and Baserow stop only while their volumes are archived, as Baserow's docs ask; Compose prefixes volume names with the folder name, hence automation_. The archives hold n8n's key, Baserow's uploads and .env, so the folder stays private. If a step fails, set -e stops the script before the ping and the monitor alerts you. Make it executable and schedule it for 03:30:
sudo chmod 700 /usr/local/sbin/automation-backup
echo '30 3 * * * root /usr/local/sbin/automation-backup' | sudo tee /etc/cron.d/automation-backup
Keep scheduled workflows out of that window, and copy /var/backups/automation off the server, for example with restic over SSH. Our Docker volumes and PostgreSQL guides cover the restores.
Step 6: Check It From Another Machine
A monitor on the same VPS goes down with it, so run Uptime Kuma on another machine with three monitors:
| Monitor | Type | Target |
|---|---|---|
| n8n | HTTP(s) | https://n8n.example.com/healthz/readiness |
| Baserow | HTTP(s) | https://crm.example.com/api/_health/ |
| Backup | Push | The Push URL in your script |
n8n's readiness endpoint returns 200 only "if the DB is connected and migrated", and Baserow's health path answers OK. Tick Certificate Expiry Notification on both to catch a failed renewal, and set the push monitor's heartbeat interval to 90000 seconds (25 hours), so a missed night alerts you. For failed workflows, add an error workflow from our n8n monitoring guide.
Who Maintains It
Stacks like this break months later, when nobody updated n8n, the disk filled or the only person with the passwords left. Name an owner for each job:
| How often | Job |
|---|---|
| Daily | Read the alerts from Uptime Kuma and the n8n error workflow |
| Weekly | Check free space with df -h / and that last night's files are in /var/backups/automation |
| Monthly | Read the release notes, raise the image tags (keep pg17), run sudo docker compose pull and sudo docker compose up -d; reboot if /var/run/reboot-required exists; test a restore |
| Quarterly | Remove accounts of people who left, in both apps |
| Yearly | Check which PostgreSQL versions n8n supports, and move off 17 with a dump and restore before it drops off that list |
n8n advises updating "at least once a month", and Baserow "will automatically upgrade itself on startup", so update after a good backup; docker compose up -d recreates only changed containers and keeps their volumes. PostgreSQL ships minor releases "at least once every three months" and supports 17 until 8 November 2029, but n8n supports only the two newest majors plus one older one, so its list moves up each year. A newer major can't open its data files, so just raising the tag fails with database files are incompatible with server.
Give the job to one person and a deputy who can open the same password manager, as our disaster recovery checklist suggests. Space-Node VPS plans are self-managed: you run all of this, and backups are not included in the plan price but can be arranged through a support ticket. Want help managing the server? Ask support about the scope before you order, and see our managed vs self-managed guide.
If Something Fails
- No certificate:
sudo docker compose logs caddyshows why. Check the A and AAAA records and that ports 80 and 443 reach the server. - n8n or Baserow can't log in to PostgreSQL: the role's password differs from
.env. In the psql shell from Step 3, runALTER ROLE n8n WITH PASSWORD '...';(orbaserow) with the value from.env, then restart that app, for examplesudo docker compose restart n8n.
FAQ
Do I need Redis?
No. n8n needs Redis only in queue mode, where workers pick up executions from a queue, and the Baserow image runs its own Redis inside the container unless you set REDIS_URL.
Why doesn't Baserow send emails?
Without EMAIL_SMTP and the other SMTP variables, Baserow "will not send emails and just log them to the Celery worker logs instead", so invitations and resets never arrive. Add your mail provider's SMTP settings to the baserow service.
Can my business use these tools for free? n8n's Sustainable Use License allows use "for your own internal business purposes". Baserow's Open Source Edition is MIT licensed; its premium and enterprise parts have their own licences. Twenty is mostly AGPLv3.
Should n8n keep SQLite instead? SQLite, n8n's default, works for one instance with a few users, as our SQLite or PostgreSQL guide explains. PostgreSQL runs here anyway for Baserow, so n8n on it costs little and gives both apps one backup routine.
Sources
All opened on 31 October 2026:
- n8n docs: Choose how to use n8n, Install using Docker Compose, Install with Docker, Choose n8n's database, Database variables, Deployment variables, Endpoint variables, Webhook URLs with a reverse proxy, Timezone variables, SSRF protection variables, User management, Fix memory issues, Enable queue mode, Monitor n8n, Update n8n, n8n Form Trigger, Webhook node, Baserow node, Baserow credentials
- n8n on GitHub and Docker Hub: Compose file at 2.45.7, release 2.45.7, licence, image tags
- Baserow: Install with Docker, Configuration, Supported dependencies, Webhooks, Form view, Admin settings, Community FAQ, image tags
- Baserow source: Compose file, user handler, instance settings model, health URLs, licence
- Twenty: Docker Compose docs, Compose file, licence
- PostgreSQL: pg_dump, pg_dumpall, CREATE ROLE, ALTER ROLE, CREATE DATABASE, CREATE EXTENSION, psql, Resource consumption, Release 15 notes, Versioning policy, postgres image docs, pgvector README
- Caddy: image docs, release 2.11.7, Automatic HTTPS, reverse_proxy
- Uptime Kuma: README, push API route, English UI strings
- Docker: Install on Ubuntu, Install on Debian, Packet filtering and firewalls, Compose networking, Compose interpolation, Compose project name, Compose volumes, Volumes, docker compose up, exec, logs, pull, stop, start, restart
- Ubuntu and Debian: Ubuntu Server system requirements, Ubuntu automatic updates, Debian UnattendedUpgrades
- Man pages: ufw(8), bash(1), tar(1), chmod(1), df(1), crontab(5), curl, openssl-rand