Self-Hosting Directus: A Complete Guide to Running an Open-Source Headless CMS

Directus is an open-source headless CMS and data platform that turns SQL databases into APIs, dashboards, and editorial workflows. This guide walks through self-hosting Directus on Ubuntu 24.04 LTS with Docker Compose, PostgreSQL/PostGIS, Redis, Nginx, HTTPS, backups, and day-two operational checks.

 Directus self-hosted headless CMS workspace

Caption: Directus adds an admin studio, APIs, permissions, files, and automation around your own SQL database while staying deployable on a small VPS.

Introduction

Directus is an open-source data platform and headless CMS for teams that want an admin interface and APIs on top of a SQL database. Instead of forcing content into a proprietary storage model, Directus connects to a database, reads the schema, and gives editors, developers, and operators a browser-based Studio, REST and GraphQL APIs, role-based permissions, file handling, flows, webhooks, and extension points.

Self-hosting Directus is a strong fit when your application data, editorial content, product catalog, internal tools, or client records should remain inside your own infrastructure. A developer can model tables directly in PostgreSQL, an editor can work through the Studio, and a frontend can consume the generated APIs from a website, mobile app, or server-side integration. You also control the upgrade schedule, backups, retention policy, asset storage, and network boundary instead of pushing every content workflow into a hosted SaaS account.

This guide installs Directus on Ubuntu 24.04 LTS with Docker Compose. The stack uses the official Directus image, PostgreSQL with the PostGIS image recommended in Directus' deployment example, Redis for cache, local folders for uploads and extensions, Nginx for HTTPS, and a private localhost port between Nginx and the Directus container. By the end, you will have a draft-ready production baseline at https://directus.example.com, a first admin account, a backup routine, and checks for verifying Studio, API, database, cache, and uploads.

Why Choose Directus?

  • Database-first CMS: Directus works with your SQL schema instead of hiding data in an opaque content store.
  • REST and GraphQL APIs: Collections become API endpoints that frontends and internal tools can consume quickly.
  • Editor-friendly Studio: Non-developers can manage content, files, relationships, dashboards, and workflows through a clean interface.
  • Role-based access control: Permissions can be shaped around teams, collections, fields, actions, and API access patterns.
  • Docker-ready deployment: The official image is designed for Compose, Kubernetes, and platform deployments with environment variables.
  • Extensible architecture: Custom extensions, flows, webhooks, and operation hooks let you adapt Directus without forking it.
  • Open-source ownership: You can run the core tier yourself, keep data close to your applications, and review upgrades before applying them.

Directus is not a replacement for understanding your database. Schema design, indexes, backups, migrations, and security remain your responsibility. Treat Directus as the API and operations layer around a well-managed database, not as a shortcut around database administration.

Prerequisites

Hardware Recommendations:

  • 2 CPU cores and 2 GB RAM for a small editorial or internal tools deployment
  • 4 CPU cores and 4-8 GB RAM for larger file libraries, many API users, or heavy automation flows
  • 30 GB free disk space to start, plus room for PostgreSQL data, uploads, extensions, logs, and backups
  • SSD-backed storage for PostgreSQL and uploaded assets
  • Off-server backup storage such as another VPS, object storage bucket, NAS, or encrypted backup service

Software and Accounts:

  • Ubuntu 24.04 LTS server with sudo access
  • A domain such as directus.example.com
  • DNS A or AAAA record pointing that hostname to the server
  • Docker Engine and Docker Compose v2
  • Nginx, Certbot, OpenSSL, curl, jq, and tar
  • SMTP credentials if Directus will send password reset or invitation emails
  • A password manager for generated secrets, admin credentials, and recovery notes

Security Notes:

  • Bind Directus to 127.0.0.1:8055 and publish only Nginx on ports 80 and 443.
  • Pin the Directus image version for production so restarts do not unexpectedly upgrade the application.
  • Generate a unique SECRET, database password, and temporary admin password before the first boot.
  • Keep .env out of public Git repositories because it contains authentication and database credentials.
  • Back up the database, uploads, extensions, and Compose files together.
  • Review Directus release notes before upgrades, especially when moving across major versions.

Start with an updated host and a restrictive firewall:

sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl jq gnupg openssl ufw nginx certbot python3-certbot-nginx tar

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

Installation Guide

This deployment keeps all working files under /opt/directus. Docker Compose starts three services: Directus, PostgreSQL/PostGIS, and Redis. Nginx runs on the host and forwards HTTPS traffic to the local Directus port.

1. Install Docker Engine

Install Docker and verify that the Compose plugin is available:

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
newgrp docker

docker --version
docker compose version

If newgrp docker does not update your shell, log out and back in before continuing. Running Compose without sudo keeps the deployment directory easier to operate and back up.

2. Create the Project Directory

Create folders for the Compose project, uploads, extensions, database files, Redis persistence, and local backup staging:

