Saltar a contenido

Instalación on-premise

Requisitos previos

Requisitos del servidor:

  • Servidor Linux: Ubuntu 26.04 o 24.04 LTS, Debian 12 o 13, RHEL 9 o 10, o cualquier sistema operativo con Docker Engine 24.0+ y el plugin Docker Compose
  • Docker Engine 24.0+
  • Plugin Docker Compose 2.20 o posterior (las versiones actuales son 5.x)
  • Git
  • 4 GB de RAM como mínimo, 8 GB recomendados cuando PostgreSQL se ejecuta en el mismo servidor. Más procesos de API necesitan más memoria, consulte Configuración.
  • 10 GB de disco como mínimo. Kadratis ocupa unos 1,5 GB (sus imágenes), 2 GB con la imagen de herramientas de mantenimiento. La base de datos y los archivos almacenados crecen con el tiempo.
  • Acceso HTTPS saliente a ghcr.io en la instalación y en cada actualización, para descargar las imágenes (consulte Reglas de cortafuegos).

Infraestructura aportada por el cliente:

Componente Requisito
PostgreSQL Versión 16+ con las extensiones citext, pgcrypto, uuid-ossp, y un rol de aplicación dedicado para DATABASE_URL
Almacenamiento S3 Cualquier almacenamiento compatible con S3 con un bucket: AWS S3, Cloudflare R2, Hetzner Object Storage, Garage, RustFS, un MinIO existente y otros. Kadratis necesita PutObject, GetObject, HeadObject, DeleteObject, ListObjectsV2 y GET prefirmado en ese bucket.
Proxy inverso Terminación TLS y enrutamiento (nginx, Traefik, Caddy, etc.)
Nombre y certificado Un nombre que resuelvan los usuarios y el servidor, y un certificado para ese nombre (ver más abajo)

Un MinIO que ya funciona sigue funcionando con Kadratis: no hay nada que cambiar. MinIO ya no publica nuevas descargas ni imágenes, por eso el ejemplo de instalación usa RustFS.

Opcional:

  • Configuración del correo saliente: clave API de Resend o datos de un relay o servidor SMTP
  • SSO con Microsoft Entra (consulte SSO con Microsoft Entra)
  • Un agente de programación con IA que realice la instalación por usted (consulte Instalación asistida por IA)

Nombre y certificado

Los usuarios abren Kadratis en una dirección HTTPS, por ejemplo https://kadratis.company.com. Esa dirección necesita un nombre que resuelva a su servidor y un certificado que corresponda a ese nombre. Tres casos cubren la mayoría de las redes:

Caso Nombre Certificado
Nombre público Un registro en el DNS público que apunta al servidor (o al cortafuegos situado delante) De una autoridad pública, por ejemplo Let's Encrypt con certbot. El puerto 80 debe ser accesible desde internet.
Nombre interno Un registro en el DNS de su empresa. Para una prueba, una línea en /etc/hosts en el servidor y en cada cliente. De la autoridad de certificación interna de su empresa. Los navegadores de los puestos gestionados ya confían en ella, así que los usuarios no ven ninguna advertencia.
Autofirmado Igual que el nombre interno Creado en el servidor. Solo para pruebas: todos los navegadores muestran una advertencia que cada usuario debe aceptar.

Muchas instalaciones no tienen DNS público. El nombre interno con un certificado interno es una configuración normal y admitida. El certificado del proxy inverso sirve a los navegadores. La API también abre sus propias conexiones, hacia SMTP, PostgreSQL o S3: si su autoridad firmó los certificados de esos servidores, consulte Certificados de una autoridad interna.

El servidor también debe resolver el nombre. Los comandos de verificación de esta guía, y la prueba de humo, se ejecutan en el servidor y llaman a Kadratis por su nombre. Si todavía no existe ningún registro DNS, añada el nombre al archivo hosts del servidor:

echo "127.0.0.1 kadratis.company.com" | sudo tee -a /etc/hosts

Sustituya kadratis.company.com por su nombre. Los clientes necesitan su propia entrada (o el registro DNS) que apunte a la dirección del servidor.

Certificado autofirmado (solo para pruebas):

sudo install -d /etc/ssl/kanap
sudo openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 \
  -keyout /etc/ssl/kanap/server.key \
  -out /etc/ssl/kanap/server.crt \
  -subj "/CN=kadratis.company.com" \
  -addext "subjectAltName=DNS:kadratis.company.com"
sudo chmod 600 /etc/ssl/kanap/server.key

