--- title: "Configuración avanzada de Directus 11 con Docker en Ubuntu" description: "Despliega Directus 11 con PostgreSQL y Redis en Docker, configura políticas de acceso, automatiza con Flows, crea una extensión hook y transforma imágenes." category: "Aplicaciones Web Extendidas" language: es source: https://cubepath.com/es/docs/aplicaciones-web-extendidas/configuracion-avanzada-de-directus-headless-cms updated: 2026-09-25 --- # Configuración avanzada de Directus 11 con Docker en Ubuntu Directus es un headless CMS de código abierto que se coloca encima de una base de datos SQL y expone su contenido mediante una API REST y GraphQL, con un panel de administración para editores. En este tutorial desplegarás Directus 11 con PostgreSQL y Redis mediante Docker Compose en Ubuntu 24.04, lo publicarás tras Nginx con HTTPS y configurarás las funciones que se usan en producción: permisos por políticas, automatizaciones con Flows, una extensión de tipo hook y transformación de imágenes. ## Requisitos previos - Un servidor con Ubuntu 24.04 LTS y al menos 2 GB de RAM, por ejemplo un VPS de CubePath. - Un usuario no root con privilegios `sudo`. - Docker Engine y el plugin de Docker Compose instalados desde el repositorio oficial de Docker. - Nginx y Certbot instalados (`sudo apt install nginx certbot python3-certbot-nginx`). - Un dominio, por ejemplo `cms.your_domain`, con un registro DNS A apuntando a `your_server_ip`. - Node.js 20 o superior en tu equipo de desarrollo, solo para compilar la extensión del paso 6. - `jq` en el servidor para leer respuestas JSON (`sudo apt install jq`). Sustituye `your_domain`, `your_server_ip` y el resto de valores de ejemplo por los tuyos a lo largo de la guía. ## Paso 1: Preparar el directorio y los secretos Directus guarda en disco los archivos subidos y las extensiones. Crea un directorio para el proyecto y los dos subdirectorios que se montarán en el contenedor: ```bash sudo mkdir -p /opt/directus/uploads /opt/directus/extensions cd /opt/directus ``` La imagen oficial ejecuta Directus con el usuario `node` (UID 1000), así que ese usuario debe poder escribir en los volúmenes: ```bash sudo chown -R 1000:1000 /opt/directus/uploads /opt/directus/extensions ``` Genera dos valores aleatorios, uno para la contraseña de PostgreSQL y otro para `SECRET`, la clave con la que Directus firma los tokens de sesión: ```bash openssl rand -base64 32 openssl rand -base64 32 ``` Guárdalos en un archivo `.env` que leerá Docker Compose: ```bash sudo nano /opt/directus/.env ``` ```ini DB_PASSWORD=pega_aqui_el_primer_valor DIRECTUS_SECRET=pega_aqui_el_segundo_valor ADMIN_EMAIL=admin@your_domain ADMIN_PASSWORD=your_strong_password PUBLIC_URL=https://cms.your_domain ``` Restringe los permisos del archivo, ya que contiene credenciales: ```bash sudo chmod 600 /opt/directus/.env ``` ## Paso 2: Definir los servicios con Docker Compose El despliegue tiene tres servicios: PostgreSQL como base de datos, Redis como caché y Directus. Directus solo escucha en `127.0.0.1`, porque el tráfico público entrará por Nginx. ```bash sudo nano /opt/directus/docker-compose.yml ``` ```yaml services: database: image: postgres:16 restart: unless-stopped volumes: - postgres_data:/var/lib/postgresql/data environment: POSTGRES_DB: directus POSTGRES_USER: directus POSTGRES_PASSWORD: ${DB_PASSWORD} healthcheck: test: ["CMD", "pg_isready", "-U", "directus", "-d", "directus"] interval: 10s timeout: 5s retries: 5 cache: image: redis:7 restart: unless-stopped directus: image: directus/directus:11 restart: unless-stopped ports: - "127.0.0.1:8055:8055" volumes: - ./uploads:/directus/uploads - ./extensions:/directus/extensions depends_on: database: condition: service_healthy cache: condition: service_started environment: SECRET: ${DIRECTUS_SECRET} PUBLIC_URL: ${PUBLIC_URL} DB_CLIENT: pg DB_HOST: database DB_PORT: 5432 DB_DATABASE: directus DB_USER: directus DB_PASSWORD: ${DB_PASSWORD} CACHE_ENABLED: "true" CACHE_AUTO_PURGE: "true" CACHE_STORE: redis REDIS: redis://cache:6379 ADMIN_EMAIL: ${ADMIN_EMAIL} ADMIN_PASSWORD: ${ADMIN_PASSWORD} ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION: 4000 volumes: postgres_data: ``` Algunas decisiones de esta configuración: - La etiqueta `directus/directus:11` fija la versión mayor. Con `latest` una actualización a una versión mayor nueva podría llegar sin que la esperes y aplicar migraciones de base de datos. - `CACHE_AUTO_PURGE` vacía la caché de Redis cada vez que se modifica contenido, de modo que la API nunca devuelve datos antiguos. - `ADMIN_EMAIL` y `ADMIN_PASSWORD` solo se usan en el primer arranque, para crear el administrador inicial. - `ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION` limita el tamaño máximo de las imágenes transformadas y evita que alguien agote la memoria pidiendo miniaturas enormes. Arranca los servicios: ```bash sudo docker compose up -d ``` En el primer arranque Directus crea sus tablas en PostgreSQL. Sigue los logs hasta ver que el servidor escucha: ```bash sudo docker compose logs -f directus ``` ```output directus-1 | [..] INFO: Initializing bootstrap... directus-1 | [..] INFO: Database empty, creating new schema directus-1 | [..] INFO: Adding first admin user... directus-1 | [..] INFO: Server started at http://0.0.0.0:8055 ``` Pulsa `Ctrl+C` para salir de los logs y comprueba que la API responde: ```bash curl http://127.0.0.1:8055/server/ping ``` ```output pong ``` ## Paso 3: Publicar Directus con Nginx y HTTPS Crea un bloque de servidor de Nginx que reenvíe las peticiones a Directus: ```bash sudo nano /etc/nginx/sites-available/directus ``` ```nginx server { listen 80; server_name cms.your_domain; client_max_body_size 100M; location / { proxy_pass http://127.0.0.1:8055; 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; } } ``` `client_max_body_size` permite subir archivos de hasta 100 MB; el valor por defecto de Nginx (1 MB) haría fallar la mayoría de subidas de imágenes. Activa el sitio, comprueba la sintaxis y recarga Nginx: ```bash sudo ln -s /etc/nginx/sites-available/directus /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx ``` Abre HTTP y HTTPS en el cortafuegos si usas UFW y solicita el certificado: ```bash sudo ufw allow 'Nginx Full' sudo certbot --nginx -d cms.your_domain ``` Certbot modifica el bloque de servidor para servir HTTPS y redirigir HTTP. Abre `https://cms.your_domain` en el navegador e inicia sesión con `ADMIN_EMAIL` y `ADMIN_PASSWORD`. Cambia la contraseña del administrador desde su perfil y elimina `ADMIN_PASSWORD` del archivo `.env`, ya que no se volverá a usar. ## Paso 4: Crear una colección y controlar el acceso con políticas A partir de Directus 11 los permisos no se asignan directamente a un rol: se definen en **políticas de acceso** y cada rol o usuario recibe una o varias políticas. Así puedes reutilizar la misma política en varios roles. Primero crea una colección de ejemplo en **Settings > Data Model > Create Collection**, con el nombre `articulos`. En la pantalla de creación marca los campos opcionales **Status**, **User Created** y **Date Created**. Después añade dos campos de tipo **Input**: `titulo` y `slug`, y uno de tipo **WYSIWYG** llamado `contenido`. El campo `status` que crea Directus tiene los valores `published`, `draft` y `archived`. Ahora crea la política para los editores en **Settings > Access Policies > Create Policy**: 1. Nombre: `Editores de artículos`. Activa **App Access** para que puedan entrar al panel. 2. En la sección de permisos añade la colección `articulos`. 3. **Create**: acceso completo. 4. **Read**: acceso completo, para que vean el trabajo de todo el equipo. 5. **Update**: acceso personalizado con la regla de ítems `User Created` igual a `$CURRENT_USER`. En la pestaña de campos, deja marcados solo `titulo`, `slug`, `contenido` y `status`. 6. **Delete**: sin acceso. Por último crea el rol en **Settings > User Roles > Create Role**, llámalo `Editor`, asígnale la política `Editores de artículos` y crea un usuario de prueba con ese rol en **User Directory**. Comprueba desde la API que las reglas funcionan. Obtén un token del usuario editor: ```bash EDITOR_TOKEN=$(curl -s -X POST https://cms.your_domain/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"editor@your_domain","password":"password_del_editor"}' \ | jq -r '.data.access_token') ``` Crea un artículo y comprueba que se guarda con el editor como autor: ```bash curl -s -X POST https://cms.your_domain/items/articulos \ -H "Authorization: Bearer $EDITOR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"titulo":"Primer artículo","slug":"primer-articulo","status":"draft"}' | jq '.data.id' ``` ```output 1 ``` Si intentas borrarlo, Directus responde con un error de permisos: ```bash curl -s -X DELETE https://cms.your_domain/items/articulos/1 \ -H "Authorization: Bearer $EDITOR_TOKEN" | jq '.errors[0].extensions.code' ``` ```output "FORBIDDEN" ``` Para publicar solo el contenido publicado sin autenticación, edita la política **Public** y concede **Read** sobre `articulos` con la regla `Status` igual a `published`. ## Paso 5: Automatizar tareas con Flows Los Flows son la herramienta de automatización integrada en Directus y sustituyen a los antiguos webhooks, que se eliminaron en la versión 11. Un Flow se compone de un disparador y una cadena de operaciones. Los disparadores disponibles son: | Disparador | Cuándo se ejecuta | |---|---| | Event Hook | Al crear, actualizar o borrar ítems. En modo *Filter* bloquea la operación hasta terminar; en modo *Action* se ejecuta después, sin bloquear | | Webhook | Al recibir una petición HTTP en `/flows/trigger/` | | Schedule (CRON) | Según una expresión cron | | Another Flow | Cuando otro Flow lo invoca | | Manual | Desde un botón en el panel de una colección | Como ejemplo, crea un Flow que avise a un sistema externo cada vez que se publica un artículo. En **Settings > Flows > Create Flow**: 1. Nombre: `Notificar publicación`. 2. Disparador: **Event Hook**, tipo **Action (Non-Blocking)**, alcance `items.update`, colección `articulos`. 3. Añade una operación **Condition** con esta regla, para continuar solo cuando el cambio pone el estado en `published`: ```json { "$trigger": { "payload": { "status": { "_eq": "published" } } } } ``` 4. En la salida de éxito de la condición añade una operación **Run Script** que prepare el cuerpo de la notificación: ```javascript module.exports = async function (data) { return { ids: data.$trigger.keys, publicado_en: new Date().toISOString(), }; }; ``` 5. A continuación añade una operación **Webhook / Request URL** con método `POST`, la URL de tu sistema externo y como cuerpo `{{$last}}`, que contiene el resultado del script anterior. Guarda el Flow, cambia el estado de un artículo a `published` desde el panel y abre el Flow: el icono de registros de la barra lateral muestra cada ejecución, con los datos que recibió y devolvió cada operación. Si una ejecución falla, ese registro es el primer sitio que debes revisar. > Nota: los scripts de **Run Script** se ejecutan en un entorno aislado sin acceso al sistema de archivos ni a módulos de Node.js. Para lógica que necesita dependencias, usa una extensión como la del paso siguiente. ## Paso 6: Crear una extensión de tipo hook Las extensiones permiten añadir código propio a la API o al panel. Una extensión de tipo **hook** se ejecuta en eventos del servidor, igual que un Flow, pero con todo el ecosistema de npm disponible. En este ejemplo validarás el título y generarás el slug automáticamente al crear un artículo. En tu equipo de desarrollo, genera el esqueleto de la extensión: ```bash npx create-directus-extension@latest ``` Responde al asistente con el tipo `hook`, el nombre `directus-extension-articulos` y el lenguaje `javascript`. Después entra en el directorio y abre el código: ```bash cd directus-extension-articulos nano src/index.js ``` Sustituye el contenido por lo siguiente: ```javascript import { defineHook } from '@directus/extensions-sdk'; import { createError } from '@directus/errors'; const TituloInvalido = createError( 'INVALID_PAYLOAD', 'El título debe tener al menos 5 caracteres', 400, ); function slugify(texto) { return texto .normalize('NFD') .replace(/[̀-ͯ]/g, '') .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); } export default defineHook(({ filter }, { logger }) => { filter('articulos.items.create', (payload) => { if (!payload.titulo || payload.titulo.trim().length < 5) { throw new TituloInvalido(); } if (!payload.slug) { payload.slug = slugify(payload.titulo); } logger.info(`Artículo nuevo con slug ${payload.slug}`); return payload; }); }); ``` `filter` se ejecuta antes de guardar y puede modificar o rechazar los datos; `action` se ejecuta después y sirve para efectos secundarios. Compila la extensión: ```bash npm run build ``` Directus 11 carga cada extensión desde su propio directorio dentro de `extensions/`, con su `package.json` y la carpeta `dist`. Copia ambos al servidor: ```bash ssh your_user@your_server_ip 'mkdir -p ~/directus-extension-articulos' scp -r package.json dist your_user@your_server_ip:~/directus-extension-articulos/ ``` En el servidor, muévela al directorio de extensiones, ajusta el propietario y reinicia Directus: ```bash sudo mv ~/directus-extension-articulos /opt/directus/extensions/ sudo chown -R 1000:1000 /opt/directus/extensions cd /opt/directus sudo docker compose restart directus ``` Comprueba que la extensión se ha cargado: ```bash sudo docker compose logs directus | grep -i extension ``` ```output directus-1 | [..] INFO: Loaded extensions: directus-extension-articulos ``` Crea un artículo con un título corto para verificar la validación: ```bash curl -s -X POST https://cms.your_domain/items/articulos \ -H "Authorization: Bearer $EDITOR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"titulo":"Hola"}' | jq '.errors[0].message' ``` ```output "El título debe tener al menos 5 caracteres" ``` Con un título válido y sin `slug`, el artículo se guarda con el slug generado, por ejemplo `configuracion-de-produccion` para el título "Configuración de producción". ## Paso 7: Transformar imágenes al vuelo Directus sirve los archivos en `/assets/` y puede redimensionarlos y convertirlos de formato en la propia petición, guardando el resultado para las siguientes. Los parámetros principales son: | Parámetro | Valores | |---|---| | `width`, `height` | Tamaño en píxeles | | `fit` | `cover`, `contain`, `inside`, `outside` | | `quality` | 1 a 100 | | `format` | `auto`, `jpg`, `png`, `webp`, `tiff`, `avif` | | `withoutEnlargement` | `true` para no ampliar imágenes pequeñas | Sube una imagen desde **File Library** y copia su identificador. Con un archivo accesible para el público, esta petición devuelve una miniatura de 400x300 en WebP: ```bash curl -s -o miniatura.webp \ "https://cms.your_domain/assets/ID_DEL_ARCHIVO?width=400&height=300&fit=cover&format=webp&quality=80" file miniatura.webp ``` ```output miniatura.webp: RIFF (little-endian) data, Web/P image ``` Permitir cualquier combinación de parámetros deja que un cliente genere miles de variantes y llene el disco. En producción, define tamaños con nombre en **Settings > Settings > Files & Storage > Storage Asset Presets** (por ejemplo `miniatura` con 400x300, `cover`, WebP) y cambia **Storage Asset Transform** a **Presets Only**. Los clientes piden entonces `/assets/ID_DEL_ARCHIVO?key=miniatura`. ## Solución de problemas **Directus se reinicia en bucle con errores de base de datos.** Suele ocurrir cuando se cambia `DB_PASSWORD` después del primer arranque: PostgreSQL ya creó el usuario con la contraseña anterior y la variable solo se aplica al inicializar el volumen. Restablece la contraseña dentro del contenedor: ```bash sudo docker compose exec database psql -U directus -d directus -c "ALTER USER directus WITH PASSWORD 'la_nueva_contraseña';" ``` **Las subidas fallan con `EACCES`.** Los directorios montados no pertenecen al UID 1000. Repite el `chown` del paso 1. **Las subidas grandes devuelven `413 Request Entity Too Large`.** Aumenta `client_max_body_size` en el bloque de Nginx y recárgalo. **La extensión no aparece en los logs.** Comprueba que la ruta es `/opt/directus/extensions/directus-extension-articulos/package.json` y que existe `dist/index.js`. Una carpeta con el tipo como nombre (`extensions/hooks/...`) corresponde al formato antiguo y no se carga. **Un Flow no se dispara.** Verifica que está activo, que el alcance (`items.create`, `items.update`) y la colección coinciden y revisa sus registros de ejecución. ## Conclusión Tienes Directus 11 funcionando con PostgreSQL y Redis tras Nginx con HTTPS, permisos basados en políticas, un Flow que reacciona a la publicación de contenido, una extensión propia que valida datos y transformaciones de imagen limitadas a presets. Como siguientes pasos, programa copias de seguridad diarias de la base de datos con `pg_dump` y del directorio `uploads`, mueve los archivos a un almacenamiento compatible con S3 mediante las variables `STORAGE_*` si vas a escalar a varias instancias, y consulta el SDK `@directus/sdk` para consumir la API desde tu frontend.