sudo mkdir -p /opt/directus/{uploads,extensions,data/database,data/redis,backups}
sudo chown -R "$USER":"$USER" /opt/directus
chmod 700 /opt/directus
chmod 700 /opt/directus/backups
cd /opt/directus

Keep /opt/directus private. The .env file created in the next step contains credentials that should not be readable by other users on the server.

3. Generate Secrets and Create .env

Directus requires a long SECRET for signing tokens. This example also generates a PostgreSQL password and an initial admin password for scripted first boot:

cd /opt/directus

DIRECTUS_SECRET_VALUE="$(openssl rand -hex 32)"
DIRECTUS_DB_PASSWORD_VALUE="$(openssl rand -hex 24)"
DIRECTUS_ADMIN_PASSWORD_VALUE="$(openssl rand -base64 36 | tr -d '\n')"

cat > .env <<EOF
DIRECTUS_VERSION=12.0.2
DIRECTUS_DOMAIN=directus.example.com
PUBLIC_URL=https://directus.example.com
SECRET=${DIRECTUS_SECRET_VALUE}
DB_PASSWORD=${DIRECTUS_DB_PASSWORD_VALUE}
ADMIN_EMAIL=admin@directus.example.com
ADMIN_PASSWORD=${DIRECTUS_ADMIN_PASSWORD_VALUE}
LICENSE_KEY=
EOF

chmod 600 .env
printf 'Initial Directus admin password: %s\n' "${DIRECTUS_ADMIN_PASSWORD_VALUE}"

Save the generated admin password in a password manager. Change it after the first login and remove scripted admin credentials from future automation if you manage users manually through the Studio.

4. Create compose.yaml

Create a Compose file based on Directus' official production example. It pins the Directus version, waits for healthy PostgreSQL and Redis services, persists uploads and extensions, and exposes Directus only on localhost:

