Directus Headless CMS Advanced Configuration

Directus is a flexible open-source headless CMS that wraps any SQL database with a dynamic REST and GraphQL API, a no-code data studio, and extensible automation tools. This guide covers advanced Directus configuration including Flows automation, granular permissions, custom extensions, webhooks, and multi-database deployments on Linux.

Prerequisites

  • Docker and Docker Compose installed
  • PostgreSQL or MySQL database (or use the included SQLite for development)
  • Node.js 18+ (for extension development)
  • Basic familiarity with Directus concepts (collections, fields, roles)

Docker Deployment

# docker-compose.yml
version: '3'

services:
  database:
    image: postgis/postgis:15-master
    environment:
      POSTGRES_USER: directus
      POSTGRES_PASSWORD: directus-db-password
      POSTGRES_DB: directus
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U directus"]
      interval: 10s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]

  directus:
    image: directus/directus:latest
    ports:
      - "8055:8055"
    depends_on:
      database:
        condition: service_healthy
      cache:
        condition: service_healthy
    environment:
      SECRET: "your-32-char-random-secret-key"
      DB_CLIENT: "pg"
      DB_HOST: database
      DB_PORT: 5432
      DB_DATABASE: directus
      DB_USER: directus
      DB_PASSWORD: directus-db-password
      CACHE_ENABLED: "true"
      CACHE_STORE: "redis"
      REDIS: "redis://cache:6379"
      ADMIN_EMAIL: "[email protected]"
      ADMIN_PASSWORD: "SecureAdminPass123!"
      PUBLIC_URL: "https://cms.yourdomain.com"
      STORAGE_LOCATIONS: "local"
      STORAGE_LOCAL_ROOT: "/directus/uploads"
      EXTENSIONS_AUTO_RELOAD: "true"
    volumes:
      - uploads:/directus/uploads
      - extensions:/directus/extensions

volumes:
  db_data:
  uploads:
  extensions:
# Start the stack
docker compose up -d

# View logs
docker compose logs directus --tail 50 -f

# Run migrations manually
docker compose exec directus npx directus database migrate:latest

Flows Automation

Directus Flows provide a visual no-code automation builder. Access them via Settings → Flows.

Common Flow Patterns

Send a webhook when content is published:

  1. Trigger: Event Hook → items.update on articles collection
  2. Condition: Check status = "published"
  3. Action: Webhook/Request to your endpoint

Scheduled data sync:

  1. Trigger: Schedule (cron) → 0 */6 * * *
  2. Action: Read Data from collection
  3. Action: Transform data with Run Script
  4. Action: Create/Update records
// Example "Run Script" operation in a Flow
// Available context: $trigger, $last, $accountability, $env
module.exports = async function(data) {
    const items = data.$last;

    // Transform and filter items
    const processed = items
        .filter(item => item.status === 'published')
        .map(item => ({
            id: item.id,
            slug: item.title.toLowerCase().replace(/\s+/g, '-'),
            published_at: new Date().toISOString()
        }));

    return { processed_count: processed.length, items: processed };
};

Triggering Flows via API

# Manual trigger (requires flow to have Manual trigger type)
curl -X POST "https://cms.yourdomain.com/flows/trigger/FLOW-UUID" \
  -H "Authorization: Bearer your-token" \
  -H "Content-Type: application/json" \
  -d '{"articleId": 42, "action": "reindex"}'

Granular Permissions and Roles

Directus uses role-based access control with field-level granularity:

Creating Roles via API

# Create an Editor role
curl -X POST "https://cms.yourdomain.com/roles" \
  -H "Authorization: Bearer admin-token" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Editor",
    "icon": "edit",
    "description": "Can create and edit articles",
    "enforce_tfa": false
  }'

Setting Collection Permissions

# Allow Editor role to read articles
curl -X POST "https://cms.yourdomain.com/permissions" \
  -H "Authorization: Bearer admin-token" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "editor-role-uuid",
    "collection": "articles",
    "action": "read",
    "permissions": {},
    "validation": {},
    "fields": ["id", "title", "content", "status"],
    "presets": null
  }'

# Allow Editor to create, but only set status to "draft"
curl -X POST "https://cms.yourdomain.com/permissions" \
  -H "Authorization: Bearer admin-token" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "editor-role-uuid",
    "collection": "articles",
    "action": "create",
    "permissions": {},
    "validation": {
      "status": {"_eq": "draft"}
    },
    "fields": ["title", "content", "category"],
    "presets": {"status": "draft", "author": "$CURRENT_USER"}
  }'

Row-Level Permissions

# Editors can only edit their own articles
curl -X POST "https://cms.yourdomain.com/permissions" \
  -H "Authorization: Bearer admin-token" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "editor-role-uuid",
    "collection": "articles",
    "action": "update",
    "permissions": {
      "author": {"_eq": "$CURRENT_USER"}
    },
    "fields": ["title", "content"]
  }'

Custom Extensions

Directus supports four extension types: endpoints, hooks, interfaces, and displays.

Custom API Endpoint

# Create extension directory
mkdir -p /var/lib/docker/volumes/directus_extensions/_data
cd /var/lib/docker/volumes/directus_extensions/_data

