Baserow
A no-code online database platform you can self-host.
Baserow is an open-source alternative to Airtable that lets you create relational databases through a spreadsheet-like interface without writing code. It supports multiple views (grid, gallery, kanban, form), REST API access, and team collaboration. Self-hosting avoids Airtable's per-seat pricing and keeps your data on your own infrastructure.
Alternatives considered
Cloud Hosted
| Tool | Open Source | Free Tier | Monthly Cost |
|---|---|---|---|
| Airtable | No | Limited | From $20/seat |
| Grist | Yes | Limited | From $8/seat |
| Firebase | No | Limited | Pay-as-you-go |
Self Hosted
| Tool | Open Source | Full Features | Notes |
|---|---|---|---|
| NocoDB | Yes | Yes | Airtable-like, MIT license |
| Grist | Yes | Yes | Spreadsheet/database hybrid |
Installation
Architecture
- Deployments:
backend(wsgi + worker + celery beat),web-frontend, andminiofor S3-compatible file storage - StatefulSet:
valkey/valkey:9.0.3-alpinefor Redis-compatible caching/queuing - Images:
baserow/backend:2.2.0,baserow/web-frontend:2.1.6,minio/minio:RELEASE.2024-06-26T01-06-18Z(all digest-pinned) - Database: CNPG PostgreSQL cluster with Longhorn-encrypted PVCs
- Networking: HTTPRoute via internal gateway
Security
- App containers run as
runAsUser: 9999,runAsNonRoot: true,allowPrivilegeEscalation: false, capabilities dropped - Longhorn PVCs encrypted at rest via SOPS-managed keys
Updates
Managed by Renovate. All images are digest-pinned.
Data Management
- Database: CNPG PostgreSQL cluster (Longhorn-encrypted PVCs)
- Object storage: MinIO PVC (
minio-data-encrypted, Longhorn-encrypted) for file uploads - Backups: k8up
Schedule(schedule-hetzner) backs up PVCs to Hetzner S3 (workload-talos-baserow-6d5183c9).PreBackupPodrunspg_dumpfor consistent PostgreSQL backups.
User Management
No OIDC configured. Users are managed through the Baserow admin UI.
Configuration Management
- App secret key, Redis host, S3 credentials, and URLs from SOPS-encrypted secret
- Database credentials injected from CNPG-generated secret
Migrate from one Instance to another
Migrating and backing up data is sparsely documented in Baserow currently.
- Export data — this includes database and file storage
- For docker all-in-one:
docker-compose exec -it baserow ./baserow.sh backend-cmd manage export_workspace_applications {{old_workspace_id}} --indent
- For selfhosted kubernetes exec into wsgi pod:
cd /baserow/backend
source ../venv/bin/activate
./baserow export_workspace_applications {{old_workspace_id}} --indent
- Move data to new instance. Depending on your setup via
docker cporkubectl cp - Import data
- For docker all-in-one:
docker-compose exec -it baserow ./baserow.sh backend-cmd manage import_workspace_applications {{new_workspace_id}} {{filename excluding .zip or .json}}
- For selfhosted kubernetes exec into wsgi pod:
cd /baserow/backend
source ../venv/bin/activate
./baserow import_workspace_applications {{new_workspace_id}} {{filename excluding .zip or .json}}
Note: If you are working with a self-hosted environment, there is only one workspace. So you need to insert one of the available workspace IDs.
Administration
Usage
Create tables and views from the web UI. Use the built-in form view to collect data from external users. Access data programmatically via the REST API. Invite team members to collaborate on shared workspaces and databases.
Cluster-specific deviations from the above live in the per-cluster README — see k8s/apps/talos/baserow/README.md.
Cluster Deployment
Baserow — Talos cluster
Cluster-specific notes only. General product info, "why we use it", and alternatives live in docusaurus/docs/apps/baserow.mdx.
Deviations from defaults
Defaults live in docusaurus/docs/apps/baserow.mdx — document anything this cluster does differently here, with a one-line reason.
- Image:
baserow/backend:2.2.2@sha256:78a0bba005145d73cf7f1dfeb273e76d61ea11773bdd4b74f24823fe0e24c1d0 - Image:
baserow/web-frontend:2.2.2@sha256:6edc9117a520399ef787cbd596f4f1cd26db2b3051c10cf26c6331636b79c9b5 - Image:
minio/minio:RELEASE.2024-06-26T01-06-18Z@sha256:337d6f0e90b1992759b832ede340a170cc8ca5cd5db097898b15a3ed121351f7 - Image:
valkey/valkey:9.1.0-alpine@sha256:a35428eba9043cc0b79dbe54100f0c92784f2de00ad09b01182bfb1c5c83d1bd
Rendered manifests (kustomize build)
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app: backend-asgi
name: backend-asgi
namespace: baserow
spec:
replicas: 1
selector:
matchLabels:
app: backend-asgi
ingress: public
template:
metadata:
labels:
app: backend-asgi
ingress: public
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- backend-asgi
topologyKey: kubernetes.io/hostname
weight: 1
containers:
- args:
- gunicorn
env:
- name: DATABASE_HOST
valueFrom:
secretKeyRef:
key: host
name: cnpg-app
- name: DATABASE_NAME
valueFrom:
secretKeyRef:
key: dbname
name: cnpg-app
- name: DATABASE_USER
valueFrom:
secretKeyRef:
key: user
name: cnpg-app
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: cnpg-app
- name: DATABASE_PORT
valueFrom:
secretKeyRef:
key: port
name: cnpg-app
envFrom:
- secretRef:
name: baserow
image: baserow/backend:2.2.2@sha256:78a0bba005145d73cf7f1dfeb273e76d61ea11773bdd4b74f24823fe0e24c1d0
imagePullPolicy: Always
name: backend-asgi
ports:
- containerPort: 8000
name: backend-asgi
readinessProbe:
exec:
command:
- curl
- '--fail'
- '--silent'
- http://localhost:8000/api/_health/
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 5
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
workingDir: /baserow
securityContext:
fsGroup: 9999
fsGroupChangePolicy: OnRootMismatch
runAsGroup: 9999
runAsNonRoot: true
runAsUser: 9999
seccompProfile:
type: RuntimeDefault
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app: backend-worker
name: backend-worker
namespace: baserow
spec:
replicas: 1
selector:
matchLabels:
app: backend-worker
ingress: public
template:
metadata:
labels:
app: backend-worker
ingress: public
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- backend-worker
topologyKey: kubernetes.io/hostname
weight: 1
containers:
- args:
- celery-worker
env:
- name: DATABASE_HOST
valueFrom:
secretKeyRef:
key: host
name: cnpg-app
- name: DATABASE_NAME
valueFrom:
secretKeyRef:
key: dbname
name: cnpg-app
- name: DATABASE_USER
valueFrom:
secretKeyRef:
key: user
name: cnpg-app
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: cnpg-app
- name: DATABASE_PORT
valueFrom:
secretKeyRef:
key: port
name: cnpg-app
envFrom:
- secretRef:
name: baserow
image: baserow/backend:2.2.2@sha256:78a0bba005145d73cf7f1dfeb273e76d61ea11773bdd4b74f24823fe0e24c1d0
imagePullPolicy: Always
name: backend-worker
readinessProbe:
exec:
command:
- /bin/bash
- '-c'
- /baserow/backend/docker/docker-entrypoint.sh celery-worker-healthcheck
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 10
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
- args:
- celery-exportworker
envFrom:
- secretRef:
name: baserow
image: baserow/backend:2.2.2@sha256:78a0bba005145d73cf7f1dfeb273e76d61ea11773bdd4b74f24823fe0e24c1d0
imagePullPolicy: Always
name: backend-export-worker
readinessProbe:
exec:
command:
- /bin/bash
- '-c'
- /baserow/backend/docker/docker-entrypoint.sh celery-exportworker-healthcheck
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 10
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
- args:
- celery-beat
envFrom:
- secretRef:
name: baserow
image: baserow/backend:2.2.2@sha256:78a0bba005145d73cf7f1dfeb273e76d61ea11773bdd4b74f24823fe0e24c1d0
imagePullPolicy: Always
name: backend-beat-worker
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
fsGroup: 9999
fsGroupChangePolicy: OnRootMismatch
runAsGroup: 9999
runAsNonRoot: true
runAsUser: 9999
seccompProfile:
type: RuntimeDefault
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app: backend-wsgi
name: backend-wsgi
namespace: baserow
spec:
replicas: 1
selector:
matchLabels:
app: backend-wsgi
ingress: public
template:
metadata:
labels:
app: backend-wsgi
ingress: public
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- backend-wsgi
topologyKey: kubernetes.io/hostname
weight: 1
containers:
- args:
- gunicorn-wsgi
- '--timeout'
- '60'
env:
- name: DATABASE_HOST
valueFrom:
secretKeyRef:
key: host
name: cnpg-app
- name: DATABASE_NAME
valueFrom:
secretKeyRef:
key: dbname
name: cnpg-app
- name: DATABASE_USER
valueFrom:
secretKeyRef:
key: user
name: cnpg-app
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: cnpg-app
- name: DATABASE_PORT
valueFrom:
secretKeyRef:
key: port
name: cnpg-app
envFrom:
- secretRef:
name: baserow
image: baserow/backend:2.2.2@sha256:78a0bba005145d73cf7f1dfeb273e76d61ea11773bdd4b74f24823fe0e24c1d0
imagePullPolicy: Always
name: backend-wsgi
ports:
- containerPort: 8000
name: backend-wsgi
readinessProbe:
exec:
command:
- sh
- '-c'
- curl -f -s http://localhost:8000/api/_health/
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 5
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
startupProbe:
exec:
command:
- sh
- '-c'
- curl -f -s http://localhost:8000/api/_health/
failureThreshold: 30
periodSeconds: 5
timeoutSeconds: 5
workingDir: /baserow
securityContext:
fsGroup: 9999
fsGroupChangePolicy: OnRootMismatch
runAsGroup: 9999
runAsNonRoot: true
runAsUser: 9999
seccompProfile:
type: RuntimeDefault
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app.kubernetes.io/instance: minio
app.kubernetes.io/name: minio
name: minio
namespace: baserow
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/instance: minio
app.kubernetes.io/name: minio
ingress: public
template:
metadata:
labels:
app.kubernetes.io/instance: minio
app.kubernetes.io/name: minio
ingress: public
spec:
containers:
- args:
- server
- '--console-address'
- ':9001'
- /data
env:
- name: MINIO_ROOT_USER
valueFrom:
secretKeyRef:
key: AWS_ACCESS_KEY_ID
name: baserow
- name: MINIO_ROOT_PASSWORD
valueFrom:
secretKeyRef:
key: AWS_SECRET_ACCESS_KEY
name: baserow
- name: MINIO_SERVER_URL
value: https://minio.baserow.web.kueber.eu
- name: MINIO_CONFIG_DIR
value: /config
image: >-
minio/minio:RELEASE.2024-06-26T01-06-18Z@sha256:337d6f0e90b1992759b832ede340a170cc8ca5cd5db097898b15a3ed121351f7
livenessProbe:
failureThreshold: 3
httpGet:
path: /minio/health/live
port: 9000
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 10
name: minio
ports:
- containerPort: 9000
name: web
protocol: TCP
- containerPort: 9001
name: admin
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /minio/health/ready
port: 9000
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 15
successThreshold: 1
timeoutSeconds: 10
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
volumeMounts:
- mountPath: /data
name: minio-data
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
volumes:
- name: minio-data
persistentVolumeClaim:
claimName: minio-data-encrypted
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app: web-frontend
name: web-frontend
namespace: baserow
spec:
replicas: 1
selector:
matchLabels:
app: web-frontend
ingress: public
template:
metadata:
labels:
app: web-frontend
ingress: public
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-frontend
topologyKey: kubernetes.io/hostname
weight: 1
containers:
- args:
- /baserow/web-frontend/docker/docker-entrypoint.sh
- nuxt
envFrom:
- secretRef:
name: baserow
image: baserow/web-frontend:2.2.2@sha256:6edc9117a520399ef787cbd596f4f1cd26db2b3051c10cf26c6331636b79c9b5
imagePullPolicy: Always
name: web-frontend
ports:
- containerPort: 3000
name: web-frontend
readinessProbe:
httpGet:
path: /_health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: valkey
namespace: baserow
spec:
replicas: 1
selector:
matchLabels:
app: valkey
serviceName: valkey
template:
metadata:
labels:
app: valkey
spec:
containers:
- args:
- valkey-server
image: valkey/valkey:9.1.0-alpine@sha256:a35428eba9043cc0b79dbe54100f0c92784f2de00ad09b01182bfb1c5c83d1bd
livenessProbe:
initialDelaySeconds: 10
periodSeconds: 10
tcpSocket:
port: 6379
name: valkey
ports:
- containerPort: 6379
name: client
readinessProbe:
initialDelaySeconds: 3
periodSeconds: 5
tcpSocket:
port: 6379
resources:
limits:
memory: 512Mi
requests:
cpu: 50m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
volumeMounts:
- mountPath: /conf
name: conf
- mountPath: /data
name: data
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 999
seccompProfile:
type: RuntimeDefault
volumes:
- emptyDir: {}
name: conf
- emptyDir: {}
name: data