services:
  database:
    image: postgis/postgis:13-master
    restart: unless-stopped
    volumes:
      - ./data/database:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: "directus"
      POSTGRES_PASSWORD: "${DB_PASSWORD}"
      POSTGRES_DB: "directus"
    healthcheck:
      test: ["CMD", "pg_isready", "--host=localhost", "--username=directus"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_interval: 5s
      start_period: 30s

  cache:
    image: redis:6
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - ./data/redis:/data
    healthcheck:
      test: ["CMD-SHELL", "[ $$(redis-cli ping) = 'PONG' ]"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_interval: 5s
      start_period: 30s

  directus:
    image: directus/directus:${DIRECTUS_VERSION}
    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_healthy
    healthcheck:
      test: ["CMD-SHELL", "wget --spider -q http://localhost:8055/server/ping || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_interval: 5s
      start_period: 30s
    environment:
      SECRET: "${SECRET}"
      DB_CLIENT: "pg"
      DB_HOST: "database"
      DB_PORT: "5432"
      DB_DATABASE: "directus"
      DB_USER: "directus"
      DB_PASSWORD: "${DB_PASSWORD}"
      CACHE_ENABLED: "true"
      CACHE_AUTO_PURGE: "true"
      CACHE_STORE: "redis"
      REDIS: "redis://cache:6379"
      ADMIN_EMAIL: "${ADMIN_EMAIL}"
      ADMIN_PASSWORD: "${ADMIN_PASSWORD}"
      PUBLIC_URL: "${PUBLIC_URL}"
      LICENSE_KEY: "${LICENSE_KEY}"
      WEBSOCKETS_ENABLED: "true"

The official Directus documentation notes that PUBLIC_URL must match the URL where the project is accessed because it is used for generated links, asset URLs, redirects, and licensing. Set it correctly before first production use.

 Directus Docker Compose stack

Caption: The Directus container stays private on localhost while PostgreSQL, Redis, uploads, and extensions live in persistent project folders.

5. Start Directus

Validate the Compose file, pull images, start the stack, and inspect service health:

cd /opt/directus

docker compose config
docker compose pull
docker compose up -d
docker compose ps

Watch the logs during the first boot. Directus initializes the database schema, creates the initial admin user when ADMIN_EMAIL and ADMIN_PASSWORD are set, and starts the HTTP server on port 8055:

docker compose logs --since=10m database cache directus
curl -i http://127.0.0.1:8055/server/ping

A healthy basic ping returns pong. Directus also provides /server/health for dependency-aware checks, but the official docs recommend /server/ping for lightweight liveness checks.

6. Configure Nginx and HTTPS

Create /etc/nginx/sites-available/directus.example.com:

server {
    listen 80;
    listen [::]:80;
    server_name directus.example.com;

    client_max_body_size 100m;

    location / {
        proxy_pass http://127.0.0.1:8055;
        proxy_http_version 1.1;
        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_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 300;
    }
}

Enable the site and request a certificate:

sudo ln -s /etc/nginx/sites-available/directus.example.com /etc/nginx/sites-enabled/directus.example.com
sudo nginx -t
sudo systemctl reload nginx

sudo certbot --nginx -d directus.example.com
sudo certbot renew --dry-run

After Certbot updates the server block, browse to https://directus.example.com/admin and sign in with the generated admin credentials.

Configuration

After the first login, complete these configuration tasks before inviting other users:

  • Change the admin password from the Studio user menu and store the new value securely.
  • Configure email if you need invitations, password resets, or workflow notifications. Add the SMTP environment variables documented by Directus, then restart the stack.
  • Create roles before users so editors, developers, API clients, and administrators receive only the permissions they need.
  • Review file storage. Local uploads are simple on one VPS, but high-traffic or multi-node deployments should move assets to an external storage driver.
  • Keep extensions deliberate. Mount custom extensions under extensions, version them in a private repository, and test them before production use.
  • Document schema changes. Directus can edit schema through the Studio, but production teams should still track migrations or schema exports.

Restart Directus after editing .env:

cd /opt/directus
docker compose up -d
docker compose logs --since=5m directus

Usage

Start with one small collection so you can test the full workflow from schema to API response.

1. Create a Collection

In the Studio, open Settings > Data Model, create a collection named articles, and add fields such as:

  • title as a string
  • slug as a unique string
  • status as a select field with draft and published values
  • body as rich text or markdown
  • publish_on as a datetime field

 Directus data model planning

Caption: A simple articles collection gives editors a clean Studio form while Directus exposes the same records through generated APIs.

2. Configure Roles and API Access

Create an Editor role that can create and update articles, then create a Public API role with read-only access to published articles. Keep private fields hidden from public roles.

If you create a static token for a server-side integration, store it outside frontend code. Test the API from the server:

read -rsp "Directus API token: " DIRECTUS_TOKEN
printf '\n'

curl -sS \
  -H "Authorization: Bearer ${DIRECTUS_TOKEN}" \
  "https://directus.example.com/items/articles?limit=5" | jq

For public content, prefer field filters and status filters so frontends do not receive drafts or internal notes.

3. Verify Files and Realtime

Upload a small image through the Studio, confirm it appears in uploads, and test a file URL from the browser. If you enabled realtime features, keep Nginx websocket headers in place and test from the client that needs realtime updates.

Troubleshooting

  • Directus container exits during boot: Run docker compose logs directus database. Most first-boot failures come from a malformed .env, mismatched DB_PASSWORD, or an unhealthy database container.
  • Browser redirects to the wrong URL: Check PUBLIC_URL in .env, then run docker compose up -d. The value should be the full external HTTPS URL.
  • Uploads fail or disappear after restart: Confirm ./uploads:/directus/uploads is mounted and that the host directory is owned by the deployment user.
  • Nginx returns 502: Check docker compose ps, curl http://127.0.0.1:8055/server/ping, and Nginx error logs with sudo journalctl -u nginx --since=10m.
  • API requests work for admins but not public users: Review role permissions, field-level access, item filters, and whether the collection is enabled for the role.
  • Cache warnings appear in logs: Verify CACHE_ENABLED, CACHE_STORE, and REDIS=redis://cache:6379, then confirm docker compose ps cache is healthy.
  • Certbot cannot issue a certificate: Check DNS, firewall rules, and whether another default Nginx site is intercepting the hostname.

Scaling, Securing, and Next Steps

Start with reliable backups before adding complexity. A simple backup captures PostgreSQL, uploads, extensions, .env, and compose.yaml:

cd /opt/directus
mkdir -p backups

BACKUP_DATE="$(date +%F-%H%M)"
docker compose exec -T database pg_dump -U directus directus > "backups/directus-${BACKUP_DATE}.sql"
tar -czf "backups/directus-files-${BACKUP_DATE}.tar.gz" uploads extensions .env compose.yaml
ls -lh backups

Copy the backup files off the server and test restores on a separate machine. A restore usually means stopping the stack, restoring the database dump into PostgreSQL, unpacking uploads and extensions, checking .env, and starting Directus again.

For production hardening, add monitoring around container health, disk usage, backup freshness, TLS renewal, and Directus error logs. If editors upload large assets, consider object storage instead of local files. If API traffic grows, move PostgreSQL to a managed or dedicated database host, add connection limits, keep Redis close to Directus, and benchmark high-volume endpoints before exposing them publicly.

 Directus operations checklist

Caption: Reliable Directus operations depend on version pinning, backups, role reviews, health checks, and planned upgrade windows.

Conclusion

Following this guide gives you a self-hosted Directus instance with a pinned Docker image, PostgreSQL-backed data, Redis caching, persistent uploads, HTTPS through Nginx, and a repeatable backup process. The immediate outcome is a private headless CMS that can serve editors through the Studio and developers through generated APIs. From here, build a small content model, connect one frontend, document your roles, and practice a restore before treating the deployment as production-critical.

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.