Sustituya kadratis.company.com por su nombre. Para un acceso por dirección IP, use IP:192.0.2.10 en subjectAltName y ponga la IP en -subj y en server_name. Para usar su autoridad interna, pida un certificado para el mismo nombre y haga que ssl_certificate y ssl_certificate_key del archivo de nginx apunten a los archivos que le entregue (la cadena completa y la clave privada). Nada más cambia.

Let's Encrypt: sudo apt-get install -y certbot, después sudo certbot certonly --webroot -w /var/www/html -d kadratis.company.com, con la página predeterminada de nginx respondiendo en el puerto 80. Los archivos del certificado son /etc/letsencrypt/live/kadratis.company.com/fullchain.pem y privkey.pem. El ejemplo de instalación muestra la secuencia completa, renovación incluida.

Inicio rápido

# 1. Obtener Kadratis. La rama "stable" siempre apunta a la última versión publicada.
sudo install -d -o "$USER" -g "$USER" /opt/kanap
git clone https://github.com/kanap-hq/KANAP.git /opt/kanap
cd /opt/kanap
git checkout stable

# 2. Configurar antes del primer arranque
cp infra/.env.onprem.example .env
chmod 600 .env
nano .env  # Definir DATABASE_URL, las credenciales S3, ADMIN_EMAIL, ADMIN_PASSWORD, JWT_SECRET,
#          DEFAULT_TENANT_NAME (el nombre de su organización, leído solo en el primer arranque),
#          APP_BASE_URL y CORS_ORIGINS (la dirección exacta que abren los usuarios),
#          APP_ENV=production (los usuarios acceden a Kadratis por HTTPS) y RATE_LIMIT_TRUST_PROXY=true
# Todas las variables están en la guía de Configuración

# 3. Descargar las imágenes de esta versión
docker compose -f infra/compose.onprem.yml pull

# 4. Arrancar los contenedores
docker compose -f infra/compose.onprem.yml up -d

# 5. Verificar el arranque
docker compose -f infra/compose.onprem.yml logs -f api
# Esperar "[entrypoint] Migrations complete" y después "Nest application successfully started"
# El primer arranque crea automáticamente el espacio de trabajo, el usuario administrador y la suscripción
# Pulsar Ctrl+C para dejar de seguir el registro

# 6. Configurar el proxy inverso para enrutar el tráfico hacia:
#    - /api/* → 127.0.0.1:8080 (el contenedor api)
#    - /.well-known/oauth-* → 127.0.0.1:8080, path kept (MCP connections without a key)
#    - /*     → 127.0.0.1:8081 (el contenedor web, puerto 80 dentro del contenedor)
# Comprobar que el proxy conserva Host y define X-Forwarded-Proto y X-Forwarded-For.
# Tras el primer arranque, leer las líneas [ENV], [CONFIG], [CORS], [RATE-LIMIT] y [SECURITY] del registro de la API.

# 7. Acceder a la aplicación
# https://kadratis.company.com
# Iniciar sesión con ADMIN_EMAIL / ADMIN_PASSWORD de .env

Importante: complete la configuración (paso 2) antes de arrancar los contenedores. La API lee .env al arrancar y crea el espacio de trabajo y el usuario administrador en el primer arranque con esos valores. El archivo contiene todos los secretos de la instalación: chmod 600 hace que solo su propietario pueda leerlo.

Versiones. Kadratis publica una nueva versión aproximadamente una vez al mes (26.10.1 es la primera). La rama stable siempre apunta a la última versión publicada. Cada versión incluye sus imágenes en GitHub Container Registry (ghcr.io/kanap-hq/kadratis-api, kadratis-web y kadratis-api-tools), compiladas a partir de la etiqueta de la versión. Son públicas: no hace falta ninguna cuenta. A partir de esta versión, las imágenes se llaman kadratis-*. La actualización habitual las descarga: no hay nada más que hacer. Las versiones anteriores conservan sus imágenes kanap-*. infra/compose.onprem.yml nombra las imágenes de la versión que tiene en el checkout, así que git checkout stable o git checkout v26.10.9 decide la versión que descarga pull. Las versiones hasta la 26.10.7 no tienen imágenes publicadas: se compilan en el servidor, como explica la guía de esa versión. Consulte Operaciones para actualizar, fijar una versión concreta y volver atrás. La rama main contiene todos los cambios integrados antes de que se publiquen como versión. Es posible seguirla, aunque no se recomienda para un servidor de producción: implica compilar las imágenes usted mismo.

