Back to Blog
Tutorial10 min read2026-06-29

How to Deploy Directus Headless CMS

Directus wraps any SQL database with an instant REST and GraphQL API plus an admin app. This tutorial covers deploying Directus as a container, wiring PostgreSQL, persisting uploads, and securing it for production.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# How to Deploy Directus Headless CMS

Directus is an open-source headless CMS with a clever premise: point it at any SQL database and it instantly gives you a REST API, a GraphQL API, and a polished admin app — without dictating your schema. It works *with* your existing tables rather than imposing its own structure. This tutorial covers deploying Directus to production properly.

What makes Directus different

Most CMSes own their database schema. Directus is database-first: it introspects whatever SQL database you give it and exposes those tables through its API and admin. That makes it ideal when you want a real relational database you fully control, plus a friendly editing layer on top.

It supports PostgreSQL, MySQL, SQLite, and more. For production, PostgreSQL is the most common choice.

Step 1: the container and configuration

Directus ships an official Docker image and is configured entirely through environment variables. A minimal production config:

# conceptual env for the directus container
KEY: "<random-uuid>"             # required, stable across restarts
SECRET: "<random-secret>"        # required, stable across restarts
DB_CLIENT: "pg"
DB_CONNECTION_STRING: "${DATABASE_URL}"
ADMIN_EMAIL: "admin@example.com"
ADMIN_PASSWORD: "<strong-password>"
PUBLIC_URL: "https://cms.example.com"
PORT: "8080"

Critical: KEY and SECRET must be stable and identical across every restart and replica. They sign tokens and sessions. If they change (or differ between replicas), users get logged out and tokens break. Generate them once and store them as secrets.

Step 2: wire PostgreSQL

Directus can take a single connection string. Read it from the environment so nothing is hardcoded:

DB_CLIENT=pg
DB_CONNECTION_STRING=postgresql://user:pass@host:5432/directus

On first boot against an empty database, Directus runs its own migrations to create its system tables. After that, you build collections (tables) either through the admin UI or by managing the schema yourself.

Step 3: persist file uploads

Like most CMSes, Directus stores uploaded files. By default that is local disk — which does not survive container restarts or scaling. For production, configure object storage:

STORAGE_LOCATIONS=s3
STORAGE_S3_DRIVER=s3
STORAGE_S3_KEY=<access-key>
STORAGE_S3_SECRET=<secret>
STORAGE_S3_BUCKET=<bucket>
STORAGE_S3_REGION=<region>
STORAGE_S3_ENDPOINT=<endpoint>

This is the most common Directus production mistake — uploads vanishing after a deploy. Always externalize storage.

Step 4: the Dockerfile

Using the official image directly is usually enough:

FROM directus/directus:latest
# Add extensions by copying into /directus/extensions if needed

If you need custom extensions or hooks, copy them into the extensions directory in your build.

Step 5: scaling considerations

Directus can run multiple replicas, but two things must be shared across them:

  • The database (naturally shared).
  • File storage — must be object storage, not per-container disk.
  • Optionally Redis for caching and to coordinate rate limiting and websockets across replicas:
CACHE_ENABLED=true
CACHE_STORE=redis
REDIS=${REDIS_URL}

With those shared, you can scale Directus horizontally behind a load balancer.

Step 6: secure it

  • Set a strong admin password and rotate the bootstrap credentials after first login.
  • Configure CORS for your frontend origin.
  • Use role-based permissions in Directus to expose only the collections and fields the public API should return.
  • Put it behind HTTPS with a proper PUBLIC_URL.

Consuming the API

Once deployed, your frontend gets both REST and GraphQL for free:

# REST
curl https://cms.example.com/items/articles?filter[status][_eq]=published
# GraphQL
query { articles(filter: { status: { _eq: "published" } }) { title slug } }

Common pitfalls

  • KEY/SECRET regenerated on each deploy. Logs everyone out, breaks tokens. Pin them as secrets.
  • Local file storage. Uploads lost on restart. Use object storage.
  • Public permissions too open. By default new collections are restricted — configure roles deliberately, and do not over-expose.
  • No Redis with multiple replicas. Cache and rate limits become inconsistent.

Deploying on PandaStack

PandaStack builds the official Directus image (or your customized one) straight from a Dockerfile. Provision a managed PostgreSQL and DATABASE_URL is injected automatically — set DB_CLIENT=pg and DB_CONNECTION_STRING=${DATABASE_URL} and Directus connects on boot. Provision a managed Redis for caching when you scale, and PandaStack injects REDIS_URL too. Set KEY and SECRET as stable environment variables so tokens survive deploys, and offload uploads to object storage to handle the file-persistence gotcha. You get custom domains with automatic SSL for PUBLIC_URL, live logs, rollbacks, and managed database backups. The free tier (5 web services + 1 database) is enough to run Directus plus its PostgreSQL.

References

  • [Directus documentation](https://docs.directus.io/)
  • [Directus Docker deployment guide](https://docs.directus.io/self-hosted/docker-guide.html)
  • [Directus configuration options](https://docs.directus.io/self-hosted/config-options.html)
  • [Directus storage / file adapters](https://docs.directus.io/self-hosted/config-options.html#file-storage)

---

Directus turns any SQL database into a CMS with an instant API — the deploy is mostly stable secrets, a managed DB, and external storage. PandaStack auto-wires PostgreSQL and Redis so you can ship it fast — start free at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also