How to Run NocoDB in a Sandbox With a Private HTTPS URL
Updated October 27, 2026 .md
Short answer: Run NocoDB and Postgres with Docker Compose in a small Sandboxer sandbox, create the admin from environment variables and host port 8080 with sbx host --localhost. Scripts send the __sbx_auth cookie plus an xc-token API token. Turn on invite-only signup, because anyone with the private link can otherwise register.
NocoDB puts a spreadsheet-style UI and a REST API on top of a SQL database. With Postgres as its database, every NocoDB table is a real Postgres table, so the same rows are visible to the grid, the API and psql.
What we ran#
Everything below ran on production on October 27, 2026, in one small sandbox (2 vCPU, 4 GB, region eu-central) with NocoDB 2026.10.0 from nocodb/nocodb:latest and postgres:17. The browser checks used headless Chromium (Playwright) on our test machine. Tokens and passwords are replaced with <token> and <password>. Single runs unless the table says otherwise.
| Step | Result |
|---|---|
sbx new --type small |
3.8 s to ready |
docker compose up -d with both pulls, until /api/v1/health answered |
40 s; NocoDB itself was up 13 s after its container started |
| Memory after start | NocoDB 799 MB, Postgres 59 MB; 2.6 GB of 3.9 GB still available |
| List 3 records through the hosted URL, from our test machine | median 0.21 s over 20 calls (0.20 to 0.22 s) |
Same call inside the sandbox on localhost:8080 |
median 25 ms over 20 calls |
sudo systemctl restart docker until healthy |
13.4 s, browser session and API token still valid |
sbx stop --wait, then sbx resume |
2.9 s and 2.1 s, memoryRestored: true, browser still signed in |
Resume with --type default (boots from disk) |
4.0 s, NocoDB healthy 13 s after boot, data and API token intact |
small is enough. NocoDB's image is 906 MB, and the Node process uses about 800 MB of RAM.
1. The Compose file#
NocoDB's README runs the image with NC_DB pointing at Postgres and a fixed NC_AUTH_JWT_SECRET. This file does the same with Postgres in the stack, and uses the environment variables that matter in a sandbox:
# noco/compose.yaml
services:
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: noco
POSTGRES_PASSWORD: ${PG_PASSWORD}
POSTGRES_DB: noco
volumes:
- pg:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "noco", "-d", "noco"]
interval: 3s
retries: 30
nocodb:
image: nocodb/nocodb:latest
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "127.0.0.1:8080:8080"
environment:
NC_DB: pg://db:5432?u=noco&p=${PG_PASSWORD}&d=noco
NC_AUTH_JWT_SECRET: ${JWT_SECRET}
NC_ADMIN_EMAIL: admin@example.com
NC_ADMIN_PASSWORD: ${ADMIN_PASSWORD}
NC_SITE_URL: ${SITE_URL}
NC_DISABLE_TELE: "true"
volumes:
- data:/usr/app/data
volumes:
pg:
data:Why these settings:
NC_ADMIN_EMAILandNC_ADMIN_PASSWORDcreate the super admin at startup. Without them, the first person to open the UI creates the admin account.NC_SITE_URLis the address NocoDB uses for links in emails, such as invites and password resets. The UI itself worked from the hosted URL without extra settings, because it calls the API on the origin it was loaded from.127.0.0.1:8080:8080. The sandbox has a public IPv6 address, and Docker can bypass ufw rules. Publishing on loopback keeps NocoDB reachable only through the hosted URL.restart: unless-stoppedbrings both containers back when Docker or the VM restarts.
2. Get the URL first, then start#
The URL goes into NC_SITE_URL, so host the port before you start. A private URL can exist before anything listens on the port:
sbx new --name nocodb --type small --no-env
id=sb_ig50v88ri0 # the id that sbx new printed; its subdomain was maple-alert-cinder
sbx host $id 8080 --localhost --title NocoDB{"ok":true,"type":"port.hosted","sandboxId":"sb_ig50v88ri0","port":8080,"url":"https://maple-alert-cinder-8080.box.sandboxer.com?_token=<token>","access":"private","isProtected":true,"localhost":true}Copy the folder in, write random secrets to .env without printing them, and start the stack. The pulls take longer than sbx exec's 30-second default, so raise the timeout:
sbx scp -r ./noco $id:/home/user/
sbx exec $id --timeout 600 "cd noco && umask 077 && {
echo PG_PASSWORD=\$(openssl rand -hex 16)
echo JWT_SECRET=\$(openssl rand -hex 32)
echo ADMIN_PASSWORD=\$(openssl rand -hex 12)
echo SITE_URL=https://maple-alert-cinder-8080.box.sandboxer.com
} > .env && docker compose up -d --quiet-pull \
&& until curl -fs localhost:8080/api/v1/health >/dev/null; do sleep 0.5; done \
&& curl -s localhost:8080/api/v1/health && docker compose ps" Container noco-db-1 Healthy
Container noco-nocodb-1 Started
{"message":"OK","timestamp":1793101540180,"uptime":13.188656253}
SERVICE IMAGE STATUS PORTS
db postgres:17 Up 16 seconds (healthy) 5432/tcp
nocodb nocodb/nocodb:latest Up 13 seconds 127.0.0.1:8080->8080/tcpRead the admin password when you need it with sbx exec $id "grep ADMIN_PASSWORD noco/.env".
3. Open the UI through the private URL#
| Request | Result |
|---|---|
/ or /api/v1/health, no token |
401, the Sandboxer "Access token required" page |
/?_token=<token> |
302 to /, sets the __sbx_auth cookie |
/ with the cookie |
200, the NocoDB sign-in page |
In a browser, open the full ?_token= link once, then sign in as admin@example.com. We did this in headless Chromium: the sign-in worked, the workspace loaded, and a deep link such as /w9sd0xte/p00ntx9hkxipp65/m1ajgz0zz889w09/…/contacts-contacts opened the grid directly on a fresh load. NocoDB keeps its own session in httpOnly cookies (nc_token, refresh_token) next to the proxy's __sbx_auth, and they don't interfere.
4. Close signup before you share the link#
With NC_ADMIN_EMAIL set, NocoDB still allows public signup. Anyone who has your private link can register. We tried it through the hosted URL, with the __sbx_auth cookie but no NocoDB login:
curl -s -b jar -H 'Content-Type: application/json' \
-d '{"email":"stranger@example.com","password":"<password>"}' $U/api/v1/auth/user/signup{"token":"<token>"}The new account appeared in the user list with the org-level-viewer role. Turn on invite-only signup as the super admin. NocoDB's docs put the switch in the super admin settings; we used the API, which needs a signed-in session token (xc-auth), because NocoDB refuses API tokens for app settings (ERR_API_TOKEN_NOT_ALLOWED):
curl -s -b jar -H "xc-auth: $JWT" -H 'Content-Type: application/json' \
-d '{"invite_only_signup":true}' $U/api/v1/app-settings{"msg":"The app settings have been saved"}The same signup request then returned 400: {"msg":"Not allowed to signup, contact super admin."}. NocoDB also documents an NC_INVITE_ONLY_SIGNUP environment variable for this; we used the setting instead. Check the user list for accounts created before you locked it.
5. Call the API from your laptop#
There are two gates. The proxy wants __sbx_auth (its value is the URL's token). NocoDB wants an xc-token API token. The proxy passes xc-token through untouched.
Sign in once to create a long-lived API token. $U is the base URL, $T the URL token, and login.json holds the admin email and password:
U=https://maple-alert-cinder-8080.box.sandboxer.com
curl -s -o /dev/null -c jar "$U/?_token=$T" # trade the URL token for the cookie once
JWT=$(curl -s -b jar -H 'Content-Type: application/json' -d @login.json \
$U/api/v1/auth/user/signin | python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
X=$(curl -s -b jar -H "xc-auth: $JWT" -H 'Content-Type: application/json' \
-d '{"description":"laptop"}' $U/api/v1/tokens | python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')Use $X from here on, not the sign-in token. A password sign-in signs out that user's other sessions: our browser landed on /signin right after a script signed in as the same user, while the API token kept working. We saw this twice.
Create a base, a table and records with the API token:
curl -s -b jar -H "xc-token: $X" -H 'Content-Type: application/json' -d '{"title":"crm"}' $U/api/v2/meta/bases
# {"id":"p00ntx9hkxipp65","title":"crm",…}
curl -s -b jar -H "xc-token: $X" -H 'Content-Type: application/json' -d @table.json \
$U/api/v2/meta/bases/p00ntx9hkxipp65/tables
# {"id":"m1ajgz0zz889w09","table_name":"contacts",…}
curl -s -b jar -H "xc-token: $X" -H 'Content-Type: application/json' \
-d '[{"Name":"Ada Lovelace","Email":"ada@example.com","Stage":"customer"},
{"Name":"Alan Turing","Email":"alan@example.com","Stage":"lead"}]' \
$U/api/v2/tables/m1ajgz0zz889w09/records{"table_name":"contacts","title":"Contacts","columns":[
{"column_name":"name","title":"Name","uidt":"SingleLineText"},
{"column_name":"email","title":"Email","uidt":"Email"},
{"column_name":"stage","title":"Stage","uidt":"SingleSelect","dtxp":"'lead','customer'"}]}[{"Id":1},{"Id":2}]That JSON is table.json. Filtering uses NocoDB's where syntax; the v3 data API answered on the same host:
curl -s -b jar -H "xc-token: $X" "$U/api/v2/tables/m1ajgz0zz889w09/records?fields=Id,Name,Stage&where=(Stage,eq,lead)"
curl -s -b jar -H "xc-token: $X" "$U/api/v3/data/p00ntx9hkxipp65/m1ajgz0zz889w09/records?fields=Name,Stage"{"list":[{"Id":2,"Name":"Alan Turing","Stage":"lead"}],"pageInfo":{"totalRows":1,…}}
{"records":[{"id":1,"id_fields":{"Id":1},"fields":{"Name":"Ada Lovelace","Stage":"customer"}},{"id":2,…}]}How to tell which gate said no:
| Request | Answer |
|---|---|
xc-token, no cookie |
401 HTML, "Access token required" (the Sandboxer proxy) |
Cookie: __sbx_auth=<token> header plus xc-token |
200 |
Cookie, bad xc-token |
401 JSON, ERR_AUTHENTICATION_REQUIRED (NocoDB) |
From our test machine the list call took 0.21 s median; inside the sandbox it took 25 ms. Code that runs in the sandbox should call http://localhost:8080 and skip the cookie.
6. The tables are plain Postgres#
Each NocoDB base is a Postgres schema named after the base id. Rows written with SQL show up in the API and the grid:
sbx exec $id "cd noco && docker compose exec -T db psql -U noco -d noco \
-c 'select id, name, email, stage from p00ntx9hkxipp65.contacts order by id' \
-c \"insert into p00ntx9hkxipp65.contacts (name, email, stage) values ('Grace Hopper', 'grace@example.com', 'lead')\"" id | name | email | stage
----+--------------+------------------+----------
1 | Ada Lovelace | ada@example.com | customer
2 | Alan Turing | alan@example.com | lead
(2 rows)
INSERT 0 1The next where=(Stage,eq,lead) call returned both Alan Turing and Grace Hopper, and the grid showed 3 records. This is useful for loading data with COPY or letting a script in the sandbox write with SQL while people edit in the UI. NocoDB's metadata lives in the public schema of the same database, so don't drop tables there.
7. Shared views still need the sandbox token#
NocoDB can share a view as a read-only link. We created one with POST /api/v2/meta/views/<view id>/share, which returned a uuid, and opened /nc/view/<uuid> in a fresh browser with no NocoDB login:
| Link | Result |
|---|---|
/nc/view/<uuid> |
401 from the Sandboxer proxy |
/nc/view/<uuid>?_token=<token> |
200, the Contacts grid, read only |
So a share link from a private sandbox must carry the sandbox token, and anyone with that token can reach the sign-in page and every other share link too. To share views with outsiders, host the port with --public and rely on NocoDB's own auth. Switching between private and public mints a new token for private links.
8. What survives restarts, stop and resume#
Docker restart. sudo systemctl restart docker took both containers down. restart: unless-stopped brought them back, and NocoDB answered /api/v1/health 13.4 s later. The browser session and the API token both still worked.
Stop and resume.
sbx stop $id --wait # 2.9 s
sbx resume $id # 2.1 s
sbx info $id # "memoryRestored": trueThe containers kept running through it (Up 5 minutes), the API returned all records, and the headless browser opened the grid deep link without signing in again.
Boot from disk. A resize or a resume on a different CPU type boots from disk. We stopped again and ran sbx resume $id --type default: memoryRestored: false, resumeReason: resized. Docker started with the VM, NocoDB was healthy 13 s after boot, and the records and API token were intact.
9. Clean up#
sbx delete $id --yesAfter the delete, sbx info returned not_found and the URL returned 404. Export anything you need first, for example with pg_dump inside the db container and sbx scp. A small sandbox costs $0.018/hour while it runs and nothing while it is stopped.
FAQ#
Can I use SQLite instead of Postgres?#
Yes. NocoDB's README runs the image with only a volume on /usr/app/data, and it then keeps everything in a SQLite file there. We tested Postgres, because it lets you read and write the same tables with SQL and pg_dump.
Why did my script sign me out of the browser?#
In our tests, a password sign-in through /api/v1/auth/user/signin ended the same user's other sessions. Sign in once to create an API token, then send it as xc-token. API tokens survived sign-ins, Docker restarts and resumes.
Do I need NC_AUTH_JWT_SECRET?#
NocoDB's README sets it, and so did we. In one test we emptied it and recreated the container, and the existing browser session still worked, so this version seems to keep the secret in its own database. Set it anyway, as the docs recommend.
Is small big enough?#
For a team workspace or a demo, yes. NocoDB used about 800 MB and Postgres 59 MB, leaving 2.6 GB of the 3.9 GB that free reported available. Moving to a smaller size isn't supported yet, so start small and resize up if needed.
Can an app in the same sandbox use the API?#
Yes. Call http://localhost:8080 with xc-token; no cookie is needed, and the call took 25 ms median against 0.21 s through the hosted URL from our test machine.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.