Requisito del rol de base de datos: DATABASE_URL debe usar un rol de aplicación de PostgreSQL dedicado. No lo apunte a postgres ni a otro rol de administración del clúster. Kadratis se niega a arrancar si no puede aplicar RLS de forma efectiva.

Dirección y orígenes: defina APP_BASE_URL y CORS_ORIGINS con la dirección exacta que abren los usuarios, con el puerto cuando no es el estándar. Todos los enlaces que envía Kadratis proceden de APP_BASE_URL. Defina APP_ENV=production cuando los usuarios accedan a Kadratis por HTTPS: la API se niega entonces a arrancar sin estos dos valores y marca siempre la cookie de sesión como Secure. Consulte Configuración.

Elección del correo: los despliegues on-premise pueden usar Resend o SMTP para el correo saliente. SMTP es útil cuando el cliente ya tiene un relay de correo interno o un proveedor gestionado como Microsoft 365. Configure una de estas opciones si desea que el restablecimiento de contraseña, las invitaciones y los correos de notificación funcionen desde el primer día.

Compilar las imágenes usted mismo

Las imágenes publicadas sirven para la mayoría de las instalaciones. Compílelas en el servidor para un fork, una corrección local, un servidor que no alcanza ghcr.io o una API en otro host. El archivo infra/compose.onprem.build.yml, añadido sobre el archivo principal, las compila a partir del checkout y las nombra kadratis-api:local, kadratis-web:local y kadratis-api-tools:local:

docker compose -f infra/compose.onprem.yml -f infra/compose.onprem.build.yml build
docker compose -f infra/compose.onprem.yml -f infra/compose.onprem.build.yml up -d

Indique los dos archivos en cada up y run posterior: con el archivo principal solo, Compose arranca las imágenes publicadas. La compilación necesita el paquete docker-buildx-plugin y 6 GB de RAM en el servidor. Descarga paquetes npm y Alpine (consulte Reglas de cortafuegos). Para una API en otro host, pase su dirección en el shell al compilar la imagen web: VITE_API_URL=https://api.company.com docker compose -f infra/compose.onprem.yml -f infra/compose.onprem.build.yml build web. Compose no la lee de .env.

Ejemplo de proxy inverso (nginx)

