WordPress is a CMS pretending to be a backend. Strapi locks you into its data model. Contentful charges $300/month for anything serious. Directus is different: it wraps any SQL database — Postgres, MySQL, MariaDB, SQLite, MS SQL — and instantly gives you a beautiful admin UI plus auto-generated REST and GraphQL APIs. Your data stays in plain SQL tables you control; Directus is just the API + UI layer on top.
This guide installs Directus with Postgres backing, configures HTTPS via Nginx, sets up role-based permissions, connects S3-compatible file storage (Backblaze B2 in our example), and walks through using the auto-generated APIs from a Next.js frontend. By the end you'll have a real headless CMS — the kind agencies and modern dev teams actually use — running on a $7.99/month VPS.
- A Linux VPS — Ubuntu 22.04 or Debian 12, 2 GB RAM minimum
- A domain pointed at your VPS
- (Optional) An S3-compatible storage account for file uploads
- Roughly 30 minutes
For Directus pre-installed with Postgres and S3-compatible storage available, see our Directus VPS plans.
1. Why Directus vs Strapi / Contentful
Three trade-offs that distinguish the modern headless CMS options:
| Directus | Strapi | Contentful | |
|---|---|---|---|
| Data ownership | Plain SQL tables you control | Strapi's own schema in your DB | Contentful's cloud, not yours |
| Cost | Self-host (free) or Cloud ($15+/mo) | Self-host (free) or Cloud ($99+/mo) | $489/mo entry tier |
| Wraps existing DB? | ✅ Yes — point at existing tables | ❌ Strapi owns the schema | ❌ Cloud only |
| API style | REST + GraphQL auto-generated | REST + GraphQL | REST + GraphQL |
| Real-time | WebSocket subscriptions built-in | Needs plugin | Yes |
| Visual schema editor | ✅ Yes (modify tables from UI) | ✅ Yes | ✅ Yes |
| License | Business Source License → MIT | SSPL | Proprietary |
Directus wins if you have an existing database you want to wrap (legacy ERP, analytics warehouse, custom app schema). Strapi wins if you're starting fresh and prefer a more opinionated content model. Contentful wins if you want zero ops and have the budget. For most self-hosters and agencies, Directus is the better choice.
2. Prepare the VPS
apt update && apt upgrade -y
apt install -y curl ca-certificates gnupg ufw
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
adduser directus
usermod -aG sudo directus
rsync --archive --chown=directus:directus ~/.ssh /home/directus
3. Install Docker
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
usermod -aG docker directus
4. Deploy Directus + Postgres
su - directus
mkdir -p ~/directus/{database,uploads,extensions}
cd ~/directus
# Generate strong random keys
KEY=$(openssl rand -hex 16)
SECRET=$(openssl rand -hex 32)
ADMIN_PW=$(openssl rand -base64 24)
DB_PW=$(openssl rand -base64 24)
cat > .env <<EOF
KEY=$KEY
SECRET=$SECRET
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=$ADMIN_PW
DB_PASSWORD=$DB_PW
EOF
chmod 600 .env
cat .env # Save these somewhere safe!
Create compose.yaml:
services:
database:
image: postgis/postgis:16-master
container_name: directus-db
restart: unless-stopped
volumes:
- ./database:/var/lib/postgresql/data
environment:
POSTGRES_USER: directus
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: directus
healthcheck:
test: ["CMD", "pg_isready", "-U", "directus"]
interval: 5s
retries: 10
cache:
image: redis:7-alpine
container_name: directus-cache
restart: unless-stopped
directus:
image: directus/directus:latest
container_name: directus
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:
KEY: ${KEY}
SECRET: ${SECRET}
ADMIN_EMAIL: ${ADMIN_EMAIL}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
DB_CLIENT: pg
DB_HOST: database
DB_PORT: 5432
DB_DATABASE: directus
DB_USER: directus
DB_PASSWORD: ${DB_PASSWORD}
CACHE_ENABLED: "true"
CACHE_STORE: redis
REDIS: "redis://cache:6379"
PUBLIC_URL: "https://cms.example.com"
CORS_ENABLED: "true"
CORS_ORIGIN: "true"
Start it:
docker compose up -d
docker compose logs -f
First start pulls ~2 GB of images and initializes the Postgres schema. Watch for Server started at http://0.0.0.0:8055 — that's when you can move on.
5. Nginx reverse proxy + HTTPS
sudo apt install -y nginx certbot python3-certbot-nginx
sudo tee /etc/nginx/sites-available/directus <<'EOF'
server {
listen 80;
server_name cms.example.com;
# File uploads can be large
client_max_body_size 1G;
proxy_read_timeout 600s;
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;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
EOF
sudo ln -s /etc/nginx/sites-available/directus /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d cms.example.com
Visit https://cms.example.com and sign in with the admin email + password you set in .env.
6. Create your first collection
Directus calls database tables "collections". Click Settings (gear icon) → Data Model → + Create Collection.
Build an example "articles" collection:
- Collection Name: articles
- Primary Key Field: id (UUID — Directus default)
- Optional Fields: enable Status, Created on/by, Updated on/by
- Click Finish
Now add fields. Click into the collection → + Create Field:
- title — String — required
- slug — String — required, unique. (Configure auto-fill from title in the field's "Interface" settings.)
- body — Text — Markdown interface
- featured_image — Many-to-one → directus_files (built-in files collection)
- published_at — Date
- author — Many-to-one → directus_users
Save. The Postgres table is created with the corresponding columns automatically. Now Content tab → Articles → + Create Item. Fill in the form, save. You just created your first piece of content.
The API is already live:
# REST
curl https://cms.example.com/items/articles
# GraphQL
curl -X POST https://cms.example.com/graphql -H 'Content-Type: application/json' \
-d '{"query":"{ articles { id title slug body } }"}'
You did not write a single line of API code. Directus introspected the schema and generated REST + GraphQL endpoints.
7. Roles and permissions
By default, all content is private. To expose articles to your frontend you need to grant read permissions to the Public role.
Settings → Access Control → click the Public role → articles → toggle Read to "All Access".
Now the API returns articles without authentication. For more control, use field-level permissions ("public can read title and body but not author email") or filter rules ("public can only read where status = published").
For authenticated users (your app's frontend acting on behalf of a logged-in user): create a custom role with the permissions you need, generate an access token under that user's profile, send Authorization: Bearer <token> in API requests.
🐾 Directus pre-tuned
Our Directus VPS plans ship with Postgres optimised, Redis caching wired up, and S3-compatible storage available — focus on building, not on plumbing.
See Directus VPS Plans →8. File storage with S3-compatible providers
By default, file uploads go to the ./uploads volume on the VPS. Fine for small projects. For larger media libraries (or for CDN distribution), use an S3-compatible store.
Backblaze B2 is the cheapest option ($0.005/GB/month, way under AWS S3). Sign up, create a bucket, generate an application key, then add to .env:
STORAGE_LOCATIONS=b2
STORAGE_B2_DRIVER=s3
STORAGE_B2_KEY=YOUR_KEY_ID
STORAGE_B2_SECRET=YOUR_APPLICATION_KEY
STORAGE_B2_BUCKET=your-bucket-name
STORAGE_B2_REGION=us-west-002
STORAGE_B2_ENDPOINT=https://s3.us-west-002.backblazeb2.com
docker compose up -d to apply. New uploads go to B2; existing uploads stay on the local volume. To migrate existing files, copy them up to B2 manually and Directus will find them by filename.
9. Use the API from a Next.js app
The official JavaScript SDK is the cleanest way to consume Directus from a frontend:
npm install @directus/sdk
In Next.js (server-rendered page):
import { createDirectus, rest, readItems } from '@directus/sdk';
const client = createDirectus('https://cms.example.com').with(rest());
export async function getStaticProps() {
const articles = await client.request(
readItems('articles', {
filter: { status: { _eq: 'published' } },
sort: ['-published_at'],
limit: 10,
fields: ['id', 'title', 'slug', 'published_at', 'featured_image.*'],
})
);
return { props: { articles }, revalidate: 60 };
}
For mutations (creating / updating content), authenticate first with authentication() and a static token. Directus handles real-time updates via WebSocket if you wire realtime() — useful for live dashboards.
10. Reference: features, limits, scaling
Database support
| Database | Supported | Notes |
|---|---|---|
| PostgreSQL 12+ | ✅ Recommended | Best performance, most features |
| MySQL 8+ | ✅ | Full feature support |
| MariaDB 10.5+ | ✅ | MySQL-compatible |
| SQLite | ✅ | Great for dev, fine for small prod |
| MS SQL Server | ✅ | For wrapping legacy enterprise schemas |
| Oracle DB | ✅ | Same — legacy schema wrapping |
Flows (workflow automation)
Directus has a built-in workflow engine called Flows — visual node-based automations triggered by events (item created, time-based, webhook). Common use cases: send Slack notification on new article, post to social media on publish, validate data with custom logic. Lighter than dedicated automation tools like n8n but well-integrated.
Extensions and hooks
Custom logic via extensions — Node.js modules that hook into events. Useful patterns: server-side validation, custom field interfaces, custom display formatters, custom layouts, custom panels in the dashboard. Drop extension folders into ./extensions and Directus auto-loads them on restart.
Per-plan capacity
| Plan | Content volume | Notes |
|---|---|---|
| Starter ($7.99, 2 GB) | Up to 100k items | Single small project, light editor team |
| Pro ($15.99, 4 GB) | Up to 1M items | Production agency setups, multiple projects |
| Premium ($35.99, 8 GB) | 10M+ items | Multi-project agency, high-traffic frontend |
Cloud caching
For high-traffic public endpoints, put a CDN in front. Directus supports Cache-Control headers natively; configure the cache TTL per role/permission. CloudFront, Cloudflare, Fastly — all work without extra config on the Directus side.
Migration from Strapi / Contentful
No official importer. The typical migration path is custom: export source as JSON, write a small Node.js script that POSTs to /items/your_collection with the right shape. Manageable for <10k items per collection; tedious above that.
FAQ
Will Directus mess up my existing database schema?
No — Directus is non-destructive when pointed at an existing database. It introspects your existing tables and exposes them through its admin UI and API. If you create new fields through Directus, those become real columns in your tables. If you delete a Directus-managed collection, that table is dropped. For an existing DB, the safe path is: connect, browse, build APIs, leave the existing data alone.
Directus vs Strapi — quick verdict?
Directus wraps any SQL database with no schema changes; Strapi has its own data model that lives in your DB. Directus wins if you have an existing DB or want a database-first approach. Strapi wins if you're greenfield and want a more opinionated content model. License-wise, Strapi is SSPL (more restrictive); Directus is Business Source License that converts to MIT after 4 years (less restrictive long-term).
Is Directus production-ready?
Yes — used by Hubspot, NASA, Mozilla, plus thousands of agencies. The 10.x line has been stable since 2023. The team ships frequently and is responsive on GitHub. The free self-hosted version has every feature of the paid Cloud version — there's no artificial limitation.
Does Directus have built-in authentication for end users?
Yes — Directus has full user management with email/password, OAuth (Google, GitHub, Facebook, custom OIDC), SAML, and LDAP. The directus_users collection is the user table; create roles with permissions; issue tokens via the auth API. For app login (not just CMS admin), Directus is a fine auth backend.
Can I use Directus with my Next.js / Astro / Nuxt site?
Yes — all of them. Directus has official SDK packages for JavaScript / TypeScript. For Next.js, fetch in getStaticProps or React Server Components with readItems. For mutations and live data, the SDK supports authentication and WebSocket subscriptions.
What if I want to leave Directus later?
Your data is in plain SQL tables. Stop running Directus, point any other tool (Strapi, Hasura, custom backend) at the same database. No data export needed — there's no proprietary format. This is the killer reason to use Directus over Contentful: zero lock-in.