How to Deploy NocoDB on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-host NocoDB, the spreadsheet-style Airtable alternative, on your own VPS with Docker Compose, PostgreSQL, Redis and HTTPS, plus the backups and upgrades that keep it safe.
- A VPS with at least 2 GB RAM (NocoDB's own container idles around 1.5 GB in our test)
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain you can point at the server
- Docker Engine + Compose installed (see the base guide below)
What NocoDB is
NocoDB puts a spreadsheet-style interface on top of a relational database. You get grid, kanban, calendar, gallery and form views, a REST API for every table, and sharing and permissions. You get all of it without writing SQL. It can keep its tables in its own PostgreSQL database, or you can connect an existing PostgreSQL or MySQL database and work on those tables in place.
It is the usual self-hosted answer to Airtable. The other one is Baserow, and NocoDB vs Baserow covers the differences. The short version: NocoDB is the better fit when you want a UI over a database you already run, and Baserow is the more self-contained product. Check the licence before you commit. The catalog lists NocoDB under the Sustainable Use License, not an OSI licence, so read it if you plan to resell or embed NocoDB.
Server sizing
On our test box (GCP e2-standard-2, Ubuntu 26.04, Docker), a NocoDB stack idled at about 1,557 MB of RAM and used about 2.2 GB of disk for images and data. That was the stack with NocoDB, a worker, PostgreSQL and Redis. The catalog's minimum is 2 GB RAM. Treat that as the floor:
- 2 GB RAM / 1–2 vCPU: fine for one person or a small team with modest tables.
- 4 GB RAM / 2 vCPU: the comfortable choice. You get headroom for imports, the worker's background jobs, and the reverse proxy.
- Disk: start with 20–40 GB. Attachments are stored in the NocoDB data volume unless you configure S3-compatible storage, so size the disk for your uploads.
Prepare the server
This guide assumes Docker Engine and the Compose plugin are installed, along with a firewall. If not, work through Docker & Compose on Ubuntu first.
Only SSH and the web ports need to be open. NocoDB itself will listen on loopback only:
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose
Paid link — we earn a commission if you shop through it.
Install NocoDB (Docker Compose)
NocoDB publishes a plain Compose stack (the quickstart-demo example in its
repository). It runs the NocoDB app, a background worker for imports,
exports and automations, PostgreSQL and Redis. The version below
follows that file, with three changes for a server:
- It uses generated secrets instead of the demo password.
- It binds the app to
127.0.0.1so that only the reverse proxy can reach it. - It sets
NC_SITE_URLto your public URL.
Create the project directory and an .env file with generated values:
mkdir -p ~/nocodb && cd ~/nocodb
if [ ! -f .env ]; then
cat > .env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 24)
NC_AUTH_JWT_SECRET=$(openssl rand -hex 32)
NC_SITE_URL=https://nocodb.example.com
EOF
fi
chmod 600 .env
The if guard stops you overwriting the database password on a second run.
Change the password after PostgreSQL has initialised and NocoDB can no longer
log in. Replace nocodb.example.com with your real hostname. NocoDB builds
invitation and password-reset links from NC_SITE_URL, so a wrong value
produces broken links.
Now the Compose file:
cd ~/nocodb
cat > docker-compose.yml <<'YAML'
services:
nocodb:
image: nocodb/nocodb:latest
restart: unless-stopped
environment:
NC_DB: "pg://db:5432?u=nocodb&p=${POSTGRES_PASSWORD}&d=nocodb"
NC_REDIS_URL: "redis://redis:6379"
NC_SITE_URL: ${NC_SITE_URL}
NC_AUTH_JWT_SECRET: ${NC_AUTH_JWT_SECRET}
NC_DISABLE_MUX: "true"
depends_on:
db: { condition: service_healthy }
redis: { condition: service_healthy }
volumes:
- nocodb_data:/usr/app/data
ports:
# Loopback only: Caddy is the public entry point.
- "127.0.0.1:8080:8080"
healthcheck:
test: ["CMD-SHELL", "wget -q --tries=1 --spider http://localhost:8080/api/v1/health || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
worker:
image: nocodb/nocodb:latest
restart: unless-stopped
environment:
NC_DB: "pg://db:5432?u=nocodb&p=${POSTGRES_PASSWORD}&d=nocodb"
NC_REDIS_URL: "redis://redis:6379"
NC_SITE_URL: ${NC_SITE_URL}
NC_AUTH_JWT_SECRET: ${NC_AUTH_JWT_SECRET}
NC_WORKER_CONTAINER: "true"
depends_on:
nocodb: { condition: service_healthy }
volumes:
- nocodb_data:/usr/app/data
db:
image: postgres:17.10
restart: unless-stopped
environment:
POSTGRES_USER: nocodb
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: nocodb
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nocodb -d nocodb"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7
restart: unless-stopped
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
nocodb_data:
postgres_data:
redis_data:
YAML
Bring it up. --wait blocks until the health checks pass, which takes a minute
or two on the first run while the images download:
cd ~/nocodb
docker compose up -d --wait
docker compose ps
Check that the app answers on loopback. Expect a 200:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/api/v1/health
If you'd rather not maintain a Compose file, NocoDB also ships a single-server
installer, noco.sh, that generates this stack plus Traefik and Let's Encrypt
for you. It is the method shown on the NocoDB page. The
hand-written file above is easier to read, back up and diff.
HTTPS + domain
Point an A record for nocodb.example.com at the server's public IP. Then
put Caddy in front of 127.0.0.1:8080. The full setup is in
Automatic HTTPS with Caddy. The site
block is:
nocodb.example.com {
reverse_proxy 127.0.0.1:8080
}
Caddy passes WebSocket upgrades through without extra configuration. Reload
Caddy, then open https://nocodb.example.com.
First login and securing it
The first account to sign up becomes the super admin. Do it straight after the site goes live, before anyone else finds the URL. Then:
- Close public signup. In the super-admin settings, switch signups to
invite-only. The environment-variable form is
NC_INVITE_ONLY_SIGNUP. After that, new users only get in through an invitation. - Keep the app port private. The Compose file binds
8080to loopback. Do not change it to8080:8080. Docker-published ports bypassufw, so that change would expose NocoDB over plain HTTP. - Protect
.env. It holds the database password and the JWT secret. It ischmod 600and should never go into git. - Set up email if you want invitations and password resets to work. The SMTP variables are listed in NocoDB's environment-variable reference.
- Telemetry: NocoDB sends anonymous usage data by default. Add
NC_DISABLE_TELE: "true"to both app services to turn it off.
Backups
Your tables and NocoDB's metadata live in PostgreSQL. Uploaded attachments
live in the nocodb_data volume. Back up both.
A logical dump of the database is safe to take while the stack is running:
cd ~/nocodb
mkdir -p backups
docker compose exec -T db pg_dump -U nocodb -d nocodb | gzip > backups/nocodb-db-$(date +%F).sql.gz
ls -lh backups/
Attachments come from the data volume. Compose prefixes the volume name with
the project directory, so check the real name with docker volume ls:
cd ~/nocodb
docker run --rm -v nocodb_nocodb_data:/data:ro -v "$PWD/backups":/backup alpine \
tar czf /backup/nocodb-data-$(date +%F).tar.gz -C /data .
Copy backups/ and your .env off the server. Without the .env file
you have the data but not the secrets that match it. To restore, start a fresh
stack with the same .env. Load the dump with
gunzip -c … | docker compose exec -T db psql -U nocodb -d nocodb, then
unpack the attachments archive into the new volume.
Upgrades
cd ~/nocodb
docker compose pull
docker compose up -d --wait
Take the pg_dump backup above first, every time, because a new version can
change the database schema when it starts. latest follows current releases.
If you'd rather choose when to upgrade, pin nocodb/nocodb to a version tag
from its releases page and change the tag deliberately. Read the release notes
before a big version jump.
Troubleshooting
docker compose up --wait times out on nocodb. Read the logs with
docker compose logs nocodb --tail 100. The usual cause is a database
connection error, because the password in NC_DB no longer matches the one
PostgreSQL was first initialised with. PostgreSQL only reads
POSTGRES_PASSWORD when its volume is empty, so changing .env later does not
change the password.
Invitation or reset links point to localhost. NC_SITE_URL is unset or
wrong. Fix it in .env and run docker compose up -d.
Imports hang. Imports and exports run on the worker container. Check that
it is running (docker compose ps) and read docker compose logs worker.
The box runs out of memory. The stack idles around 1.5 GB. Large CSV imports push it higher. Move to 4 GB, or add swap as a stopgap.
Verification + next steps
You're done when:
https://nocodb.example.comloads with a valid certificate;- you have signed in as the super admin and closed public signup;
- a test base survives
docker compose restart; - a dated
pg_dumpsits somewhere other than this server.
From there, connect an existing database as a data source, or try the REST API with a scoped API token. If you're still choosing, compare it with Baserow on a VPS or look at Directus for a headless data platform. For ranked hosts, see Best VPS for databases.