# Create endpoint extension
mkdir -p endpoints/stats
cat > endpoints/stats/index.js << 'EOF'
export default {
    id: 'stats',
    handler: (router, { services, database }) => {
        router.get('/', async (req, res) => {
            const { ItemsService } = services;
            const articlesService = new ItemsService('articles', {
                schema: req.schema,
                accountability: req.accountability
            });

            const count = await articlesService.readByQuery({
                aggregate: { count: ['*'] }
            });

            res.json({
                total_articles: count[0].count,
                generated_at: new Date().toISOString()
            });
        });
    }
};
EOF

Custom Hook Extension

mkdir -p hooks/send-notification
cat > hooks/send-notification/index.js << 'EOF'
export default ({ action }) => {
    // Send notification when an article is published
    action('items.update', ({ payload, key, collection }) => {
        if (collection !== 'articles') return;
        if (payload.status !== 'published') return;

        console.log(`Article ${key} was published`);

        // Trigger your notification logic here
        fetch('https://ntfy.yourdomain.com/cms-updates', {
            method: 'POST',
            body: `Article ID ${key} was just published`,
            headers: { 'Title': 'New Article Published' }
        }).catch(console.error);
    });
};
EOF
# Restart Directus to load extensions
docker compose restart directus

Webhooks and Event Hooks

Legacy Webhooks

# Create a webhook via API
curl -X POST "https://cms.yourdomain.com/webhooks" \
  -H "Authorization: Bearer admin-token" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Revalidate Next.js Cache",
    "method": "POST",
    "url": "https://yourapp.com/api/revalidate",
    "status": "active",
    "data": true,
    "actions": ["create", "update", "delete"],
    "collections": ["articles", "pages"],
    "headers": [
      {"header": "x-revalidation-secret", "value": "your-secret-token"}
    ]
  }'

Environment-Based Event Hooks

# In docker-compose.yml environment section:
HOOKS_HTTP_CONTROLLERS: "true"

# Configure hook via env vars
HOOKS_TIMEOUT: "30000"

Asset Transformation

Directus can transform images on the fly via query parameters:

# Resize and convert image
# Original: /assets/IMAGE-UUID
# Transformed: /assets/IMAGE-UUID?width=800&height=600&fit=cover&format=webp&quality=80

# Available transformation parameters:
# width, height - dimensions in pixels
# fit - cover, contain, fill, inside, outside
# format - jpeg, png, webp, avif, tiff
# quality - 1-100
# focal_point_x, focal_point_y - smart crop focus
# withoutEnlargement - don't upscale small images

# Configure image transformation limits in environment:
# ASSETS_TRANSFORM_MAX_CONCURRENT=4
# ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION=6000

Storage with S3

# Add to docker-compose.yml environment:
STORAGE_LOCATIONS: "s3"
STORAGE_S3_DRIVER: "s3"
STORAGE_S3_KEY: "your-access-key"
STORAGE_S3_SECRET: "your-secret-key"
STORAGE_S3_BUCKET: "my-directus-uploads"
STORAGE_S3_REGION: "us-east-1"
# Optional: custom endpoint for MinIO
STORAGE_S3_ENDPOINT: "https://minio.yourdomain.com"

Multi-Database Support

Directus supports PostgreSQL, MySQL, MariaDB, SQLite, MS SQL Server, and CockroachDB:

# MySQL configuration
DB_CLIENT: "mysql"
DB_HOST: "mysql-server"
DB_PORT: "3306"
DB_DATABASE: "directus"
DB_USER: "directus"
DB_PASSWORD: "password"

# SQLite (development only)
DB_CLIENT: "sqlite3"
DB_FILENAME: "/directus/database.db"

# MariaDB
DB_CLIENT: "mysql"
DB_HOST: "mariadb"
DB_PORT: "3306"

# CockroachDB (PostgreSQL compatible)
DB_CLIENT: "cockroachdb"
DB_HOST: "cockroachdb-node"
DB_PORT: "26257"

Database Migrations

# Create a snapshot (schema export)
docker compose exec directus npx directus schema snapshot /directus/snapshot.yaml

# Apply a snapshot to another instance
docker compose exec directus npx directus schema apply /directus/snapshot.yaml

# Run pending migrations
docker compose exec directus npx directus database migrate:latest

# Check migration status
docker compose exec directus npx directus database migrate:status

Troubleshooting

Directus fails to start:

# Check all required env vars are set
docker compose exec directus env | grep -E "DB_|SECRET|PUBLIC_URL"

# Check database connectivity
docker compose exec directus npx directus database install --yes

Extensions not loading:

# Verify extension directory is mounted
docker compose exec directus ls /directus/extensions/

# Check extension syntax errors
docker compose logs directus 2>&1 | grep -i "extension\|error"

# Force extension reload (if EXTENSIONS_AUTO_RELOAD=true)
touch /path/to/extension/index.js

Permission issues with content API:

# Test with admin token first to isolate permission issues
curl "https://cms.yourdomain.com/items/articles" \
  -H "Authorization: Bearer admin-token"

# Then test with role token
# Compare field visibility differences

Conclusion

Directus's combination of a powerful Flows automation engine, granular RBAC permissions, and a flexible extension API makes it one of the most developer-friendly headless CMS platforms available. Its database-first approach means you can wrap an existing production database and get a full-featured admin panel and API without any data migration. Pair it with a CDN and a modern frontend framework for a production-ready content platform.