Requisitos del proxy inverso:

  1. Terminar TLS en el puerto 443
  2. Enrutar /api/* directamente al contenedor de la API (puerto 8080 en 127.0.0.1), sin el prefijo /api. No envíe /api/ a través del contenedor web (puerto 8081): su propia ruta /api/ no transmite X-Forwarded-For, así que Kadratis contaría y registraría cada solicitud con la dirección del contenedor web.
  3. Enrutar todas las demás solicitudes al contenedor web (puerto 8081 en 127.0.0.1)
  4. Definir X-Forwarded-Proto: https y conservar Host. Kadratis construye todos los enlaces que envía a partir de APP_BASE_URL. El ejemplo también envía X-Forwarded-Host, con el mismo valor que Host. Cuando el sitio usa un puerto no estándar, añada a CORS_ORIGINS la dirección exacta con su puerto.
  5. Enviar X-Forwarded-For con la dirección del cliente (el ejemplo lo hace) y definir RATE_LIMIT_TRUST_PROXY=true. Kadratis usa esa dirección para sus límites de inicio de sesión. El puerto de la API debe seguir vinculado a 127.0.0.1, como hace compose.onprem.yml. Si no hay nada delante de la API, defina RATE_LIMIT_TRUST_PROXY=false.
  6. Aceptar cuerpos de 80 MB (client_max_body_size 80m): la carga de un archivo de presupuesto envía hasta 72 MB (el archivo, hasta 48 MB, y el resultado de su verificación) y los adjuntos llegan a 20 MB.
  7. Enrutar /.well-known/oauth-* hacia la API (puerto 8080), conservando la ruta. Los asistentes de IA como Claude la leen para conectarse mediante MCP sin clave. Las demás direcciones /.well-known/ siguen en el contenedor web. Consulte Conectar asistentes de IA mediante MCP.

Como los contenedores se vinculan a 127.0.0.1, nginx se ejecuta en el mismo host y hace de proxy hacia localhost.

El archivo está escrito para nginx 1.25.1 y posteriores (Ubuntu 26.04 incluye la 1.28). Ubuntu 24.04 incluye nginx 1.24: en ese caso, escriba listen 443 ssl http2; y listen [::]:443 ssl http2; y elimine la línea http2 on;.

server {
    # HTTP/2: el navegador envía las decenas de solicitudes de una página por una sola conexión.
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name kadratis.company.com;

    ssl_certificate     /path/to/fullchain.pem;
    ssl_certificate_key /path/to/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    # Límite de subida: cargas de archivos de presupuesto hasta 72 MB, adjuntos hasta 20 MB
    client_max_body_size 80m;

    # Normalizar /api → /api/
    location = /api { return 301 /api/; }

    # API: quitar el prefijo /api antes de reenviar
    location ^~ /api/ {
        proxy_pass http://127.0.0.1:8080/;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;

        # Comprimir las respuestas JSON y CSV de la API (una página de la lista de presupuesto se reduce unas 8 veces).
        # Las respuestas de IA en streaming (application/x-ndjson) quedan fuera a propósito.
        gzip on;
        gzip_proxied any;
        gzip_comp_level 5;
        gzip_min_length 1024;
        gzip_vary on;
        gzip_types application/json text/csv text/plain;

        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Solicitudes largas (exportaciones, importaciones)
        proxy_read_timeout  300s;
        proxy_send_timeout  300s;
        proxy_redirect off;
    }

    # MCP connections without a key: discovery served by the API, path kept
    # (no trailing slash on proxy_pass). Other /.well-known addresses stay below.
    location ^~ /.well-known/oauth- {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_redirect off;
    }

    # Todo lo demás → SPA
    location / {
        proxy_pass http://127.0.0.1:8081;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_redirect off;
    }
}

server {
    listen 80;
    listen [::]:80;
    server_name kadratis.company.com;

    # Renovación del certificado con Let's Encrypt (webroot); inofensivo en otro caso
    location /.well-known/acme-challenge/ { root /var/www/html; }

    location / { return 301 https://$host$request_uri; }
}

Compresión y HTTP/2: el ejemplo comprime las respuestas de la API y activa HTTP/2. Mantenga ambos en su propio proxy: una página de la lista de presupuesto ocupa unos 390 KB de JSON sin comprimir y 47 KB comprimida. Si su nginx tiene el módulo brotli (libnginx-mod-http-brotli-filter en Debian y Ubuntu), brotli on; brotli_types application/json text/csv text/plain; en el mismo location comprime un poco mejor; gzip es suficiente.

host.docker.internal: cuando PostgreSQL o el almacenamiento S3 se ejecutan en el host Docker (fuera de un contenedor), use host.docker.internal como nombre de host en DATABASE_URL y S3_ENDPOINT. El archivo compose.onprem.yml incluye la asignación extra_hosts que lo hace posible. Apunta a la dirección del puente Docker del servidor (172.17.0.1 por defecto), así que los servicios del host deben aceptar conexiones desde esa red (consulte el ejemplo de instalación).

Estado. La API responde a GET /health en su propio puerto (http://127.0.0.1:8080/health) y a GET /api/health a través del proxy. Ambos devuelven {"status":"ok","text_processing":"ready"}. Monitorización explica los demás valores de text_processing. docker compose -f infra/compose.onprem.yml ps muestra healthy para los contenedores api y web cuando responden.

Arquitectura de red

                    ┌─────────────────────────────────────────────────────┐
                    │              Customer Infrastructure                 │
                    │                                                      │
    Network         │  ┌──────────────┐    ┌─────────────────────────┐   │
        │           │  │ Your Reverse │    │     Docker Host         │   │
        │           │  │    Proxy     │    │                         │   │
   ┌────▼────┐      │  │   (TLS)      │    │  ┌─────┐    ┌─────┐    │   │
   │ Browser │──────┼─▶│   :443       │───▶│  │ api │    │ web │    │   │
   └─────────┘      │  └──────────────┘    │  │:8080│    │:8081│    │   │
                    │                      │  └─────┘    └─────┘    │   │
                    │  ┌──────────────┐    └─────────────────────────┘   │
                    │  │  PostgreSQL  │                                   │
                    │  │   (yours)    │◀──────── DATABASE_URL            │
                    │  └──────────────┘                                   │
                    │  ┌──────────────┐                                   │
                    │  │  S3 Storage  │◀──────── S3_ENDPOINT             │
                    │  │   (yours)    │                                   │
                    │  └──────────────┘                                   │
                    └─────────────────────────────────────────────────────┘

Modelo de despliegue: un contenedor de API y un contenedor web. No se admite ejecutar varios contenedores de API o web. Para más usuarios simultáneos, ejecute varios procesos de API dentro del contenedor de la API con API_WORKERS (consulte Configuración). Para la alta disponibilidad, confíe en las políticas de reinicio de Docker y en la redundancia de la infraestructura (alta disponibilidad de la base de datos, durabilidad de S3).

Primer inicio de sesión

  1. Abra https://<su-nombre>
  2. Inicie sesión con ADMIN_EMAIL y ADMIN_PASSWORD de .env
  3. Cambie la contraseña del administrador en su perfil si empezó con un valor que no desea conservar. La advertencia [SECURITY] del registro de la API desaparece cuando se cambia la contraseña (ver más abajo).
  4. Añada su logotipo y sus colores en Administración → Personalización (opcional)
  5. Invite a más usuarios (si el correo está configurado)

Sobre la cuenta de administrador. Kadratis la crea en el primer arranque a partir de ADMIN_EMAIL y ADMIN_PASSWORD, y solo entonces. Cambiar esas dos líneas más tarde no cambia nada mientras exista un administrador activo: cambie la contraseña en la aplicación. Si no queda ningún administrador activo, el siguiente arranque restablece la cuenta ADMIN_EMAIL como administrador habilitado y conserva su contraseña actual. ADMIN_PASSWORD debe ser un valor propio de 12 caracteres o más: openssl rand -base64 18 genera uno. Un valor de ejemplo o uno más corto hace que la API muestre una advertencia [SECURITY] en cada arranque hasta que se cambie la contraseña de la cuenta. Consulte Configuración.

Opcional: acceso a los datos por ámbito

El acceso a los datos limita a una persona a una parte del presupuesto y de los contratos, por empresa o por centro de coste. Sin este paso, todos trabajan con todos los datos, como en las versiones anteriores, y Limitado a aparece en gris en la ventana del usuario. Omítalo si nadie debe estar limitado.

El paso crea un rol de PostgreSQL, kanap_data_limited, que Kadratis adopta para las peticiones de las personas limitadas. Se hace una vez por servidor PostgreSQL, por un superusuario o un rol con CREATEROLE. Da al rol de aplicación el derecho de adoptar ese rol sin heredar sus derechos. Después, Kadratis establece por sí mismo los derechos y las reglas de acceso de ese rol en su base, en cada arranque y en cada actualización.

1. Ejecute el script. Está en el repositorio. Pase el rol de aplicación de DATABASE_URL en app_role. Con el ejemplo de instalación (rol y base kanap, PostgreSQL en el servidor):

cd /opt/kanap
sudo -u postgres psql -X -v ON_ERROR_STOP=1 -v app_role=kanap -v mode=install \
  -f /opt/kanap/infra/scripts/data-access-role.sql -d kanap

Con un servicio PostgreSQL gestionado, ejecute el mismo comando psql con la cuenta de administrador del servicio, desde una máquina que llegue a la base.

El script termina con una tabla data-access-role: final state. En PostgreSQL 16 y posteriores muestra limited_role_exists = t, app_is_member = t, membership_inherit = false, membership_set = true y app_inherits_limited = f. Volver a ejecutarlo no cambia nada.

En PostgreSQL 15, el script hace además que el rol de aplicación deje de heredar los derechos de otros roles, y debe ejecutarse en cada base que pertenece a ese rol. La cabecera del script lo explica y da el bucle que hay que usar.

2. Reinicie la API, en un momento tranquilo: al arrancar, establece los derechos y las reglas de acceso del rol.

cd /opt/kanap
docker compose -f infra/compose.onprem.yml restart api
docker compose -f infra/compose.onprem.yml logs api | grep DATA-ACCESS

El registro muestra [DATA-ACCESS] data-scoped access available. Si en su lugar lista lo que falta, lea las líneas anteriores. Si la API esperó demasiado una tabla ocupada, lo indica y lo vuelve a intentar en su siguiente arranque: reiníciela de nuevo un poco más tarde.

Retirar la función. Devuelva primero Todos los datos a las personas limitadas (Administración > Acceso a los datos las lista): mientras falte el rol, se rechazan. Después ejecute el script con mode=uninstall, como superusuario, y reinicie la API:

cd /opt/kanap
sudo -u postgres psql -X -v ON_ERROR_STOP=1 -v app_role=kanap -v mode=uninstall \
  -f /opt/kanap/infra/scripts/data-access-role.sql -d kanap
docker compose -f infra/compose.onprem.yml restart api

El script retira los derechos y las reglas de acceso del rol en la base, y después el propio rol. Cuando otras bases del servidor aún conservan algo para ese rol, las nombra: ejecútelo en cada una.

Una restauración de la base puede requerir repetir el paso: vea Restauración.