Saltar al contenido principal

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​

VariableDescripción
APP_URLLa 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_URLCadena 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_SECRETAl 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_KEYAl 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​

VariableValor por defectoDescripción
LOG_LEVELinfotrace, 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_MIGRATIONStrueAplica las migraciones de la base de datos al arrancar. Pon false solo si las ejecutas de otra forma.
TRUST_PROXY_HOPS1Cuá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_URLhttps://sol2flow.comA 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:

VariablePredeterminadoDescripción
DATABASE_POOL_SIZE10; 5 con ROLE=workerConexiones 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_SIZE5; 2 con ROLE=web, que solo encola trabajosConexiones 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ónConexiones como máximo
Un solo contenedor (sin ROLE), el compose.yml predeterminado10 + 5 = 15
Un contenedor con ROLE=web10 + 2 = 12 cada uno
Un contenedor con ROLE=worker5 + 5 = 10 cada uno
Migraciones al arrancar, herramientas de línea de comandos, copias de seguridad, psql1–2 cada una mientras se ejecuta
Una actualización gradual que arranca primero el contenedor nuevolos 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.

VariablePor defectoDescripció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:

  1. 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).
  2. Para la ciudad, ve a Rules → Settings, busca Managed Transforms y activa Add visitor location headers. Eso añade, entre otras, cf-ipcity y cf-region.
  3. Define GEO_HEADERS=cloudflare en .env y aplícalo con docker compose up -d.
aviso

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 geoipupdate de MaxMind, por ejemplo como servicio en tu compose.yml:

    services:
    geoipupdate:
    image: ghcr.io/maxmind/geoipupdate
    restart: unless-stopped
    environment:
    GEOIPUPDATE_ACCOUNT_ID: '123456'
    GEOIPUPDATE_LICENSE_KEY: '<tu clave de licencia>'
    GEOIPUPDATE_EDITION_IDS: GeoLite2-City
    GEOIPUPDATE_FREQUENCY: '72' # horas
    volumes: ['./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:

VariableValor por defectoDescripció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_FREQUENCYdailyUn 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_FILES10Cuá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.log apunta siempre al archivo actual (un enlace simbólico), así que tail -F /data/logs/app.log sigue el log de un archivo al siguiente. Usa -F, no -f: tail -f se queda en el archivo que abrió primero. Con LOG_FILE_FREQUENCY=none y sin límite de tamaño, app.log es el propio archivo; con límite, los archivos llenos pasan a app.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.log para la app y /data/logs/worker.log para 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:

VariableValor por defectoDescripció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_ENVIRONMENTproductionEl entorno en el que se archivan los errores, p. ej. staging.
SENTRY_RELEASEla versión de la imagenLa versión a la que pertenecen los errores; por defecto, la versión de la imagen, p. ej. 1.4.0.
SENTRY_TRACES_SAMPLE_RATE0Proporción de peticiones trazadas para medir el rendimiento, de 0 a 1. 0 = solo errores.
SENTRY_LOGSfalsetrue 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 SECRET_KEY="$(openssl rand -base64 50)" -e CREATE_SUPERUSER=[email protected]:<a password> \
-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​

VariableValor por defectoDescripción
STORAGE_DRIVERlocallocal (un directorio; monta un volumen) o s3 (cualquier almacenamiento de objetos compatible con S3).
STORAGE_LOCAL_DIR/data/uploadsDó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_ENDPOINTDirecció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_REGIONus-east-1La región del bucket (Cloudflare R2: auto).
S3_BUCKETsol2flowEl 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_STYLEtruetrue para https://host/bucket/key (la mayoría de almacenamientos autoalojados), false para https://bucket.host/key.
MAX_UPLOAD_MB10Obsoleta. 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​

VariableValor por defectoDescripción
MAIL_DRIVERlogsmtp para enviar correo; log escribe los mensajes en el log de la app en su lugar (solo para pruebas).
MAIL_FROMsol2flow <[email protected]>Nombre y dirección del remitente.
SMTP_HOSTlocalhostServidor SMTP.
SMTP_PORT1025Normalmente 587 (STARTTLS) o 465 (TLS).
SMTP_USER, SMTP_PASSWORD(vacío)Credenciales, si el servidor las necesita.
SMTP_SECUREfalsetrue 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.

VariableValor por defectoDescripción
BOOTSTRAP_ADMIN_EMAIL(vacío)Correo del primer miembro del staff, el Propietario. Opcional.
BOOTSTRAP_ADMIN_USERNAMEadminSu 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/.

VariableDesde un archivo
AUTH_SECRETAUTH_SECRET_FILE
APP_ENCRYPTION_KEYAPP_ENCRYPTION_KEY_FILE
DATABASE_URLDATABASE_URL_FILE
S3_ACCESS_KEY_IDS3_ACCESS_KEY_ID_FILE
S3_SECRET_ACCESS_KEYS3_SECRET_ACCESS_KEY_FILE
SMTP_USERSMTP_USER_FILE
SMTP_PASSWORDSMTP_PASSWORD_FILE
BOOTSTRAP_ADMIN_PASSWORDBOOTSTRAP_ADMIN_PASSWORD_FILE
SENTRY_DSNSENTRY_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 create y la mayoría de los editores. Los espacios al principio o al final forman parte del valor.
  • Define uno u otro. Si X y X_FILE está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 (modo 0444 por defecto); con un archivo que montas tú, revisa sus permisos.
  • DATABASE_URL_FILE contiene 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:

VariableValor por defectoDescripción
SOL2FLOW_VERSIONlatestLa etiqueta de la imagen que se ejecuta. Fija una versión en producción.
POSTGRES_PASSWORDsol2flowContraseñ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.

No para producción

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.