Analytics cookies only (Google Analytics, PostHog). No ad networks, no cross-site tracking. See our privacy policy.
Updated Oct 2026
verified on Ubuntu 26.04 · Oct 2026Self-host NocoBase with Docker Compose and PostgreSQL on a 2 GB VPS — a pinned image, generated APP_KEY, a loopback port behind Caddy HTTPS and backups.
NocoBase is a data-model-driven low-code platform: you
define collections, fields and relations first, then compose pages, blocks,
workflows and role-based permissions over them in the browser, with
everything beyond the kernel delivered as plugins. It is a TypeScript and
Node.js application, and the official Docker route runs one
nocobase/nocobase image beside PostgreSQL (MySQL and MariaDB variants are
documented too) with a ./storage directory for uploads and the database. It
is rated 2 / 5 to deploy.
Read the licence before you build on it. The Community Edition is published under a NocoBase License Agreement that incorporates Apache-2.0 and adds supplementary terms: keep the NocoBase branding in the interface, and do not offer a no-code or low-code SaaS built on it. Those terms are what separate it from the plain Apache-2.0 of Appsmith in the same category. If a spreadsheet-style interface over a database is all you need, see NocoDB or Baserow; if you are weighing it against the hosted product it most resembles, see Airtable alternatives.
Upstream's system-requirements page gives a single-node minimum of 1 CPU core and 2 GB of memory, which it says "only ensures that the system can run; it does not guarantee performance", and recommends 2 cores and at least 4 GB. It lists no disk figure.
In our install check (GCP e2-standard-2, Ubuntu 26.04, October 2026) the stack of NocoBase and PostgreSQL idled at about 592 MB of RAM and about 2.0 GB of Docker disk. A 2 GB plan leaves room for Caddy and a handful of users; move to the 4 GB recommendation when workflows and several concurrent editors arrive.
Install Docker and the Compose plugin by following Docker and Compose on Ubuntu, and confirm the versions:
docker --version
docker compose version
Open SSH plus the two ports Caddy will use. NocoBase itself gets no public port:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
Docker publishes container ports by editing the firewall directly, which
bypasses ufw rules. That is why the compose file below binds NocoBase's port
to 127.0.0.1. Upstream's own Docker page says much the same of its built-in-Nginx image:
it is "usually better not to expose 13000 directly to the public internet" and to let a
reverse proxy on the host handle the domain and HTTPS.
Paid link — we earn a commission if you shop through it.
Generate the two secrets first. APP_KEY is, in upstream's words, "the
application's secret key, used for generating user tokens, etc."; changing it
invalidates every existing token, and losing it does the same, so this file
is part of your backups. The block only writes .env if it does not exist,
so re-running it cannot rotate the keys under a live database:
mkdir -p ~/nocobase && cd ~/nocobase
[ -f .env ] || cat > .env <<EOF
APP_KEY=$(openssl rand -hex 32)
DB_PASSWORD=$(openssl rand -hex 16)
EOF
chmod 600 .env
Now the compose file. Apart from the port binding it is the file we verified
against a fresh server, which in turn is upstream's PostgreSQL example with
two deliberate changes: a pinned version instead of the rolling latest-full
tag (upstream: "For production environments, it is recommended to pin a
specific version number to avoid unintentional automatic upgrades"), and the
generated secrets from .env in place of the placeholders.
cd ~/nocobase
cat > docker-compose.yml <<'EOF'
services:
app:
image: nocobase/nocobase:2.2.27
restart: always
depends_on:
- postgres
environment:
APP_KEY: ${APP_KEY}
DB_DIALECT: postgres
DB_HOST: postgres
DB_PORT: 5432
DB_DATABASE: nocobase
DB_USER: nocobase
DB_PASSWORD: ${DB_PASSWORD}
TZ: Etc/UTC
volumes:
- ./storage:/app/nocobase/storage
ports:
- "127.0.0.1:13000:80"
postgres:
image: postgres:16
restart: always
command: postgres -c wal_level=logical
environment:
POSTGRES_USER: nocobase
POSTGRES_DB: nocobase
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- ./storage/db/postgres:/var/lib/postgresql/data
EOF
The plain 2.2.27 tag is the slim image. Upstream's -full tags add the
database client tools its backup and migration plugins need, plus LibreOffice
for PDF printing; switch to 2.2.27-full if you rely on those plugins.
wal_level=logical on the PostgreSQL side is carried over from upstream's
example unchanged.
Start the stack. Upstream says installation runs automatically on the first
start, and that a successful boot ends with a log line reading NocoBase server running at: http://localhost:13000/:
cd ~/nocobase
docker compose up -d
out=000
for i in $(seq 1 60); do
out=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:13000/api/__health_check)
[ "$out" = "200" ] && break
sleep 5
done
docker compose ps
echo "health check over loopback: $out"
[ "$out" = "200" ] || false
/api/__health_check is the endpoint NocoBase's own CLI polls when it waits
for the app to come up. In the server's gateway code it answers 200 with the
body ok only once the application's status is running; while the app is
still preparing or initialising, the gateway returns an error instead. That
makes it a stricter readiness signal than /, which would serve the
single-page app's HTML before the backend is ready.
Create an A record for your hostname (say nocobase.example.com)
pointing at the server. With no domain yet, an sslip.io name resolves to
your server's IP with no DNS setup, which is what this guide uses so every
command runs as written:
DOMAIN="nocobase.$(curl -4s https://ifconfig.me).sslip.io"
echo "$DOMAIN"
Keep this shell session open (or re-run the line) for the rest of the guide, and replace the value with your real hostname when you have one.
There is nothing to tell NocoBase about its address. Upstream's production
guidance is to serve pages and API from the same origin through a reverse
proxy that forwards api/ and files/ to NocoBase, and to leave
API_BASE_URL (default: empty) alone; cross-origin setups need
CORS_ORIGIN_WHITELIST and break file previews. It also asks for one
hostname per NocoBase instance rather than one hostname with several ports,
because browsers do not isolate cookies by port.
Run Caddy as a container on the host network. It obtains and renews a Let's Encrypt certificate on its own (see Automatic HTTPS with Caddy) and proxies to the loopback port:
mkdir -p ~/caddy
cat > ~/caddy/Caddyfile <<EOF
$DOMAIN {
reverse_proxy 127.0.0.1:13000
}
EOF
docker rm -f caddy >/dev/null 2>&1 || true
docker run -d --name caddy --restart unless-stopped --network host \
-v ~/caddy/Caddyfile:/etc/caddy/Caddyfile:ro -v caddy_data:/data caddy:2
Upstream's own Nginx configurations keep the app on 127.0.0.1:13000 and
route /api/, /ws and /files/ to NocoBase (its hand-written example
serves the front-end files itself). The Docker image serves pages and API
from one port, so a single reverse_proxy line does all of that in Caddy: it passes incoming headers
including Host through unchanged, sets X-Forwarded-For and
X-Forwarded-Proto, and supports WebSocket connections without extra
configuration, so the /ws channel works as is.
Check that HTTPS answers with a trusted certificate, retrying while the certificate is issued:
out=000
for i in $(seq 1 24); do
out=$(curl -s -o /dev/null -w '%{http_code} %{ssl_verify_result}' "https://$DOMAIN/api/__health_check") && [ "${out%% *}" = "200" ] && break
sleep 5
done
echo "health check over HTTPS and certificate check: $out"
[ "${out%% *}" = "200" ] || false
A second number of 0 means the certificate verified.
Open https://nocobase.example.com (your hostname) and sign in with the
default administrator upstream's Docker page documents:
admin@nocobase.com / admin123. Change that password before you do
anything else; the install is reachable from the internet now, and the
default is public knowledge.
If you would rather never have the default exist, upstream's environment
reference lists INIT_ROOT_EMAIL, INIT_ROOT_PASSWORD and
INIT_ROOT_NICKNAME for the root user created at installation. They are
read when the instance installs itself, so to use them you add them to the app service's
environment block before the first docker compose up -d; on an
installed instance, change the password in the UI instead.
Then open the plugin manager and enable what you need. The data-model work starts in Collections; build a collection, then a page with a table block over it, and set roles before you invite anyone.
Three things need protecting:
./storage/db/postgres, but a
pg_dump is the portable copy../storage otherwise, which holds uploads and plugin data..env, because APP_KEY signs every user token. Restore the database
without it and everyone is logged out.Dump the database with the stack running, then archive the rest. The tar
excludes storage/db because the dump already covers it:
cd ~/nocobase
mkdir -p ~/nocobase-backup
docker compose exec -T postgres pg_dump -U nocobase nocobase | gzip > ~/nocobase-backup/nocobase-$(date +%F).sql.gz
sudo tar czf ~/nocobase-backup/nocobase-files-$(date +%F).tgz --exclude=storage/db storage .env docker-compose.yml
ls -lh ~/nocobase-backup
sudo is there because the container writes ./storage as root. Copy the
archives off the server to object storage or another machine on a schedule.
To restore, bring up an empty stack with the same .env and compose file,
pipe the dump into psql in the postgres container, unpack storage, and
start app. An untested backup is a guess, so restore into a throwaway
server now and then.
Upstream also ships a backup plugin that works inside the UI; it needs the
-full image tag for its database client tools.
Upstream's Docker upgrade page says to back up your database first, then
change the app image to the target version, pull and recreate it. It notes
that only upgrades are supported, never downgrades, and that third-party
plugins are upgraded separately through the plugin manager.
cd ~/nocobase
sed -i 's|nocobase/nocobase:2.2.27|nocobase/nocobase:NEW_VERSION|' docker-compose.yml
docker compose pull app
docker compose up -d app
docker compose logs --tail 50 app
The PostgreSQL tag is a separate decision. A major PostgreSQL upgrade needs
a dump and restore, not just a new image; the postgres:16 line follows that
major only.
The health check keeps returning an error after up -d. The gateway code
answers /api/__health_check with 200 only when the application is
running; during the first-start installation it returns an error code such
as APP_INITIALIZING. Follow docker compose logs -f app until the
NocoBase server running at line, then try again. If the install itself
fails, the usual cause is the database: check docker compose logs postgres
and that DB_PASSWORD in .env is the value both services started with.
Everyone was logged out. APP_KEY changed. Upstream's reference says
old tokens become invalid when it does. Restore the original .env from your
backup and recreate app.
Uploaded files 403 or fail to preview. Upstream's docs say legacy
/storage/uploads/ files require a login check by default, and that
/files/ must reach NocoBase through the same origin as the pages. Confirm
you are browsing the Caddy hostname, not a second one, and that nothing in
front rewrites /files/. LEGACY_LOCAL_STORAGE_PUBLIC_ACCESS=true makes
those legacy files anonymous again if that is what you want.
Two NocoBase instances on one hostname log each other out. Upstream's production page: browsers do not isolate cookies by port, so give each instance its own hostname.
Port 13000 is reachable from the internet. The port line lost its
127.0.0.1: prefix. Fix the compose file, docker compose up -d, then check
that ss -tln | grep 13000 shows 127.0.0.1.
Certificate never issues. That is a Caddy and DNS problem rather than a
NocoBase one: confirm the A record resolves (dig +short nocobase.example.com) and that ports 80 and 443 are reachable. Look at
docker logs caddy.
You are done when https://nocobase.example.com loads with a valid
certificate, the default administrator password has been changed, port 13000
is bound only to 127.0.0.1, and a database dump plus the storage archive
and .env are landing off the server.
From here, model your first collection, build a page over it, and set roles before inviting the team. If the apps you need are screens over data that already lives in other systems rather than a new data model, look at Appsmith and its deploy guide.