What Baserow asks of the machine
Baserow's supported-versions page lists software, not hardware. For release 2.4.1 it gives these floors and recommendations; it publishes no CPU or RAM minimum, so size from the process count below and your own row and file volume.
| Dependency | Supported | Recommended |
|---|---|---|
| Docker | >= 19.03 | Latest |
| docker-compose | >= 1.19.0 | Latest |
| PostgreSQL | >= 14 | 15+ |
| pgvector | >= 0.8.1 | Latest |
| Redis | >= 6.2.7 | Latest |
The compose file starts Caddy, the backend, the web frontend, three Celery workers (tasks, exports, beat), PostgreSQL 15 with pgvector, and Redis. For memory, the setting to watch is BASEROW_AMOUNT_OF_WORKERS: in the standalone images this stack uses it defaults to the number of available cores, and each Celery process uses memory. On a many-core host, set it in .env.
All-in-one image or the compose stack
| baserow/baserow (all-in-one) | docker-compose.yml (multi-container) | |
|---|---|---|
| Containers | One: PostgreSQL, Redis, Caddy and Baserow inside | Caddy, backend, web-frontend, Celery workers, db, redis |
| Secrets | Generated on first start and kept in /baserow/data (.secret, .pgpass) | You set SECRET_KEY, DATABASE_PASSWORD, REDIS_PASSWORD in .env |
| Celery workers | Default 1 | Default = available cores |
| Backup | Stop, then tar the baserow_data volume | Docs: backend backup command (stack idle). This page adds pg_dump plus the media volume |
The compose file's header recommends the all-in-one image unless you are an advanced user. On a dedicated host the separate containers pay off: PostgreSQL is reachable with ordinary tools, pg_dump runs against a live stack, and each service restarts on its own. This page uses the compose stack.
The machine to run it on
A dedicated server is a whole machine: root, no hypervisor, no other tenant competing with PostgreSQL for I/O. Classes that fit, from our own range:
- Moonshot cartridges for one service per machine: a Baserow for one team, alone on its box. The Xeon D-1587 and E3-1585L v5 cartridges carry NVMe storage.
- Single-socket servers for an application with its database on the same machine, which is exactly this stack.
- Dual-socket servers when Baserow shares the host with other services, such as the API backends people wire to it.
Racked single-, dual- and quad-socket servers carry an out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC), as do the m510 and m710x cartridges, so a firewall mistake costs a console session, not a reinstall. Ports are unmetered, the network is our own AS19624, and a racked machine is handed over in about 30 minutes once payment is approved.
Installing the compose stack on Ubuntu
Work as root (sudo -i first). The block enables ufw with SSH only, switching IPv6 on before any allow rule so the rules get IPv6 copies. It fetches the three files the compose stack needs (docker-compose.yml, the Caddyfile it mounts, and .env.example), writes 50-character random values for the mandatory secrets into .env at mode 600, and sets BASEROW_PUBLIC_URL to the tunnel address of the first login. The docs say this URL must match the browser's address bar; any other host is treated as a published application. HOST_PUBLISH_IP=127.0.0.1 keeps Caddy's 80 and 443 on loopback.
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
# Docker Engine 28+ and the Compose plugin, per docs.docker.com (check: docker version)
mkdir -p /opt/baserow && cd /opt/baserow
B=https://raw.githubusercontent.com/baserow/baserow/refs/heads/master
curl -fsSLo docker-compose.yml $B/docker-compose.yml
curl -fsSLo Caddyfile $B/Caddyfile
curl -fsSLo .env $B/.env.example
chmod 600 .env
for v in SECRET_KEY DATABASE_PASSWORD REDIS_PASSWORD; do
sed -i "s/^$v=.*/$v=$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)/" .env
done
sed -i 's|^BASEROW_PUBLIC_URL=.*|BASEROW_PUBLIC_URL=http://localhost:8080|' .env
echo 'HOST_PUBLISH_IP=127.0.0.1' >> .env
docker compose up -d
docker compose logs -f backend
The secrets pass briefly through sed's arguments, visible to ps on that host; shell history keeps only the $(...) expression. The docs write docker-compose; with the Compose plugin it is docker compose.
First signup over a tunnel, then public
Baserow's admin-panel docs state that the first user to sign up on a self-hosted instance becomes an Instance Admin with staff privileges. Make that signup over SSH, through your own login, while nothing listens publicly. Then, under Admin tools, Settings, Account restrictions, turn off Allow creating new accounts: the signup page disappears. To let invited colleagues register, enable Allow signups via workspace invitations, which shows once the first toggle is off.
# 1. On your workstation, with your own login on the server:
ssh -N -L 8080:127.0.0.1:80 you@your-server
# Browse to http://localhost:8080 and sign up. Then Admin tools > Settings:
# switch off "Allow creating new accounts".
# 2. On the server, as root in /opt/baserow, once DNS points at it:
sed -i 's|^BASEROW_PUBLIC_URL=.*|BASEROW_PUBLIC_URL=https://baserow.example.com|' .env
sed -i '/^HOST_PUBLISH_IP=/d' .env
echo 'BASEROW_CADDY_ADDRESSES=:443' >> .env
ufw allow 80,443/tcp
docker compose up -d
Without HOST_PUBLISH_IP the mapping falls back to 0.0.0.0, and Docker publishes on [::] too. Published traffic skips ufw on IPv4, and on IPv6 when Docker's own IPv6 networking is on; otherwise the [::] listener is docker-proxy, which ufw filters, hence ufw allow 80,443/tcp. Bind any other published port to 127.0.0.1, which stays local on Docker Engine 28+ (moby#45610). :443 makes Caddy fetch the certificate.
Outgoing email
Without EMAIL_SMTP, Baserow sends nothing and writes each message to the Celery worker log instead, so invitations and password resets never arrive. Add to .env, then docker compose up -d:
EMAIL_SMTP=true,EMAIL_SMTP_HOST,EMAIL_SMTP_PORT,EMAIL_SMTP_USER,EMAIL_SMTP_PASSWORD,FROM_EMAIL.EMAIL_SMTP_USE_TLSfor explicit TLS (generally 587) orEMAIL_SMTP_USE_SSLfor implicit TLS (generally 465), never both.- The docs treat each switch as on for any non-empty value, so (our reading) a value of False would still switch it on: leave the unused one blank.
Inbound email for workflow triggers is a separate service behind the inbound-email compose profile and needs port 25; leave it off unless you use that trigger.
Backups and upgrades
The data lives in PostgreSQL and the uploads in the media volume, so a backup is both, plus .env. The block writes a nightly script and a cron file; pasting it stops nothing. pg_dump gives a consistent snapshot of a running database (PostgreSQL's behaviour, not a Baserow claim); the media archive follows a moment later, so a file uploaded in between can be missing. baserow_media is named after the directory /opt/baserow. The same PostgreSQL page warns that beyond simple cases pg_dump is not the right tool for regular production backups; a single-team Baserow is the simple case (our reading), and the docs' own backup command remains the one to use before an upgrade.
mkdir -p -m 700 /var/backups/baserow
cat > /opt/baserow/backup.sh <<'EOF'
set -e
cd /opt/baserow
d=/var/backups/baserow; t=$(date +%F)
docker compose exec -T db pg_dump -U baserow -Fc baserow > $d/db-$t.dump
docker run --rm -v baserow_media:/media:ro -v $d:/out ubuntu tar czf /out/media-$t.tar.gz -C /media .
cp .env $d/env-$t
EOF
chmod 700 /opt/baserow/backup.sh
echo '30 3 * * * root umask 077; bash /opt/baserow/backup.sh > /var/log/baserow-backup.log 2>&1' > /etc/cron.d/baserow-backup
Before an upgrade, use the docs' own tool: stop the stack and run the backend backup -f command into a directory owned by 9999:9999. The docs want the database idle while it runs, and a restore to go into a new, empty database. Copy every archive off the machine yourself; the dumps and env copies hold secrets.
Images default to the version pinned in the compose file (2.4.1 as written; override with BASEROW_VERSION). To move up: back up, stop with docker compose down, download the current docker-compose.yml and Caddyfile, start again, and follow the logs until the line saying Baserow is now available appears. Migrations run at startup with no intermediate versions; worker errors like column ... does not exist during them are expected, per the docs. A volume first created before 1.24 holds PostgreSQL 11 data and needs the documented pgautoupgrade step first.
Licence: what is MIT and what is not
Baserow's LICENSE file makes the Baserow OSE (Open Source Edition) everything outside its listed exceptions, and puts it under the "MIT Expat" license. The exceptions: docs/ is under CC BY-SA 4.0, third-party components keep their own licences, and premium/ and enterprise/ are "licensed under the license defined in" their own LICENSE files; the Premium Edition licence allows production use only with a valid subscription for the correct number of users. Self-hosted licences come as Premium, Advanced or Enterprise, one type per instance, and free and paid users can share workspaces. A key is tied to one instance ID and registered under Admin tools, Manage licenses.
When this is the wrong setup
- You need horizontal scaling: the docs point to the standalone images on Kubernetes or Helm, with external PostgreSQL, Redis and object storage.
- Nobody will patch Docker and the OS or read the backup log. Basic management covers hardware, not your containers; support lists the tiers.
- PostgreSQL already runs elsewhere: set the
DATABASE_*variables and drop thedbservice.
Related builds: self-hosted Supabase for a Postgres-backed API platform, and Nginx Proxy Manager if several apps share the host's 80 and 443.
Questions
Is self-hosted Baserow free?
The Open Source Edition, everything outside the LICENSE file's exceptions (premium, enterprise, docs and third-party components), is under the MIT Expat licence and costs nothing to run on your own server. Code in the premium and enterprise directories is under separate licences and needs a paid self-hosted licence (Premium, Advanced or Enterprise) for production use. Free and licensed users can work in the same workspaces.
How much RAM does Baserow need?
The docs publish no RAM or CPU minimum; the requirements page lists software versions only. Memory grows with the Celery and gunicorn worker counts, and in the compose stack Celery defaults to one worker per core. Set BASEROW_AMOUNT_OF_WORKERS yourself; on the all-in-one image, BASEROW_RUN_MINIMAL trades some speed for less memory.
Why does my Baserow show a published application instead of the login page?
BASEROW_PUBLIC_URL does not match the address in the browser bar. Baserow treats any other host as an application builder domain. Set it to the exact scheme, host and non-standard port you browse to, and restart the stack.
Who becomes the admin of a new Baserow instance?
The first account to sign up becomes the Instance Admin with staff rights. Create it over an SSH tunnel while the ports are bound to 127.0.0.1, then turn off public account creation in the admin settings before you expose the site.
Can I use my own reverse proxy instead of Caddy?
Yes. The docs have Nginx and Apache guides and strongly recommend keeping the bundled Caddy and forwarding your proxy to it, since it already routes the API, WebSocket and media paths; a no-caddy compose variant exists for those who want to drop it. Keep Caddy's port on 127.0.0.1 so only your proxy reaches it.