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:
- Trigger: Event Hook →
items.updateonarticlescollection - Condition: Check
status = "published" - Action: Webhook/Request to your endpoint
Scheduled data sync:
- Trigger: Schedule (cron) →
0 */6 * * * - Action: Read Data from collection
- Action: Transform data with Run Script
- 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.


