Introducción
Baserow es una alternativa libre a Airtable: una base de datos colaborativa con interfaz de hoja de cálculo, edición en tiempo real y API REST. A diferencia de la mayoría de aplicaciones que se despliegan en un hosting de Core-Admin, Baserow no es PHP: es un conjunto de servicios (Django, Celery, PostgreSQL, Redis y un frontend Nuxt) que necesitan ejecutarse de forma independiente.
Este artículo describe cómo desplegar Baserow como contenedor Docker escuchando únicamente en localhost, y publicarlo bajo un dominio con certificado usando la función Reverse proxy (ProxyPass) del módulo Webhosting Management de Core-Admin.
El mismo patrón sirve para cualquier otro servicio que no sea PHP y hable HTTP: Grafana, n8n, Metabase, Gitea, Uptime Kuma, etc. Los detalles específicos de Baserow están señalados como tales.
Requisitos previos
- Core-Admin instalado con el módulo Webhosting Management activo
- Docker instalado y funcionando en el servidor
- Un alojamiento ya creado para el dominio que se va a usar (por ejemplo
baserow.ejemplo.com), con su certificado SSL ya emitido (Let’s Encrypt o importado) - El dominio resolviendo a la IP del servidor
- Acceso de administrador al panel de Core-Admin (la pestaña Site Options requiere permisos de administrador)
- Un puerto local libre por encima de 1024. En los ejemplos se usa el
18267
Nota: el alojamiento no necesita contenido. Su
document_rootquedará vacío: sólo actuará de frontal HTTPS.
Por qué este planteamiento y no otro
Antes de entrar en materia conviene justificar la elección, porque hay varias formas de hacerlo y no todas son equivalentes.
| Opción | Valoración |
|---|---|
Imagen todo-en-uno baserow/baserow + ProxyPass del alojamiento |
Recomendada. Un único contenedor, un único puerto, y el certificado, la redirección a HTTPS y los logs los sigue gestionando Core-Admin |
docker-compose con los servicios separados |
Sólo si se va a escalar o se quiere el PostgreSQL fuera del contenedor. Más piezas que mantener y actualizar |
| Instalación manual sobre el sistema | Desaconsejada: obliga a mantener a mano Python, Node, PostgreSQL, Redis y supervisord, y complica mucho las actualizaciones |
| Contenedor LXC dedicado (módulo LXC Management) + ProxyPass | Alternativa razonable si el servidor ya trabaja con LXC y no se quiere introducir Docker. El esquema de publicación es idéntico |
Sobre el módulo Docker Manager
Core-Admin incluye un módulo Docker Manager que lista contenedores, imágenes y volúmenes, y permite arrancarlos, pararlos y suspenderlos desde el panel. Es cómodo para el día a día y el contenedor que creemos a mano aparecerá en él automáticamente, porque el listado se obtiene de docker ps.
Sin embargo, para este despliegue conviene tener presentes dos limitaciones:
- La entidad Web mapping de Docker Manager genera su propio VirtualHost de Apache con su propio Let’s Encrypt. Si el dominio ya tiene un alojamiento creado, ambas configuraciones entran en conflicto. Para este caso hay que publicar con el ProxyPass del alojamiento, no con el Web mapping.
- El servicio de creación de contenedores construye el
docker runcon--name,-e,-py--restart, pero no soporta volúmenes (-v). Cualquier servicio que necesite persistencia —Baserow la necesita— debe crearse desde línea de comandos.
Paso 1: desplegar el contenedor
Crear el volumen de datos
Baserow guarda absolutamente todo su estado en /baserow/data: la base de datos PostgreSQL, los ficheros subidos por los usuarios y los secretos generados en el primer arranque. Ese directorio debe ir en un volumen.
docker volume create baserow_data
Arrancar el contenedor
docker run -d --name baserow --restart unless-stopped --shm-size=1g -e BASEROW_PUBLIC_URL=https://baserow.ejemplo.com -e BASEROW_CADDY_ADDRESSES=http://:80 -e BASEROW_TRIGGER_SYNC_TEMPLATES_AFTER_MIGRATION=false -v baserow_data:/baserow/data -p 127.0.0.1:18267:80 baserow/baserow:2.3.3
Conviene detenerse en cada opción, porque casi todas resuelven un problema concreto:
| Opción | Motivo |
|---|---|
-p 127.0.0.1:18267:80 |
Crítico por seguridad. Sin el 127.0.0.1, Docker inserta sus reglas en la cadena DOCKER y en nat PREROUTING, por delante de INPUT, y el servicio queda accesible desde internet saltándose el Firewall Manager de Core-Admin y sin cifrado |
--shm-size=1g |
Imprescindible para las copias de seguridad. Docker asigna sólo 64 MB de memoria compartida, y Baserow construye los volcados de PostgreSQL en /dev/shm. Con la base vacía funciona; en cuanto haya datos reales el backup fallará con No space left on device
|
BASEROW_PUBLIC_URL |
URL pública del servicio. De aquí deriva Baserow su ALLOWED_HOSTS y las URL que genera |
BASEROW_CADDY_ADDRESSES=http://:80 |
Evita que el Caddy interno intente gestionar su propio HTTPS. El TLS lo termina Apache |
BASEROW_TRIGGER_SYNC_TEMPLATES_AFTER_MIGRATION=false |
Evita la sincronización de las ~157 plantillas de ejemplo, que consume varios minutos de CPU y disco en cada actualización. Omítelo si quieres disponer de las plantillas |
--restart unless-stopped |
El contenedor vuelve a levantarse tras un reinicio del servidor |
baserow/baserow:2.3.3 |
Fija siempre una versión concreta. Con latest una actualización de imagen puede cambiarte de versión sin pretenderlo |
Nota sobre descargas: la imagen ronda los 600 MB repartidos en unas 25 capas. En servidores con salida inestable es habitual que el
docker pullse corte conconnection reset by peer. Reintentar retoma donde se quedó; si ocurre a menudo,{"max-concurrent-downloads": 1}en/etc/docker/daemon.jsonhace las descargas más lentas pero mucho más fiables.
Seguir el primer arranque
El primer arranque crea el PostgreSQL interno y ejecuta todas las migraciones. Puede tardar varios minutos:
docker logs -f baserow
Sal con Ctrl+C cuando deje de moverse (eso no para el contenedor) y espera a que el healthcheck pase a healthy:
docker ps --filter name=baserow --format '{{.Status}} | {{.Ports}}'
Debe mostrar Up X minutes (healthy) | 127.0.0.1:18267->80/tcp. Ese 127.0.0.1 en la columna de puertos es la confirmación de que el servicio no está expuesto.
Comprobar que responde
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: baserow.ejemplo.com" http://127.0.0.1:18267/
curl -s -H "Host: baserow.ejemplo.com" http://127.0.0.1:18267/api/settings/ | head -c 200; echo
El primero debe devolver 302 (redirección al login) y el segundo un JSON con la configuración de la instancia.
Muy importante: la cabecera
-H "Host: ..."no es un adorno. Si haces la prueba contra127.0.0.1sin ella, obtendrás 404 en toda la aplicación, incluida la API. No es un fallo: el Caddy interno enruta según elHost, y con unHostdesconocido lo manda todo al frontend. Esto tiene una consecuencia directa en el siguiente paso.
Paso 2: preparar el alojamiento
Estos tres ajustes se aplican desde línea de comandos sobre el alojamiento ya existente.
Desactivar el motor PHP
El alojamiento sólo va a hacer de proxy, así que no tiene sentido mantener un pool PHP-FPM levantado. Además elimina toda la superficie de ataque de PHP en ese sitio:
crad-webhosting-mgr.pyc --set-php-engine baserow.ejemplo.com off
Forzar HTTPS
crad-webhosting-mgr.pyc --set-option baserow.ejemplo.com redirect_http_to_https 1
Ampliar el tiempo de espera del proxy
Las importaciones y exportaciones grandes de Baserow pueden superar los 300 segundos por defecto:
crad-webhosting-mgr.pyc --set-option baserow.ejemplo.com proxy_timeout 600
Paso 3: crear el ProxyPass
Esta parte se hace desde el panel web. No existe comando de línea para las reglas de ProxyPass.
- Inicia sesión en el panel de Core-Admin
- Entra en el módulo Webhosting Management
- Selecciona el alojamiento
baserow.ejemplo.com - Abre la pestaña Reverse proxy
- Pulsa Add Proxy pass
Rellena la ficha con estos valores:
| Campo | Valor |
|---|---|
| Path | / |
| Remote URL | http://127.0.0.1:18267 |
| Is active | activado |
| Preserve host header | activado |
| Skip peer SSL certificate check | desactivado (el backend es HTTP, no aplica) |
| Bypass folders | vacío |
Preserve host header es obligatorio aquí, no opcional. Sin esta casilla Apache envía
Host: 127.0.0.1:18267al backend y, como vimos en el paso anterior, Baserow responde 404 en toda la aplicación. El panel advierte de que la opción afecta a todos los ProxyPass del alojamiento; si sólo hay una regla, como es el caso, no tiene efectos colaterales.
Al guardar, Core-Admin regenera la configuración del VirtualHost, activa los módulos de proxy necesarios (proxy, proxy_http, proxy_wstunnel, ssl…) y recarga Apache automáticamente. No hay que hacer nada más a mano.
Paso 4: WebSockets y cabeceras de protocolo
Este paso es el que más se olvida, y sin él Baserow parece funcionar pero se comporta de forma extraña.
El generador de configuración de Core-Admin escribe directivas ProxyPass y ProxyPassReverse planas. Eso basta para HTTP, pero no cubre dos cosas:
-
WebSockets: Baserow usa
/ws/para la colaboración en tiempo real, las notificaciones y el progreso de las importaciones. Sin proxy de WebSocket, la aplicación carga pero se queda reconectando: los cambios hechos en una pestaña no aparecen en otra sin recargar. -
Protocolo original:
mod_proxy_httpañadeX-Forwarded-For,X-Forwarded-HostyX-Forwarded-Server, pero noX-Forwarded-Proto. Sin ella, Django puede generar enlaces y redirecciones enhttp://.
La solución limpia, sin dejar de que Core-Admin gestione el VirtualHost, es la opción de sitio custom_apache2_site_definitions_before, cuyo contenido se escribe antes de las reglas de ProxyPass. Como Apache resuelve por orden de aparición, la regla de /ws/ gana sobre la de /.
- En el alojamiento, abre la pestaña Site Options (requiere permisos de administrador)
- Localiza la opción
custom_apache2_site_definitions_before - Introduce este contenido:
ProxyPass "/ws/" "ws://127.0.0.1:18267/ws/"
ProxyPassReverse "/ws/" "ws://127.0.0.1:18267/ws/"
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
Los módulos mod_proxy_wstunnel y mod_headers ya los activa Core-Admin al crear el ProxyPass, no hay que hacer nada más.
Nota: estas directivas se aplican también al VirtualHost del puerto 80. Por eso se combinan con
redirect_http_to_https=1del paso 2: así por HTTP nunca llega a servirse tráfico real.
Paso 5: verificación
Comprueba que la configuración generada tiene el orden correcto:
grep -n "ws://\|ProxyPass \"" /etc/apache2/sites-enabled/baserow.ejemplo.com*.conf
Las líneas de /ws/ deben tener números de línea menores que las de ProxyPass "/", en los dos VirtualHost (el de :80 y el de :443). Algo así:
10: ProxyPass "/ws/" "ws://127.0.0.1:18267/ws/"
11: ProxyPassReverse "/ws/" "ws://127.0.0.1:18267/ws/"
17: ProxyPass "/" "http://127.0.0.1:18267/"
71: ProxyPass "/ws/" "ws://127.0.0.1:18267/ws/"
72: ProxyPassReverse "/ws/" "ws://127.0.0.1:18267/ws/"
78: ProxyPass "/" "http://127.0.0.1:18267/"
Y que el servicio responde a través de Apache:
curl -s -o /dev/null -w "%{http_code}\n" https://baserow.ejemplo.com/
curl -s https://baserow.ejemplo.com/api/settings/ | head -c 200; echo
La comprobación definitiva del WebSocket sólo puede hacerse desde el navegador: abre el sitio, pulsa F12, ve a Network y filtra por WS. La conexión a wss://baserow.ejemplo.com/ws/core/ debe aparecer como 101 Switching Protocols y mantenerse abierta. Si en su lugar ves reconexiones continuas, revisa el orden del paso 4.
Paso 6: primer acceso y cierre del registro
Recién instalado, Baserow permite que cualquiera se registre. El sitio ya es público, así que este paso no admite demora:
- Entra en
https://baserow.ejemplo.com/signupy crea tu cuenta. La primera cuenta es la de administrador - Ve a Admin → Settings y desactiva Allow new signups
- Si necesitas dar de alta a más gente, usa las invitaciones a espacio de trabajo
Puedes verificar el estado en cualquier momento:
curl -s https://baserow.ejemplo.com/api/settings/ | head -c 120; echo
El campo allow_new_signups debe estar a false.
Copias de seguridad
Este es el punto donde más despliegues se quedan a medias, porque no resulta evidente: el backup de alojamiento de Core-Admin no cubre nada de esto. Ese backup respalda el document_root, que en este montaje está vacío. Los datos viven en el volumen de Docker.
Qué hay que respaldar, y cómo
El volumen contiene dos cosas con necesidades distintas:
| Contenido | Ruta en el volumen | Método |
|---|---|---|
| Base de datos PostgreSQL | /baserow/data/postgres |
Volcado lógico, nunca copia en crudo |
| Ficheros subidos por los usuarios | /baserow/data/media |
Copia directa (no se reescriben una vez subidos) |
La documentación oficial de Baserow propone parar el contenedor y hacer un tar del volumen entero. Es correcto, pero implica cortar el servicio en cada copia. Y copiar en caliente el directorio postgres sin parar es aún peor: produce un backup silenciosamente corrupto, porque los ficheros de datos cambian mientras se leen.
La alternativa sin cortes es el volcado lógico que la propia imagen expone, y que sí produce una instantánea consistente:
docker exec baserow ./baserow.sh backend-cmd backup -f /baserow/data/backups/prueba.tar.gz
Si termina con Successfully backed up Baserow, ya tienes el mecanismo. (Aquí es donde importa el --shm-size=1g del paso 1.)
Script de copia diaria
Guarda esto como /usr/local/bin/crad-baserow-backup.sh con permisos 0700:
#!/bin/bash
# Baserow backup: hot PostgreSQL dump + uploaded media, without stopping the service.
set -u
CONTAINER="baserow"
DEST="/var/backups/baserow"
KEEP_DAYS=7
STAMP=$(date +%Y%m%d-%H%M%S)
LOG="/var/log/baserow-backup.log"
log () { echo "$(date '+%F %T') $*" >> "$LOG"; }
mkdir -p "$DEST" && chmod 0700 "$DEST" || exit 1
# the hot dump requires a running container
if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null)" != "true" ]; then
log "ERROR: container $CONTAINER is not running, aborting"
exit 1
fi
DATA=$(docker volume inspect -f '{{.Mountpoint}}' baserow_data 2>/dev/null)
if [ -z "$DATA" ] || [ ! -d "$DATA" ]; then
log "ERROR: unable to resolve baserow_data mountpoint"
exit 1
fi
# 1. consistent logical dump of PostgreSQL
docker exec "$CONTAINER" mkdir -p /baserow/data/backups >> "$LOG" 2>&1
if ! docker exec "$CONTAINER" ./baserow.sh backend-cmd backup \
-f "/baserow/data/backups/db-$STAMP.tar.gz" >> "$LOG" 2>&1; then
log "ERROR: database dump failed"
exit 1
fi
# 2. move it out of the very volume it is backing up
if ! mv "$DATA/backups/db-$STAMP.tar.gz" "$DEST/"; then
log "ERROR: unable to move the dump out of the volume"
exit 1
fi
chown root:root "$DEST/db-$STAMP.tar.gz"
chmod 0600 "$DEST/db-$STAMP.tar.gz"
# 3. uploaded files: safe to copy hot, they are never rewritten
if [ -d "$DATA/media" ]; then
tar -C "$DATA" -czf "$DEST/media-$STAMP.tar.gz" media 2>> "$LOG"
chmod 0600 "$DEST/media-$STAMP.tar.gz"
else
log "INFO: no media directory yet, skipping uploaded files"
fi
# 4. rotation
find "$DEST" -maxdepth 1 -name 'db-*.tar.gz' -mtime +$KEEP_DAYS -delete
find "$DEST" -maxdepth 1 -name 'media-*.tar.gz' -mtime +$KEEP_DAYS -delete
log "OK: db-$STAMP.tar.gz $(du -h "$DEST/db-$STAMP.tar.gz" 2>/dev/null | cut -f1)"
El paso 2 del script no es un detalle menor: una copia guardada dentro del volumen que respalda no es una copia. Los ficheros terminan en /var/backups/baserow, fuera de Docker.
Programación y retención
Programa la ejecución antes de que pase tu sistema de copias, no a una hora arbitraria. Si tu trabajo de Bacula arranca a las 23:55:
echo '30 23 * * * root /usr/local/bin/crad-baserow-backup.sh' > /etc/cron.d/baserow-backup && chmod 0644 /etc/cron.d/baserow-backup
Un volcado a las 03:15 cuando la copia externa pasa a las 23:55 significa que cada cinta se lleva el volcado del día anterior: un día entero de datos perdidos en una restauración.
La retención local de 7 días sólo tiene que cubrir un par de fallos seguidos; el histórico largo lo guarda el sistema de copias.
Integración con el sistema de copias
Comprueba que /var/backups/baserow entra en el FileSet de tu cliente de backup. Con Bacula, el FileSet que genera Core-Admin para un servidor gestionado incluye /, /boot, /var, /dev y /storage, así que normalmente queda cubierto sin tocar nada. Puedes verificarlo sobre un trabajo Full desde el Director:
crad-bacula-mgr.pyc --list-client-jobs <cliente>-fd --level Full --limit 5
crad-bacula-mgr.pyc --browse-job-files <JobId> --catalog <catalogo> --directory /var/backups/
Importante: en Directores que gestionan muchos clientes, los JobId se repiten entre catálogos. Pasa siempre
--catalog(el nombre aparece en la columnaCatalogdel listado de trabajos); de lo contrario puedes acabar consultando el trabajo de otro cliente y obtener un listado vacío sin ninguna pista de por qué.
Considera excluir /var/lib/docker
Al introducir Docker en el servidor, el backup pasa a llevarse también las capas de imagen (varios GB) y el directorio de datos crudo de PostgreSQL del contenedor. Ese segundo punto es el problemático: es una copia inconsistente, inservible y engañosa, porque alguien podría intentar restaurar desde ahí en una urgencia. Las imágenes, por su parte, se vuelven a descargar.
Valora excluir /var/lib/docker del FileSet: el respaldo bueno es el volcado lógico del script.
Prueba la restauración
Un backup no probado no es un backup. La restauración puede ensayarse en un volumen desechable, sin tocar producción:
docker volume create baserow_restore_test
docker run -it --rm -v /var/backups/baserow:/backup -v baserow_restore_test:/baserow/data baserow/baserow:2.3.3 backend-cmd-with-db restore -f /backup/db-XXXXXXXX-XXXXXX.tar.gz
docker volume rm baserow_restore_test
Actualizaciones
docker pull baserow/baserow:<nueva-version>
docker stop baserow && docker rm baserow
Vuelve a lanzar el docker run del paso 1 cambiando la etiqueta de versión y manteniendo el mismo volumen. Las migraciones de base de datos las ejecuta la imagen al arrancar.
Recomendaciones:
- Ejecuta el script de backup antes de actualizar
- Cambia de versión de una en una dentro de la rama, y lee las notas de publicación antes de saltar de rama mayor
- No hay que tocar nada en Core-Admin: el puerto y el
Hostno cambian, así que el ProxyPass sigue siendo válido
Detalles técnicos
Qué genera Core-Admin en el VirtualHost
La regla de ProxyPass creada desde el panel produce, dentro de cada VirtualHost del alojamiento:
# Proxy pass from core-admin panel
# ProxyPass from rule-id=5
ProxyPass "/" "http://127.0.0.1:18267/"
ProxyPassReverse "/" "http://127.0.0.1:18267/"
# Preserve host header as indicated by caller
ProxyPreserveHost On
Las reglas se ordenan automáticamente de la ruta más específica a la más general. Si la URL remota empieza por https://, Core-Admin añade además SSLProxyEngine on y, si se marca la casilla correspondiente, SSLProxyCheckPeerCN off y SSLProxyCheckPeerExpire off.
El campo Bypass folders
La ficha de ProxyPass tiene un campo Bypass folders en la pestaña Advanced que acepta una lista de carpetas separadas por comas. Genera declaraciones del tipo:
ProxyPass "/static" "!"
Sirve para que ciertas rutas se sirvan directamente desde el document_root del alojamiento en lugar de ir al backend. Con Baserow no hace falta, porque el contenedor sirve sus propios estáticos, pero es útil con otras aplicaciones.
Nunca actives Custom site config en este alojamiento
En la pestaña Custom site config existe una casilla para tomar el control manual de la configuración del sitio. Si se activa, Core-Admin marca el alojamiento con # core_admin_dont_modify y deja de gestionar el VirtualHost: a partir de ahí, ni el ProxyPass del panel ni las opciones de sitio se aplicarán. Para este despliegue no es necesaria en ningún momento.
Cuota de disco y estadísticas
El volumen de Docker no cuenta contra la cuota de disco del alojamiento, y su crecimiento no aparece en las estadísticas del panel. Conviene tenerlo presente al dimensionar el servidor.
Monitorización
El checker de Apache no sabe si el contenedor está vivo: si Baserow se cae, Apache devolverá 503 y el sitio seguirá “respondiendo”. Merece la pena configurar una comprobación HTTP contra https://baserow.ejemplo.com/api/settings/ para detectarlo.
Preguntas frecuentes
Baserow carga, pero los cambios de una pestaña no aparecen en otra sin recargar.
Falta el proxy de WebSocket, o las reglas de /ws/ han quedado por debajo del ProxyPass "/". Revisa el paso 4 y comprueba el orden con el grep del paso 5.
Obtengo 404 en toda la aplicación, incluida la API.
Casi con seguridad falta marcar Preserve host header en la ficha del ProxyPass. El Caddy interno de Baserow enruta según la cabecera Host.
curl contra 127.0.0.1:18267 me devuelve 404, ¿está mal el despliegue?
No. Prueba con -H "Host: baserow.ejemplo.com". Sin la cabecera correcta el 404 es el comportamiento esperado.
El backup falla con No space left on device.
El contenedor se creó sin --shm-size. Baserow construye el volcado en /dev/shm, al que Docker asigna 64 MB por defecto. Recrea el contenedor con --shm-size=1g; los datos están en el volumen y no se pierde nada. Verifica con docker exec baserow df -h /dev/shm.
El arranque tarda muchísimo y el log habla de plantillas.
Es la sincronización de las ~157 plantillas de ejemplo. Añade -e BASEROW_TRIGGER_SYNC_TEMPLATES_AFTER_MIGRATION=false al recrear el contenedor.
¿Puedo usar el mismo alojamiento para servir además contenido PHP?
Técnicamente sí, con un Path distinto de / y usando Bypass folders, pero no es recomendable: mezcla dos cosas con ciclos de vida y superficies de ataque distintas. Es preferible un alojamiento por servicio.
¿Sirve esto para otros servicios?
Sí. Grafana, n8n, Metabase, Gitea, Uptime Kuma y en general cualquier servicio que hable HTTP encajan en este mismo patrón. Lo único que cambia es el puerto, las variables de entorno del contenedor y si necesita o no WebSockets.
¿Y si prefiero no instalar Docker en el servidor?
Usa el módulo LXC Management para levantar un contenedor de sistema con el servicio dentro, y publícalo con el mismo ProxyPass apuntando a la IP interna del contenedor.
Resumen
-
docker volume createydocker runcon-p 127.0.0.1:<puerto>:80y--shm-size=1g -
--set-php-engine off,redirect_http_to_https 1yproxy_timeout 600sobre el alojamiento - ProxyPass
/→http://127.0.0.1:<puerto>con Preserve host header -
/ws/yX-Forwarded-Protoencustom_apache2_site_definitions_before - Crear la cuenta de administrador y cerrar el registro público
- Script de volcado lógico diario a
/var/backups/, programado antes de la copia externa
Los dos puntos que más problemas dan y que no son evidentes: Preserve host header (sin él, 404 en toda la aplicación) y --shm-size=1g (sin él, el backup falla justo cuando hay datos que perder).


