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.

🐾 What you'll need:
  • 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:

DirectusStrapiContentful
Data ownershipPlain SQL tables you controlStrapi's own schema in your DBContentful's cloud, not yours
CostSelf-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 styleREST + GraphQL auto-generatedREST + GraphQLREST + GraphQL
Real-timeWebSocket subscriptions built-inNeeds pluginYes
Visual schema editor✅ Yes (modify tables from UI)✅ Yes✅ Yes
LicenseBusiness Source License → MITSSPLProprietary

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:

  1. Collection Name: articles
  2. Primary Key Field: id (UUID — Directus default)
  3. Optional Fields: enable Status, Created on/by, Updated on/by
  4. Click Finish

Now add fields. Click into the collection → + Create Field:

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.

SettingsAccess 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

DatabaseSupportedNotes
PostgreSQL 12+✅ RecommendedBest performance, most features
MySQL 8+Full feature support
MariaDB 10.5+MySQL-compatible
SQLiteGreat for dev, fine for small prod
MS SQL ServerFor wrapping legacy enterprise schemas
Oracle DBSame — 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

PlanContent volumeNotes
Starter ($7.99, 2 GB)Up to 100k itemsSingle small project, light editor team
Pro ($15.99, 4 GB)Up to 1M itemsProduction agency setups, multiple projects
Premium ($35.99, 8 GB)10M+ itemsMulti-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.

🐱
OliveVPS Team

We use Directus as the content backend for several agency client sites — each in its own Postgres, all served from one Directus VPS. The Pro plan handles roughly 12 active projects with shared Postgres tenants. Migration from a custom Express+Postgres backend took an afternoon; saved months of API maintenance.