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
AorAAAArecord 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:8055and publish only Nginx on ports80and443. - 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
.envout 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.
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:
titleas a stringslugas a unique stringstatusas a select field with draft and published valuesbodyas rich text or markdownpublish_onas a datetime field
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, mismatchedDB_PASSWORD, or an unhealthy database container. - Browser redirects to the wrong URL: Check
PUBLIC_URLin.env, then rundocker compose up -d. The value should be the full external HTTPS URL. - Uploads fail or disappear after restart: Confirm
./uploads:/directus/uploadsis 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 withsudo 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, andREDIS=redis://cache:6379, then confirmdocker compose ps cacheis 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.
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.