How to Deploy Directus on a VPS: Headless CMS with REST and GraphQL APIs

How to Deploy Directus on a VPS: Headless CMS with REST and GraphQL APIs

Directus is an open-source headless CMS that wraps any SQL database (PostgreSQL, MySQL, SQLite) with auto-generated REST and GraphQL APIs, a no-code admin interface for content editors, role-based access control, real-time subscriptions, and a file storage system. Directus Cloud starts at $15/month. Self-hosting on a VPS provides the full feature set at infrastructure cost only — and you own your data schema and content without vendor lock-in.

What Directus Provides

  • Auto-generated API: Create a database table → REST and GraphQL endpoints appear automatically
  • Admin Dashboard: Visual content management for non-technical team members
  • Role-Based Access: Fine-grained permissions per collection per role (read, create, update, delete)
  • File Storage: Asset management with image transformations, CDN integration
  • Realtime: WebSocket subscriptions for live data feeds
  • Flows: No-code automation (webhooks, scheduled tasks, email triggers)

Step 1: Docker Compose Setup

mkdir -p /opt/directus && cd /opt/directus
nano docker-compose.yml
version: '3.8'

services:
  directus:
    image: directus/directus:latest
    container_name: directus
    restart: always
    ports:
      - "127.0.0.1:8055:8055"
    environment:
      SECRET: ${DIRECTUS_SECRET}
      DB_CLIENT: pg
      DB_HOST: directus-db
      DB_PORT: 5432
      DB_DATABASE: directus
      DB_USER: directus
      DB_PASSWORD: ${POSTGRES_PASSWORD}
      ADMIN_EMAIL: ${ADMIN_EMAIL}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      PUBLIC_URL: https://cms.yourdomain.com
      # File storage
      STORAGE_LOCATIONS: local
      STORAGE_LOCAL_ROOT: /directus/uploads
      # Email (optional)
      EMAIL_TRANSPORT: smtp
      EMAIL_SMTP_HOST: ${SMTP_HOST}
      EMAIL_SMTP_PORT: 587
      EMAIL_SMTP_USER: ${SMTP_USER}
      EMAIL_SMTP_PASSWORD: ${SMTP_PASSWORD}
      # CORS for frontend
      CORS_ENABLED: "true"
      CORS_ORIGIN: "https://yourdomain.com,https://www.yourdomain.com"
    volumes:
      - directus_uploads:/directus/uploads
      - directus_extensions:/directus/extensions
    depends_on:
      directus-db:
        condition: service_healthy

  directus-db:
    image: postgres:16-alpine
    container_name: directus-db
    restart: always
    environment:
      POSTGRES_DB: directus
      POSTGRES_USER: directus
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - directus_db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U directus"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  directus_db:
  directus_uploads:
  directus_extensions:
nano .env
POSTGRES_PASSWORD=StrongDirectusDbPassword!
# Generate: openssl rand -base64 32
DIRECTUS_SECRET=your_very_long_random_secret_key_here
ADMIN_EMAIL=admin@yourdomain.com
ADMIN_PASSWORD=YourSecureAdminPassword!
SMTP_HOST=smtp.mailgun.org
SMTP_USER=postmaster@mg.yourdomain.com
SMTP_PASSWORD=your_smtp_password
chmod 600 .env
docker compose up -d
docker compose logs -f directus   # Wait for "Server started at port 8055"

Step 2: Nginx with SSL

sudo nano /etc/nginx/sites-available/directus
server {
    listen 80;
    server_name cms.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name cms.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/cms.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cms.yourdomain.com/privkey.pem;

    client_max_body_size 100M;

    location / {
        proxy_pass http://127.0.0.1:8055;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_read_timeout 300s;
    }
}
sudo ln -s /etc/nginx/sites-available/directus /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d cms.yourdomain.com

Step 3: Create Your Schema

Visit https://cms.yourdomain.com and log in with your admin credentials.

  1. Settings → Data Model → Create Collection
  2. Name: articles → Continue
  3. Add fields: title (String), slug (String), content (Long Text), published_at (DateTime), featured_image (Image)
  4. Create content in the Content section

Step 4: Configure API Permissions

  1. Settings → Roles & Permissions → Public (unauthenticated access)
  2. Enable Read on articles collection for public access
  3. Only expose fields that should be public (exclude admin-only fields)

Step 5: Query the API from Your Frontend

REST API

// Fetch all published articles
const response = await fetch(
  'https://cms.yourdomain.com/items/articles?' +
  new URLSearchParams({
    filter: JSON.stringify({ status: { _eq: 'published' } }),
    sort: '-published_at',
    limit: 10,
    fields: 'id,title,slug,content,published_at,featured_image.*'
  })
);
const { data } = await response.json();

// Fetch single article by slug
const res = await fetch(
  `https://cms.yourdomain.com/items/articles?` +
  `filter[slug][_eq]=${slug}&fields=*,featured_image.*`
);
const { data: [article] } = await res.json();

GraphQL API

// POST to https://cms.yourdomain.com/graphql
const query = `
  query GetArticles {
    articles(
      filter: { status: { _eq: "published" } }
      sort: ["-published_at"]
      limit: 10
    ) {
      id
      title
      slug
      content
      published_at
      featured_image {
        id
        filename_download
      }
    }
  }
`;

const { data } = await fetch('https://cms.yourdomain.com/graphql', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query }),
}).then(r => r.json());

Image Transformations

// Directus transforms images on-the-fly via URL parameters
const imageUrl = `https://cms.yourdomain.com/assets/${image_id}?` +
  `width=800&height=400&fit=cover&format=webp&quality=85`;

Step 6: Directus Flows — No-Code Automation

Flows let non-developers create automations via a visual editor:

  • Trigger: “When an article is created”
  • Action 1: Send a Slack notification
  • Action 2: Trigger a Vercel/Netlify deploy webhook to rebuild the frontend

Configure in Settings → Flows → Create Flow → select trigger → add operations.

Updating Directus

<code">cd /opt/directus
docker compose pull
docker compose up -d

Getting Started

Directus runs comfortably on 1–2 GB RAM. Ubuntu VPS plans at VPS.DO with NVMe storage handle Directus alongside your frontend application on the same server. The PostgreSQL backend ensures data durability and supports complex queries across related content collections.

Conclusion

Self-hosted Directus on a VPS delivers a production-grade headless CMS with auto-generated REST and GraphQL APIs, visual content management for editors, granular role-based permissions, and image transformation — all features that cost $15–$99/month on managed CMS platforms. The Docker Compose setup deploys in minutes and updates with a docker compose pull. Any frontend framework (Next.js, Nuxt, Astro, SvelteKit) integrates via standard HTTP API calls.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!