VPS Tutorials
Self-Host NocoDB No-Code Database on a VPS (2026): Complete Guide to Docker Compose, PostgreSQL, Redis, and Caddy HTTPS
This tutorial explains clearly how to self-host NocoDB on a VPS and turn your database into an Airtable-like spreadsheet interface. It covers choosing VPS specs, a four-container Docker Compose deployment, PostgreSQL and Redis, automatic HTTPS with Caddy, environment variables, backups, upgrades, security hardening, and troubleshooting common errors.

Self-Host NocoDB No-Code Database on a VPS (2026): Complete Guide to Docker Compose, PostgreSQL, Redis, and Caddy HTTPS
The first time I looked closely at NocoDB was because a friend had stuffed his company's customer list, project progress, and purchasing records all into Excel. The filename went from 客户表_final.xlsx to 客户表_final_真的最终版.xlsx, and in the end nobody knew which one was the latest.
Airtable can solve this problem, but once the team grows, the price isn't cheap. What's more troublesome is that a lot of small teams already keep their data in MySQL or PostgreSQL; they just don't have a spreadsheet interface that operations, sales, and support staff can use directly.
NocoDB's positioning is straightforward: turn a database into an Airtable-like visual spreadsheet. You can filter, sort, build views, import CSV, and generate APIs in the browser, and colleagues who can't write SQL can maintain data directly.
This article is about how to run NocoDB on your own VPS. We won't use a black-box install script; instead we'll hand-write a Docker Compose configuration that you can understand, back up, and migrate.
It suits these scenarios:
- A small team wants an open-source Airtable alternative.
- You have a pile of Excel / CSV files and want to move them into a database for unified management.
- Operations, support, and sales need to edit data but shouldn't go directly into the database.
- You want to put a low-code management interface on top of PostgreSQL / MySQL.
- You already have a VPS and don't want to pay per seat for another spreadsheet collaboration tool.
It's less suited to these scenarios:
- You're just one person jotting down a few to-dos; Notion or Feishu Sheets is easier.
- You don't want to maintain a server, database, and backups at all.
- You need extremely complex enterprise permissions, auditing, and approval flows.
- You plan to have hundreds of people editing online at the same time; a single small VPS may not handle it.
My advice: personal projects, small teams, internal tools, lightweight CRM, inventory sheets, and content scheduling are all good fits. If you really want a large business backend, NocoDB can serve as an auxiliary interface, but don't treat it as an all-purpose ERP.
NocoDB's official quickstart runs four containers:
| Container | Purpose |
|---|---|
nocodb | Web interface and API |
worker | Background tasks such as import, export, and automation |
db | PostgreSQL, storing metadata and business data |
redis | Cache and task queue |
You can start from as little as 1 core and 1GB, but I don't recommend being that stingy in production.
| Usage | Recommended specs |
|---|---|
| Personal testing, a few tables | 1 vCPU / 1GB RAM / 20GB SSD |
| Small team daily use | 2 vCPU / 2GB RAM / 40GB SSD |
| Many file attachments, multi-person collaboration | 2-4 vCPU / 4GB RAM / 80GB+ SSD |
What really matters is disk and backups. NocoDB itself isn't heavy; the trouble is that once you put customer data, orders, and attachments into it, it becomes your business data center. A cheap VPS is fine, but you can't skip backups.
This article assumes:
- Ubuntu 22.04 / 24.04 or Debian 12.
- Docker and Docker Compose V2 are installed.
- You have a domain, for example
nocodb.example.com. - The domain's A record points to the VPS public IP.
- Ports 80 and 443 can be opened for Caddy to obtain HTTPS automatically.
Verify Docker:
docker version
docker compose version
If Docker isn't installed yet, you can first read the article on this site about Docker Compose in production. I won't repeat the basic installation here.
NocoDB officially provides quickstart and single-server install scripts, which are indeed convenient:
curl -fsSL https://install.nocodb.com/noco.sh | bash
It will generate a Compose stack for you, and single-server mode can even handle Traefik and Let's Encrypt.
But I don't really recommend that beginners run the script directly the first time. The reason is simple: when something goes wrong, you don't know what it generated, where the data is, how to back it up, or how to migrate.
This article takes the hand-written Compose approach. The configuration is a bit longer, but you can see every container, every volume, and every password. Maintenance later is also more reliable.
Create the deployment directory:
sudo mkdir -p /opt/nocodb
sudo chown -R "$USER":"$USER" /opt/nocodb
cd /opt/nocodb
mkdir -p postgres-data redis-data nocodb-data backups caddy-data caddy-config
Generate a few random keys:
openssl rand -base64 32
openssl rand -base64 32
openssl rand -hex 32
They're used respectively for:
- PostgreSQL password
- NocoDB JWT Secret
- Database connection encryption key
Don't use password123, and don't copy the passwords from the tutorial. NocoDB may later store external database connection information, so a weak password is digging a hole for yourself.
Create .env:
nano .env
Write the following content, replacing the domain and passwords with your own:
NOCODB_DOMAIN=nocodb.example.com
POSTGRES_PASSWORD=replace_with_a_long_postgres_password
NC_AUTH_JWT_SECRET=replace_with_a_long_random_secret
NC_CONNECTION_ENCRYPT_KEY=replace_with_64_hex_chars
A few variables explained:
POSTGRES_PASSWORD: PostgreSQL database password.NC_AUTH_JWT_SECRET: key related to login sessions; must be fixed in production.NC_CONNECTION_ENCRYPT_KEY: used to encrypt external database connection information; it's recommended to set it from the start.NOCODB_DOMAIN: the domain used by the Caddy reverse proxy.
Don't upload the .env file to a Git repository. It is your production secret.
Create docker-compose.yml:
nano docker-compose.yml
Write:
services:
nocodb:
image: nocodb/nocodb:latest
container_name: nocodb
restart: unless-stopped
env_file:
- .env
environment:
NC_DB: "pg://db:5432?u=nocodb&p=${POSTGRES_PASSWORD}&d=nocodb"
NC_REDIS_URL: "redis://redis:6379"
NC_SITE_URL: "https://${NOCODB_DOMAIN}"
NC_AUTH_JWT_SECRET: "${NC_AUTH_JWT_SECRET}"
NC_CONNECTION_ENCRYPT_KEY: "${NC_CONNECTION_ENCRYPT_KEY}"
NC_DISABLE_MUX: "true"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./nocodb-data:/usr/app/data
ports:
- "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
container_name: nocodb-worker
restart: unless-stopped
env_file:
- .env
environment:
NC_DB: "pg://db:5432?u=nocodb&p=${POSTGRES_PASSWORD}&d=nocodb"
NC_REDIS_URL: "redis://redis:6379"
NC_SITE_URL: "https://${NOCODB_DOMAIN}"
NC_AUTH_JWT_SECRET: "${NC_AUTH_JWT_SECRET}"
NC_CONNECTION_ENCRYPT_KEY: "${NC_CONNECTION_ENCRYPT_KEY}"
NC_WORKER_CONTAINER: "true"
depends_on:
nocodb:
condition: service_healthy
volumes:
- ./nocodb-data:/usr/app/data
db:
image: postgres:17-alpine
container_name: nocodb-db
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-alpine
container_name: nocodb-redis
restart: unless-stopped
volumes:
- ./redis-data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
caddy:
image: caddy:2-alpine
container_name: nocodb-caddy
restart: unless-stopped
env_file:
- .env
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
depends_on:
- nocodb
There are a few things deliberately written this way:
nocodbonly binds to127.0.0.1:8080, so port 8080 can't be accessed directly from the internet.- What's actually exposed externally is Caddy's 80/443.
- PostgreSQL doesn't map 5432, so the database can't be found by internet scans.
- Redis doesn't map 6379 either.
nocodb-data,postgres-data, andredis-dataare all host directories, so you can find them at a glance when backing up.
A more stable production approach is to pin nocodb/nocodb:latest to the current stable version tag. For the first deployment you can use latest to get it running; before going live, check the current version on NocoDB GitHub Releases or Docker Hub and then pin it manually.
Create Caddyfile:
nano Caddyfile
Write:
{$NOCODB_DOMAIN} {
encode zstd gzip
reverse_proxy 127.0.0.1:8080
}
Make sure the domain already resolves to the VPS, then open the firewall:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload
If your cloud provider also has security groups, allow 80 and 443 there too. Many people open them in the system but not in the cloud console, and in the end Caddy fails to obtain a certificate.
Run:
cd /opt/nocodb
docker compose up -d
Check container status:
docker compose ps
Check logs:
docker compose logs -f --tail=100 nocodb
The first startup will pull images and initialize the database, so waiting a few minutes is normal. Once nocodb becomes healthy, open in your browser:
https://nocodb.example.com
The first registered user becomes the super admin. Right after registering, go to settings and check the email, site URL, language, and permissions; don't just throw the link to the whole company.
NocoDB has several ways to use it:
- Create an empty project, then build tables in the interface.
- Import CSV / Excel.
- Connect to an existing PostgreSQL / MySQL database.
- Generate a REST API for a table.
If you're migrating from Excel, I recommend importing a copy first; don't connect production data right away.
For example, a customer table can first be organized as:
| Field | Example |
|---|---|
| Customer name | Acme Inc |
| Contact | Zhang San |
| [email protected] | |
| Status | Following up / Closed / Paused |
| Owner | Alice |
| Next follow-up | 2026-07-15 |
Let the team try it for 1-2 weeks before deciding whether to connect the real business database. The biggest pitfall of low-code tools isn't deployment; it's that everyone designs the fields in a mess from the start.
NocoDB can connect to external databases, which is also its most valuable feature. But be cautious here.
Recommendations:
- First create a read-only account for NocoDB.
- Don't use the database root user directly.
- Don't expose the production database's 5432 / 3306 to the internet.
- Ideally, let NocoDB and the database communicate within the same intranet or Docker network.
- First verify fields, relationships, and permissions in a test database.
For example, a PostgreSQL read-only user looks roughly like this:
CREATE USER nocodb_readonly WITH PASSWORD 'change_this_password';
GRANT CONNECT ON DATABASE appdb TO nocodb_readonly;
GRANT USAGE ON SCHEMA public TO nocodb_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO nocodb_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO nocodb_readonly;
If you really want operations staff to edit data in NocoDB, create a separate account with controlled permissions and only open the tables that need editing. Don't hand over the entire database's permissions just to save effort.
NocoDB requires backing up two kinds of things:
- PostgreSQL database: projects, table structures, NocoDB metadata, business data.
/usr/app/data: attachments, local files, and other data.
First create a backup script:
nano backup-nocodb.sh
Write:
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/opt/nocodb"
BACKUP_DIR="$APP_DIR/backups"
DATE="$(date +%F-%H%M)"
mkdir -p "$BACKUP_DIR/$DATE"
cd "$APP_DIR"
docker exec nocodb-db pg_dump -U nocodb nocodb | gzip > "$BACKUP_DIR/$DATE/nocodb.sql.gz"
tar czf "$BACKUP_DIR/$DATE/nocodb-data.tar.gz" nocodb-data Caddyfile docker-compose.yml .env
find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d -mtime +14 -exec rm -rf {} \;
echo "Backup saved to $BACKUP_DIR/$DATE"
Add execute permission and test:
chmod +x backup-nocodb.sh
./backup-nocodb.sh
ls -lh backups
After confirming the backup file can be generated, add a scheduled task:
crontab -e
Back up every day at 3 AM:
0 3 * * * /opt/nocodb/backup-nocodb.sh >> /opt/nocodb/backups/backup.log 2>&1
This is only a local backup. A more reliable approach is to also use rclone to sync to object storage, another VPS, or a NAS. If you only back up on the same machine, a disk failure takes everything with it.
If a backup has never been restored, it can't really be considered usable.
Find a test VPS and copy the backup files over:
mkdir -p /opt/nocodb-restore
cd /opt/nocodb-restore
Extract the configuration and attachments:
tar xzf nocodb-data.tar.gz
Start the database and then restore the SQL:
docker compose up -d db redis
sleep 20
gunzip -c nocodb.sql.gz | docker exec -i nocodb-db psql -U nocodb nocodb
docker compose up -d
During an actual restore, the domain, certificate, and the domain in .env may need to be changed. Don't wait until the day of a production incident to practice this process for the first time.
Back up before upgrading:
cd /opt/nocodb
./backup-nocodb.sh
If you're still using latest:
docker compose pull
docker compose up -d
If you've already pinned a version tag, first change the image version in docker-compose.yml, then run the same command.
After upgrading, check:
docker compose ps
docker compose logs --tail=100 nocodb
Then open the web page and check at least three things:
- Whether you can log in.
- Whether tables can be opened.
- Whether import, export, and attachments work normally.
Once a tool like NocoDB is connected to the team workflow, don't upgrade too casually. It's recommended to do it during off-peak hours, notify the team before upgrading, and keep a rollback backup.
First check the containers:
docker compose ps
Then check the Caddy logs:
docker compose logs --tail=100 caddy
If Caddy fails to obtain a certificate, focus on checking:
- Whether the domain's A record points to the current VPS.
- Whether 80/443 are blocked by UFW or a security group.
- Whether Cloudflare proxy mode interferes with verification.
At the beginning, it's recommended to set the Cloudflare DNS record to "DNS only" first, and enable the proxy as needed after it works.
Check the application logs:
docker compose logs -f nocodb
Common causes:
- PostgreSQL password mismatch.
.envwas changed but the container wasn't restarted.- The database isn't ready yet, and the application failed to start early.
- The
NC_DBconnection string is written incorrectly.
You can check the database separately:
docker compose logs --tail=100 db
docker exec -it nocodb-db pg_isready -U nocodb -d nocodb
If you only forgot the password, use NocoDB's recovery feature first. If email isn't configured well, it will be much more troublesome.
So right when going live, it's recommended to:
- Use a long-term available email for the admin account.
- Configure SMTP.
- Keep at least two admin accounts.
- Write the initial deployment information into internal documentation.
Don't have only one admin for the whole company, registered with a personal email.
The most common problem with Chinese CSV files is encoding. Files exported from Excel are often not UTF-8.
Solutions:
- Try to export as UTF-8 CSV.
- If it's GBK, you can convert it first:
iconv -f gbk -t utf-8 old.csv > new.csv
Also, data like date fields, phone numbers, and order numbers is best imported as text first. Many spreadsheet tools try to be clever and eat leading zeros.
Focus on checking:
- Whether the database listen address is bound only to
127.0.0.1. - Whether the firewall allows it.
- Whether user permissions allow remote connections.
- Whether the NocoDB container can resolve the database hostname.
- Whether the database TLS requirements match.
But I'll say it again: use the intranet if you can; don't expose the production database directly to the internet.
NocoDB is an internal tool, but once it's exposed to the internet, treat it like a formal system.
At minimum, do the following:
- Only open 80/443, not 8080, 5432, or 6379.
- Use strong passwords for admins.
- Disable public registration you don't need.
- Give different teams the least privilege.
- Use low-privilege accounts for external database connections.
- Don't put
.envor backup files in web-accessible directories. - Regularly update images and system security patches.
- Sync backups off-site.
- Enable change history for important tables or do separate auditing.
If you're going to let customers or external collaborators use it, it's best to add a layer of Cloudflare Access, Tailscale, VPN, or at least HTTP Basic Auth. Don't expose an internal data management interface to the whole internet.
Check before formal use:
- The domain already resolves to the VPS.
- 80/443 are allowed in the system firewall and cloud security group.
- 8080 only binds to
127.0.0.1. - PostgreSQL and Redis don't expose public ports.
- The passwords and keys in
.envhave all been replaced. -
NC_SITE_URLis a real HTTPS domain. - The first admin account has been created.
- The local backup script has run successfully.
- At least one test restore has been done.
- Team permissions and table fields have been organized.
If you're just one person keeping accounts and writing to-dos, NocoDB may be a bit heavy. Notion, Feishu Sheets, and Google Sheets are all easier.
But if you already have a VPS and want to turn Excel, CSV, and databases into a collaborative, permission-controlled internal tool that can generate APIs, NocoDB is well worth trying. It's not meant to replace all backend systems, but to fill the gap between "development is too slow, Excel is too messy, and Airtable is too expensive."
I recommend first trying it with a piece of non-core data, such as content scheduling, customer leads, or inventory records. Once the team is comfortable, gradually connect more important data. The biggest fear with low-code tools is carrying core business right away; validate on a small scale first, then expand, so problems are easier to contain.