Referencia de configuración
sol2flow lee su configuración de variables de entorno; en la instalación con Docker Compose, del archivo .env que está junto a
compose.yml. La app las comprueba al arrancar y se niega a arrancar, con un mensaje claro, si falta una obligatoria o no es
válida. Cambia un valor y aplícalo con docker compose up -d.
La mayoría de los ajustes no son variables de entorno: los métodos de inicio de sesión, el registro libre, los límites de subida y los correos de notificación se configuran en la app, en Administración (Ajustes de la instancia).
Los secretos como AUTH_SECRET o DATABASE_URL también pueden venir de archivos, por ejemplo de secretos de Docker
(Secretos desde archivos).
Obligatorias
| Variable | Descripción |
|---|---|
APP_URL | La dirección pública de la instancia, p. ej. https://flow.example.com. Se usa para los enlaces de los correos, las redirecciones del inicio de sesión y las llaves de acceso. Debe coincidir exactamente con lo que escriben las personas. |
DATABASE_URL | Cadena de conexión de PostgreSQL, p. ej. postgresql://sol2flow:secret@db:5432/sol2flow?schema=public. PostgreSQL 16, 17 o 18. El compose.yml de ejemplo la construye a partir de POSTGRES_PASSWORD. |
AUTH_SECRET | Al menos 16 caracteres; usa 32 bytes aleatorios: openssl rand -base64 32. Es la clave de las firmas internas (por ejemplo, de los contadores anonimizados de límites de frecuencia). |
APP_ENCRYPTION_KEY | Al menos 32 caracteres; usa openssl rand -base64 32. Cifra los secretos guardados en la base de datos. No la pierdas ni la cambies a la ligera: consulta rotar la clave de cifrado. |
General
| Variable | Valor por defecto | Descripción |
|---|---|---|
LOG_LEVEL | info | trace, debug, info, warn, error o fatal. Los logs son líneas JSON en la salida estándar, y en un archivo con LOG_FILE (abajo). |
RUN_MIGRATIONS | true | Aplica las migraciones de la base de datos al arrancar. Pon false solo si las ejecutas de otra forma. |
TRUST_PROXY_HOPS | 1 | Cuántos proxies inversos delante de la app añaden entradas a X-Forwarded-For (1–10). Decide qué dirección cuenta como la del cliente. Consulta Proxy inverso y TLS. |
PUBLIC_WEBSITE_URL | https://sol2flow.com | A dónde lleva el pie «Compartido con sol2flow» de un enlace público de tablero (http o https; se abre en una pestaña nueva, sin referente). Pon la dirección de tu propio sitio, o déjalo vacío para mostrar el texto sin enlace. |
ROLE | (vacío) | web = no ejecutar trabajos en segundo plano en este contenedor; se usa con un contenedor worker aparte (Instalación). |
Conexiones a la base de datos
Cada contenedor de la aplicación mantiene dos pools de conexiones a PostgreSQL: uno para las páginas, las acciones y la API, y otro más pequeño para la cola de trabajos en segundo plano. Su tamaño es por contenedor:
| Variable | Predeterminado | Descripción |
|---|---|---|
DATABASE_POOL_SIZE | 10; 5 con ROLE=worker | Conexiones para las páginas, las acciones, la API y los trabajos. Un connection_limit en DATABASE_URL (o en el secreto DATABASE_URL_FILE) tiene prioridad. |
DATABASE_JOBS_POOL_SIZE | 5; 2 con ROLE=web, que solo encola trabajos | Conexiones para la cola de trabajos. |
Cuando un pool está lleno, una petición espera a que quede una conexión libre (hasta 10 segundos) en lugar de abrir
otra. En la base de datos, las conexiones aparecen como sol2flow-web, sol2flow-worker o sol2flow-all (un
contenedor sin ROLE), y las de la cola de trabajos con -jobs al final. Para ver quién tiene cuántas:
SELECT application_name, state, count(*) FROM pg_stat_activity WHERE datname = 'sol2flow' GROUP BY 1, 2;
Todo lo que se conecta cuenta contra el max_connections del servidor de base de datos: 100 por defecto, de las que 3
están reservadas para superusuarios; las demás bases de datos del mismo servidor también cuentan.
| Configuración | Conexiones como máximo |
|---|---|
Un solo contenedor (sin ROLE), el compose.yml predeterminado | 10 + 5 = 15 |
Un contenedor con ROLE=web | 10 + 2 = 12 cada uno |
Un contenedor con ROLE=worker | 5 + 5 = 10 cada uno |
Migraciones al arrancar, herramientas de línea de comandos, copias de seguridad, psql | 1–2 cada una mientras se ejecuta |
| Una actualización gradual que arranca primero el contenedor nuevo | los contenedores actualizados por duplicado, hasta que paran los antiguos |
Web y worker en un mismo servidor usan 22 conexiones, 44 como máximo mientras se sustituyen ambos, muy por debajo del
límite predeterminado. Más contenedores o pools más grandes se suman: mantenga el total, duplicado si sus actualizaciones
arrancan los contenedores nuevos antes de parar los antiguos, por debajo de max_connections menos 3. Si el log
muestra sorry, too many clients already, reduzca el tamaño de los pools o aumente max_connections en PostgreSQL.
Ubicación del inicio de sesión
sol2flow avisa de los inicios de sesión inusuales: un dispositivo nuevo, un país nuevo, o un inicio de sesión desde otro país poco después de otro en otro lugar. Para el país necesita la ubicación aproximada de cada inicio de sesión. Sin una fuente de ubicación, los avisos funcionan igualmente, pero comparan dispositivos y redes en lugar de países.
| Variable | Por defecto | Descripción |
|---|---|---|
GEO_HEADERS | (vacío) | cloudflare = leer la ubicación de las cabeceras de Cloudflare (CF-IPCountry, cf-ipcity, cf-region). Solo si todo el tráfico llega a la aplicación a través de Cloudflare. |
GEOIP_DB | (vacío) | Ruta dentro del contenedor a un archivo MaxMind GeoLite2 City o Country, o DB-IP Lite (.mmdb), por ejemplo /data/geoip/GeoLite2-City.mmdb. |
Si se definen las dos, ganan las cabeceras de Cloudflare y el archivo sirve para las peticiones que no las traen. Solo se guardan el país y la ciudad, nunca la dirección IP: consulta qué se guarda.
Detrás de Cloudflare
Si tu instancia está detrás de Cloudflare — como proxy (nube naranja) o con un Cloudflare Tunnel —, Cloudflare puede añadir la ubicación a cada petición:
- En el panel de Cloudflare, abre tu dominio y comprueba que Network → IP Geolocation está activado (lo está por
defecto). Eso añade el país (
CF-IPCountry). - Para la ciudad, ve a Rules → Settings, busca Managed Transforms y activa Add visitor location headers.
Eso añade, entre otras,
cf-ipcityycf-region. - Define
GEO_HEADERS=cloudflareen.envy aplícalo condocker compose up -d.
Define GEO_HEADERS=cloudflare solo si no se puede llegar a la aplicación por otro camino. Cualquiera puede enviar una
cabecera CF-IPCountry; solo Cloudflare delante de la aplicación garantiza que es de Cloudflare. Con un Tunnel, no
publiques el puerto de la aplicación en el host; con un registro DNS con proxy, deja pasar por tu cortafuegos solo
las direcciones de Cloudflare. Sin la variable, sol2flow ignora estas cabeceras.
Un Cloudflare Tunnel también define X-Forwarded-For; si no hay nada más delante de la aplicación, deja
TRUST_PROXY_HOPS=1.
Con una base de datos GeoIP
Si no estás detrás de Cloudflare, descarga una base de datos GeoIP gratuita y apunta GEOIP_DB a ella. La consulta se
hace dentro de la aplicación: ninguna dirección se envía a ningún sitio. sol2flow no incluye ninguna base de datos por
sus licencias:
-
MaxMind GeoLite2 (City o Country): crea una cuenta gratuita de MaxMind, acepta la licencia de GeoLite2 (EULA) y crea una clave de licencia. La licencia pide mantener los datos actualizados y citar a MaxMind donde los muestres. Lo más sencillo para mantenerlos al día es
geoipupdatede MaxMind, por ejemplo como servicio en tucompose.yml:services:geoipupdate:image: ghcr.io/maxmind/geoipupdaterestart: unless-stoppedenvironment:GEOIPUPDATE_ACCOUNT_ID: '123456'GEOIPUPDATE_LICENSE_KEY: '<tu clave de licencia>'GEOIPUPDATE_EDITION_IDS: GeoLite2-CityGEOIPUPDATE_FREQUENCY: '72' # horasvolumes: ['./geoip:/usr/share/GeoIP'] -
DB-IP Lite («IP to City Lite», formato MMDB): descárgala cada mes de db-ip.com. Tiene licencia CC BY 4.0, que pide atribuir a DB-IP.
Después monta la carpeta en la aplicación y define la ruta:
services:
app:
volumes: ['./geoip:/data/geoip:ro']
GEOIP_DB=/data/geoip/GeoLite2-City.mmdb
La aplicación lee el archivo en el primer inicio de sesión y detecta uno más reciente en menos de 10 minutos, sin reiniciar. Si el archivo falta o no se puede leer, escribe un aviso en el log y sigue sin ubicaciones.
Qué se guarda
Para cada persona, sol2flow recuerda los dispositivos y lugares desde los que inició sesión: la familia de navegador y
sistema operativo (por ejemplo «Chrome», «macOS»), el país y la ciudad, y un hash con clave de la red (las tres primeras
partes de una dirección IPv4 o los tres primeros grupos de una IPv6). Ninguna dirección IP ni la identificación completa
del navegador. Las entradas sin usar durante 180 días se eliminan automáticamente, y todas se van con la cuenta. Los
avisos muestran la dirección IP en parte oculta (203.0.x.x).
Logs y seguimiento de errores
La app escribe líneas JSON en la salida estándar (docker compose logs app). Para guardarlas también en archivos,
define LOG_FILE: la app escribe junto a esa ruta archivos con la fecha en el nombre y empieza otros nuevos sola:
| Variable | Valor por defecto | Descripción |
|---|---|---|
LOG_FILE | (vacío) | Dónde van los archivos de log y cómo se llaman, p. ej. /data/logs/app.log; la salida estándar sigue recibiendo las líneas. Vacío = solo la salida estándar. |
LOG_FILE_FREQUENCY | daily | Un archivo nuevo cada día (daily, app-2026-09-30.log) o cada hora (hourly, app-2026-09-30-14.log), en UTC; none = sin fecha en el nombre (app.log). |
LOG_FILE_MAX_SIZE | (vacío) | Vacío = sin límite de tamaño. Con un tamaño como 20m (k, m o g), un archivo lleno continúa en app-2026-09-30.1.log, luego .2.log, etc. |
LOG_FILE_MAX_FILES | 10 | Cuántos archivos de log de este nombre se guardan, los más recientes; los más antiguos se borran. Por días y sin límite de tamaño, son diez días. 0 = guardarlos todos. |
Con LOG_FILE=/data/logs/app.log, el directorio contiene por ejemplo:
app-2026-09-29.log
app-2026-09-30.log
app-2026-09-30.1.log # solo con LOG_FILE_MAX_SIZE: el segundo archivo del día
app.log -> app-2026-09-30.1.log
- Los días son días UTC: el archivo nuevo empieza a medianoche UTC, no a medianoche local.
app.logapunta siempre al archivo actual (un enlace simbólico), así quetail -F /data/logs/app.logsigue el log de un archivo al siguiente. Usa-F, no-f:tail -fse queda en el archivo que abrió primero. ConLOG_FILE_FREQUENCY=noney sin límite de tamaño,app.loges el propio archivo; con límite, los archivos llenos pasan aapp.1.log,app.2.log… (mayor = más reciente).- Un reinicio continúa el archivo del día (el más reciente del día); no se sobrescribe nada. Solo se borran archivos
de este nombre: los del nombrado anterior (
app.log.1…) y los demás archivos del directorio se quedan. - Un nombre de archivo por contenedor. Dos procesos nunca deben escribir el mismo
LOG_FILE: si un worker aparte comparte el directorio de logs, usa p. ej./data/logs/web.logpara la app y/data/logs/worker.logpara el worker.
La imagen tiene un directorio /data/logs para ello: monta ahí un volumen, como en el ejemplo.
Si no se pueden escribir los archivos (una ruta incorrecta, falta un permiso, el disco lleno), la app registra un aviso
y sigue escribiendo en la salida estándar; el log en archivos nunca la detiene.
El seguimiento de errores envía los errores a Sentry o a un servicio que hable su protocolo, como Bugsink o GlitchTip autoalojados. Está desactivado hasta que defines un DSN:
| Variable | Valor por defecto | Descripción |
|---|---|---|
SENTRY_DSN | (vacío) | El DSN de tu proyecto (https://<key>@<host>/<project>). Vacío = desactivado: no se carga ni se envía nada. Desde un archivo: SENTRY_DSN_FILE. |
SENTRY_ENVIRONMENT | production | El entorno en el que se archivan los errores, p. ej. staging. |
SENTRY_RELEASE | la versión de la imagen | La versión a la que pertenecen los errores; por defecto, la versión de la imagen, p. ej. 1.4.0. |
SENTRY_TRACES_SAMPLE_RATE | 0 | Proporción de peticiones trazadas para medir el rendimiento, de 0 a 1. 0 = solo errores. |
SENTRY_LOGS | false | true envía además las entradas del log de nivel warn o superior a los Logs de Sentry. Bugsink y GlitchTip no tienen logs: ahí no tiene efecto. |
Qué se envía: los errores en el servidor (peticiones fallidas, errores que la app registra, trabajos en segundo plano fallidos, etiquetados con el nombre del trabajo) y en el navegador (errores en las páginas, incluidos los que hay detrás de la página «Algo salió mal»). Qué no: ninguna grabación de sesión, ningún nombre, dirección de correo ni dirección IP, ninguna cookie, cuerpo de petición ni parámetro de consulta; los tokens de las direcciones (enlaces públicos de tablero, invitaciones, restablecimientos de contraseña) se eliminan. El navegador obtiene el DSN de la página, así que un cambio solo necesita reiniciar la app.
Por ejemplo, con Bugsink: ejecútalo junto a sol2flow detrás de tu proxy inverso (aquí en errors.example.com), inicia
sesión, crea un equipo y un proyecto y copia el DSN del proyecto en .env:
docker run -d --name bugsink --restart unless-stopped -p 127.0.0.1:8000:8000 -v bugsink:/data \
-e BASE_URL=https://errors.example.com -e BEHIND_HTTPS_PROXY=true bugsink/bugsink
SENTRY_DSN=https://<key>@errors.example.com/1
Los navegadores también envían sus errores a la dirección del DSN, así que debe ser accesible desde fuera, con HTTPS.
Almacenamiento
| Variable | Valor por defecto | Descripción |
|---|---|---|
STORAGE_DRIVER | local | local (un directorio; monta un volumen) o s3 (cualquier almacenamiento de objetos compatible con S3). |
STORAGE_LOCAL_DIR | /data/uploads | Dónde se guardan los archivos con el driver local. |
S3_ENDPOINT | (vacío) | Dirección de la API que usa el servidor, p. ej. https://s3.eu-central-1.amazonaws.com o https://<account>.r2.cloudflarestorage.com. Vacío = AWS para S3_REGION. |
S3_PUBLIC_ENDPOINT | = S3_ENDPOINT | Dirección de la API que usan los navegadores para los enlaces de descarga, si es distinta de la del servidor (nombre de host interno, red privada). |
S3_REGION | us-east-1 | La región del bucket (Cloudflare R2: auto). |
S3_BUCKET | sol2flow | El bucket; créalo antes. |
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY | (vacío) | Credenciales. Vacío = la cadena por defecto del SDK (por ejemplo, un rol de IAM). |
S3_FORCE_PATH_STYLE | true | true para https://host/bucket/key (la mayoría de almacenamientos autoalojados), false para https://bucket.host/key. |
MAX_UPLOAD_MB | 10 | Obsoleta. Los límites de subida se definen en Administración → General → Subidas; esta solo dimensiona las peticiones internas de formularios. |
Detalles y ejemplos: Almacenamiento.
Correo
| Variable | Valor por defecto | Descripción |
|---|---|---|
MAIL_DRIVER | log | smtp para enviar correo; log escribe los mensajes en el log de la app en su lugar (solo para pruebas). |
MAIL_FROM | sol2flow <[email protected]> | Nombre y dirección del remitente. |
SMTP_HOST | localhost | Servidor SMTP. |
SMTP_PORT | 1025 | Normalmente 587 (STARTTLS) o 465 (TLS). |
SMTP_USER, SMTP_PASSWORD | (vacío) | Credenciales, si el servidor las necesita. |
SMTP_SECURE | false | true para TLS implícito (puerto 465); false para STARTTLS o sin cifrar. |
Detalles: Correo.
Primer administrador
Solo se usan en un arranque mientras la instancia no tiene usuarios; después se ignoran.
| Variable | Valor por defecto | Descripción |
|---|---|---|
BOOTSTRAP_ADMIN_EMAIL | (vacío) | Correo del primer miembro del staff, el Propietario. Opcional. |
BOOTSTRAP_ADMIN_USERNAME | admin | Su nombre de usuario. |
BOOTSTRAP_ADMIN_PASSWORD | (vacío) | Su contraseña. La cuenta solo se crea si está definida; déjala vacía para crear el Propietario desde la línea de comandos (Instalación). |
Secretos desde archivos (secretos de Docker)
Cada secreto también se puede leer de un archivo en lugar del entorno, como hace la imagen oficial de PostgreSQL:
define X_FILE con la ruta de un archivo que contiene el valor de X. Así llegan los secretos de Docker Swarm y de
Compose a un contenedor: como archivos en /run/secrets/.
| Variable | Desde un archivo |
|---|---|
AUTH_SECRET | AUTH_SECRET_FILE |
APP_ENCRYPTION_KEY | APP_ENCRYPTION_KEY_FILE |
DATABASE_URL | DATABASE_URL_FILE |
S3_ACCESS_KEY_ID | S3_ACCESS_KEY_ID_FILE |
S3_SECRET_ACCESS_KEY | S3_SECRET_ACCESS_KEY_FILE |
SMTP_USER | SMTP_USER_FILE |
SMTP_PASSWORD | SMTP_PASSWORD_FILE |
BOOTSTRAP_ADMIN_PASSWORD | BOOTSTRAP_ADMIN_PASSWORD_FILE |
SENTRY_DSN | SENTRY_DSN_FILE |
Las reglas:
- Todo el archivo es el valor, salvo un salto de línea final (LF o CRLF), que añaden
docker secret createy la mayoría de los editores. Los espacios al principio o al final forman parte del valor. - Define uno u otro. Si
XyX_FILEestán definidas a la vez, la app no arranca y nombra la variable. Tampoco arranca si el archivo no existe o no se puede leer. Una variable vacía cuenta como no definida. - Los valores nunca aparecen en el registro, solo los nombres de las variables y las rutas.
- El usuario del contenedor, que no es
root(uid 1001), debe poder leer el archivo. Los secretos de Swarm lo permiten (modo0444por defecto); con un archivo que montas tú, revisa sus permisos. DATABASE_URL_FILEcontiene la cadena de conexión completa, no solo la contraseña. Una contraseña con caracteres especiales (@,:,/,%, …) debe ir codificada para URL; o usa una contraseña hexadecimal como abajo.
Ejemplo para Docker Swarm, en el que PostgreSQL también lee su contraseña de un secreto. Crea los secretos una vez:
DB_PASSWORD=$(openssl rand -hex 16)
printf '%s' "$DB_PASSWORD" | docker secret create sol2flow_db_password -
printf 'postgresql://sol2flow:%s@db:5432/sol2flow?schema=public' "$DB_PASSWORD" | docker secret create sol2flow_database_url -
unset DB_PASSWORD
openssl rand -base64 32 | docker secret create sol2flow_auth_secret -
openssl rand -base64 32 | docker secret create sol2flow_app_encryption_key -
openssl rand -hex 12 | tee /dev/stderr | docker secret create sol2flow_admin_password - # muestra la contraseña una vez
Guarda una copia de la clave de cifrado en un lugar seguro fuera del swarm: un secreto no se puede volver a leer
(por qué importa). Después, un archivo de stack, stack.yml:
services:
app:
image: ghcr.io/sol2flow/sol2flow:latest # fija una versión en producción
environment:
APP_URL: https://flow.example.com
AUTH_SECRET_FILE: /run/secrets/sol2flow_auth_secret
APP_ENCRYPTION_KEY_FILE: /run/secrets/sol2flow_app_encryption_key
DATABASE_URL_FILE: /run/secrets/sol2flow_database_url
BOOTSTRAP_ADMIN_PASSWORD_FILE: /run/secrets/sol2flow_admin_password
LOG_FILE: /data/logs/app.log # opcional: archivos app-AAAA-MM-DD.log, app.log → el actual
secrets: [sol2flow_auth_secret, sol2flow_app_encryption_key, sol2flow_database_url, sol2flow_admin_password]
ports: ['3000:3000']
volumes: [uploads:/data/uploads, logs:/data/logs]
db:
image: postgres:18-alpine
environment:
POSTGRES_USER: sol2flow
POSTGRES_DB: sol2flow
POSTGRES_PASSWORD_FILE: /run/secrets/sol2flow_db_password
secrets: [sol2flow_db_password]
volumes: [pgdata:/var/lib/postgresql] # from PostgreSQL 18 not …/data (data: …/18/docker)
secrets:
sol2flow_auth_secret: { external: true }
sol2flow_app_encryption_key: { external: true }
sol2flow_database_url: { external: true }
sol2flow_db_password: { external: true }
sol2flow_admin_password: { external: true }
volumes: { pgdata: {}, uploads: {}, logs: {} }
Despliégalo con docker stack deploy -c stack.yml sol2flow e inicia sesión como admin con la contraseña mostrada.
Docker Compose solo funciona igual con secretos basados en archivos
(secrets: { sol2flow_auth_secret: { file: ./auth_secret } }).
Solo para el archivo Compose
Estas las lee el compose.yml de ejemplo, no la app:
| Variable | Valor por defecto | Descripción |
|---|---|---|
SOL2FLOW_VERSION | latest | La etiqueta de la imagen que se ejecuta. Fija una versión en producción. |
POSTGRES_PASSWORD | sol2flow | Contraseña del PostgreSQL incluido. Pon una aleatoria. |
SOL2FLOW_VERSION de .env también llega al contenedor, donde es la versión que informa el seguimiento de errores. Fija
una versión completa como 1.4.0 en lugar de latest.
La app también reconoce algunos ajustes que solo existen para desarrollo y pruebas automatizadas. No pongas en un .env de producción nada
que no esté en esta página.