How to Install Directus on Ubuntu 24.04 VPS: Open-Source Headless CMS and Data Platform
Directus is an open-source headless CMS and instant-REST/GraphQL data platform that wraps any SQL database in a modern, permission-aware API and a polished no-code admin app. Unlike traditional CMSes that force you into their content model, Directus introspects your existing (or new) Postgres, MySQL, SQLite, or MSSQL schema and exposes it through REST, GraphQL, and WebSocket endpoints -- while giving editors a friendly UI to manage the data. This guide walks through a production install on Ubuntu 24.04, from a clean VPS to a hardened deployment with PM2 clustering, Nginx TLS, S3-compatible object storage, Flows automation, Webhooks, and custom Extensions.
Recommended Plan: CloudCore Professional -- 6 vCPU, 12 GB RAM, 100 GB NVMe for EUR 19.99/month handles hundreds of concurrent editors and thousands of API requests per minute.
Table of Contents
What is Directus?
Directus is an open-source (BSL -> GPLv3 after 4 years) headless data platform that turns any SQL database into a full API and admin interface in minutes. Unlike WordPress, Strapi, or Ghost -- which own their schema and hide the database -- Directus treats your database as the source of truth. Point it at a brand new Postgres instance and it generates tables through its UI; point it at an existing warehouse with 200 legacy tables and it instantly produces a REST/GraphQL API, permission model, and admin app around them.
The platform is a Node.js application (TypeScript, Koa, Knex) with a Vue 3 admin frontend. It speaks every major SQL dialect (PostgreSQL, MySQL/MariaDB, SQLite, Microsoft SQL Server, Oracle, CockroachDB) and automatically introspects columns, foreign keys, indexes, and constraints. Editors work in a dashboard that supports relational field types (many-to-one, many-to-many, many-to-any), a rich WYSIWYG, a block editor, file libraries with on-demand image transformations, translations, revisions, versions, and a visual schema builder. Developers get typed SDKs for JavaScript, GraphQL, and REST, plus a plugin system for custom endpoints, hooks, interfaces, layouts, modules, and operations.
Common use cases include powering Next.js/Nuxt/SvelteKit marketing sites, acting as the backend for mobile apps, managing product catalogs for e-commerce storefronts, running as an internal tool for ops teams, centralizing IoT telemetry, and serving as a lightweight data platform in place of Retool or Airtable. The open-source edition is production-grade -- it is the same codebase Directus Cloud runs, with no feature gates on realtime, flows, SSO (via SAML/OIDC extensions), or extensions.
If you are weighing Directus against Strapi, see our Strapi install guide -- Strapi is Node-native and opinionated about its schema, while Directus is database-native and opinionated about nothing.
Why Self-Host Directus Instead of Directus Cloud?
Directus Cloud is a fine managed option, but self-hosting on your own VPS delivers concrete advantages for most teams:
- Flat, predictable pricing. Directus Cloud charges per project tier and scales cost with active users and storage. A EUR 19.99/month CloudCore Professional VPS runs unlimited projects, unlimited admin users, unlimited API traffic, and hundreds of GB of assets with no per-seat or per-request overage.
- Full database ownership. Your Postgres instance lives on a disk you control. Take dumps whenever you want, replicate to warehouses, point analytics tools at it, run raw SQL migrations, add custom triggers -- no ticket to support required.
- No asset egress fees. Cloud platforms meter bandwidth on uploaded images and files. A self-hosted Directus on a VPS with unmetered bandwidth serves terabytes per month at no additional cost.
- Custom extensions without gatekeeping. Self-hosted Directus accepts any extension (endpoints, hooks, interfaces, modules, operations, panels, themes, bundles) -- including private NPM packages or TypeScript written in-house. Cloud has a sandboxed extension marketplace with narrower APIs.
- Data residency and compliance. Pick the region and jurisdiction. For GDPR, HIPAA BAAs, or UK/EU data-residency clauses, self-hosting on a known server is far simpler than negotiating a DPA.
- Run alongside the rest of your stack. Directus on the same VPS as your Next.js frontend, your Redis cache, and your Postgres means zero-latency internal API calls -- no public round-trip to a SaaS.
- No lock-in. Export/import is trivial: the schema IS the database. If Directus ever disappears, your data is already in plain Postgres tables ready for any other tool.
Directus Cloud vs Self-Hosted: Cost Comparison
| Scenario | Directus Cloud (Team) | Directus Cloud (Business) | Self-Hosted VPS |
|---|---|---|---|
| Starting price | ~$99/mo | ~$599/mo | EUR 19.99/mo |
| Admin users included | 10 | 25 | Unlimited |
| API requests | Metered | Metered | Unlimited |
| Asset storage | 50 GB | 250 GB | 100 GB NVMe (expandable) |
| Custom extensions | Marketplace only | Sandboxed | Any code, any runtime |
| Database access | Managed (read-only UI) | Managed | Full root |
| Backups | Automatic (retained 7-30d) | Automatic (retained 30d) | You own the schedule |
Prerequisites
Before you start, have these ready:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access (terminal on macOS/Linux, or PuTTY/Windows Terminal on Windows)
- At least 2 vCPU and 4 GB of RAM (4 vCPU / 8 GB recommended for production editorial teams)
- A domain name pointing an A record at your server IP (for TLS)
- Basic familiarity with Linux commands, Node.js, and PostgreSQL
Recommended Plan: CloudCore Professional>
For a production Directus deployment serving a small-to-mid editorial team plus frontend traffic, we recommend CloudCore Professional:>
- 6 vCPU cores
- 12 GB RAM
- 100 GB NVMe SSD
- Unmetered bandwidth
- EUR 19.99/month>
This comfortably runs PostgreSQL 16 + Directus in 4-node cluster mode + Redis + Nginx on the same machine. You can scale horizontally later by moving Postgres and object storage onto dedicated VPS instances.
Connect to your server:
ssh root@your-server-ipStep 1: Update the System and Create a Service User
Refresh the package index and apply pending upgrades before installing anything:
sudo apt update && sudo apt upgrade -yInstall a few utilities you will need later:
sudo apt install -y curl ca-certificates gnupg lsb-release ufw git build-essentialCreate a dedicated non-root user to own the Directus process -- never run Directus as root:
sudo adduser --system --group --home /opt/directus --shell /bin/bash directus
sudo mkdir -p /opt/directus
sudo chown -R directus:directus /opt/directusOpen a basic firewall allowing SSH, HTTP, and HTTPS:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableStep 2: Install Node.js 20 LTS
Directus 10+ requires Node.js >= 18; Directus 11 recommends Node.js 20 LTS. Use the official NodeSource repository so you get security patches via apt:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejsVerify:
node --version
npm --versionExpected output:
v20.18.0
10.8.2Install PM2 globally (we will use it for clustering in Step 7):
sudo npm install -g pm2For a deeper dive on multiple Node versions, nvm, and production tuning, see our Node.js install guide.
Step 3: Install and Configure PostgreSQL 16
PostgreSQL is the recommended database for Directus -- it supports JSONB, full-text search, and the geometric types Directus uses for its map interfaces. Install PostgreSQL 16 from the official PGDG repo:
sudo install -d /usr/share/postgresql-common/pgdg sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \ --fail https://www.postgresql.org/media/keys/ACCC4CF8.ascecho "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] \ https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | \ sudo tee /etc/apt/sources.list.d/pgdg.list
sudo apt update sudo apt install -y postgresql-16 postgresql-contrib-16
Confirm the service is active:
sudo systemctl status postgresqlCreate the Directus database and role. Pick a strong random password:
DIRECTUS_DB_PASS=$(openssl rand -base64 24) echo "Directus DB password: $DIRECTUS_DB_PASS"
sudo -u postgres psql <<EOF CREATE USER directus WITH PASSWORD '$DIRECTUS_DB_PASS'; CREATE DATABASE directus OWNER directus ENCODING 'UTF8'; GRANT ALL PRIVILEGES ON DATABASE directus TO directus; \c directus GRANT ALL ON SCHEMA public TO directus; EOF
Copy the generated password somewhere safe -- you will paste it into the .env file in Step 5.
Verify connectivity from the directus system user:
sudo -u directus psql -h 127.0.0.1 -U directus -d directus -c '\conninfo'Enter the password when prompted; you should see a connection confirmation. For more detailed Postgres tuning (shared_buffers, work_mem, WAL archiving), see our PostgreSQL install guide.
Step 4: Bootstrap a Directus Project
Switch to the service user and scaffold a project with the official generator:
sudo -iu directus
cd /opt/directus
npm init directus-project@latest appThe wizard asks:
PostgreSQL / Redshift (pg).127.0.0.15432directusdirectus[email protected])Expected final output:
Your project has been created at /opt/directus/app.
Start Directus by running: cd app npx directus start
The generator creates /opt/directus/app/ containing:
app/
.env
package.json
node_modules/
uploads/
extensions/
database/Do not npx directus start yet -- we will adjust .env first.
Step 5: Configure the .env File
Edit /opt/directus/app/.env and replace or append the following block. The KEY and SECRET values must be long random strings; regenerating them invalidates existing sessions and refresh tokens.
cd /opt/directus/app cp .env .env.backup
KEY=$(openssl rand -hex 16) SECRET=$(openssl rand -hex 32) echo "KEY=$KEY" echo "SECRET=$SECRET"
Open the file:
nano /opt/directus/app/.envPaste and adapt (the password is the one you generated in Step 3):
# ---------- Server ----------
HOST="127.0.0.1"
PORT="8055"
PUBLIC_URL="https://directus.yourdomain.com"
LOG_LEVEL="info"---------- Security ----------
KEY="<paste-the-KEY-value-here>"
SECRET="<paste-the-SECRET-value-here>"
ACCESS_TOKEN_TTL="15m"
REFRESH_TOKEN_TTL="7d"
REFRESH_TOKEN_COOKIE_SECURE="true"
REFRESH_TOKEN_COOKIE_SAME_SITE="lax"
REFRESH_TOKEN_COOKIE_NAME="directus_refresh_token"---------- Database ----------
DB_CLIENT="pg"
DB_HOST="127.0.0.1"
DB_PORT="5432"
DB_DATABASE="directus"
DB_USER="directus"
DB_PASSWORD="<paste-the-db-password-from-step-3>"
DB_SSL__REJECT_UNAUTHORIZED="false"---------- Cache (optional, Redis) ----------
CACHE_ENABLED="true"
CACHE_STORE="redis"
CACHE_REDIS="redis://127.0.0.1:6379"
CACHE_AUTO_PURGE="true"---------- Realtime / WebSockets ----------
WEBSOCKETS_ENABLED="true"
WEBSOCKETS_REST_ENABLED="true"
WEBSOCKETS_GRAPHQL_ENABLED="true"
WEBSOCKETS_HEARTBEAT_ENABLED="true"
WEBSOCKETS_HEARTBEAT_PERIOD="30"---------- CORS ----------
CORS_ENABLED="true"
CORS_ORIGIN="https://yourfrontend.com,https://admin.yourdomain.com"
CORS_CREDENTIALS="true"---------- Rate Limiting ----------
RATE_LIMITER_ENABLED="true"
RATE_LIMITER_STORE="memory"
RATE_LIMITER_POINTS="50"
RATE_LIMITER_DURATION="1"---------- Email (adjust to your SMTP) ----------
EMAIL_FROM="[email protected]"
EMAIL_TRANSPORT="smtp"
EMAIL_SMTP_HOST="smtp.yourprovider.com"
EMAIL_SMTP_PORT="587"
EMAIL_SMTP_USER="<smtp-user>"
EMAIL_SMTP_PASSWORD="<smtp-password>"
EMAIL_SMTP_SECURE="false"---------- Admin Bootstrap (used only on first boot) ----------
ADMIN_EMAIL="[email protected]"
ADMIN_PASSWORD="<strong-initial-password>"Lock file permissions so no other user can read secrets:
chmod 600 /opt/directus/app/.envOptional: install Redis for caching
If you enabled CACHE_STORE="redis" above, install Redis:
exit # leave the directus user
sudo apt install -y redis-server
sudo systemctl enable --now redis-server
sudo -iu directus
cd /opt/directus/appStep 6: Initialize the Database and Admin User
Run Directus once in the foreground to create its system tables and seed the admin user. The CLI reads ADMIN_EMAIL and ADMIN_PASSWORD from .env on first boot:
cd /opt/directus/app
npx directus bootstrapExpected output (abbreviated):
[17:02:11.445] INFO: Bootstrapping Directus...
[17:02:11.821] INFO: Database Installing...
[17:02:14.903] INFO: Database migrating...
[17:02:16.102] INFO: Running migrations...
[17:02:18.433] INFO: Setting up first project...
[17:02:18.901] INFO: Creating default admin user...
[17:02:19.110] INFO: Done.Start Directus briefly to confirm it boots cleanly:
npx directus startYou should see:
[17:03:01.232] INFO: Server started at http://127.0.0.1:8055In another SSH session, hit the healthcheck:
curl http://127.0.0.1:8055/server/healthExpected output:
{"status":"ok"}Stop the foreground server with Ctrl+C. You are ready to run it as a supervised service.
Step 7: Run Directus with PM2 in Cluster Mode
PM2 keeps Directus alive across crashes and lets you use all CPU cores through Node cluster mode -- a single Directus process is bound to one core, so clustering is the fastest way to scale throughput on a multi-core VPS.
Still as the directus user, create a PM2 ecosystem file at /opt/directus/app/ecosystem.config.cjs:
module.exports = {
apps: [
{
name: "directus",
script: "node_modules/directus/cli.js",
args: "start",
cwd: "/opt/directus/app",
instances: "max", // one worker per CPU core
exec_mode: "cluster",
max_memory_restart: "1G",
env: {
NODE_ENV: "production",
},
out_file: "/opt/directus/app/logs/out.log",
error_file: "/opt/directus/app/logs/err.log",
merge_logs: true,
time: true,
},
],
};Create the log directory and launch:
mkdir -p /opt/directus/app/logs
pm2 start /opt/directus/app/ecosystem.config.cjs
pm2 saveExpected output:
[PM2] Starting ... in cluster_mode (6 instances)
[PM2] Done.
┌────┬─────────────┬──────┬──────┬──────────┬────────┐
│ id │ name │ mode │ ↺ │ status │ cpu │
├────┼─────────────┼──────┼──────┼──────────┼────────┤
│ 0 │ directus │ clust│ 0 │ online │ 0.5% │
│ 1 │ directus │ clust│ 0 │ online │ 0.4% │
│ 2 │ directus │ clust│ 0 │ online │ 0.5% │
│ 3 │ directus │ clust│ 0 │ online │ 0.3% │
│ 4 │ directus │ clust│ 0 │ online │ 0.4% │
│ 5 │ directus │ clust│ 0 │ online │ 0.4% │
└────┴─────────────┴──────┴──────┴──────────┴────────┘Leave the directus shell (exit) and, as a user with sudo, register PM2 as a systemd service so it starts on boot:
exit
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u directus --hp /opt/directus
sudo systemctl enable pm2-directusVerify:
sudo systemctl status pm2-directusStep 8: Alternative - systemd Unit
If you prefer a plain systemd unit over PM2 (single-process, less tooling, easier for Kubernetes/Nomad migrations later), skip Step 7 and use this unit file instead.
Create /etc/systemd/system/directus.service:
[Unit] Description=Directus Headless CMS After=network.target postgresql.service Wants=postgresql.service[Service] Type=simple User=directus Group=directus WorkingDirectory=/opt/directus/app ExecStart=/usr/bin/node node_modules/directus/cli.js start EnvironmentFile=/opt/directus/app/.env Restart=on-failure RestartSec=5 StandardOutput=append:/opt/directus/app/logs/out.log StandardError=append:/opt/directus/app/logs/err.log NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/opt/directus/app
[Install] WantedBy=multi-user.target
Enable and start:
sudo mkdir -p /opt/directus/app/logs
sudo chown -R directus:directus /opt/directus/app/logs
sudo systemctl daemon-reload
sudo systemctl enable --now directus
sudo systemctl status directusNote: this runs a single Node process. To cluster under systemd you would template the unit ([email protected], [email protected], etc.) behind Nginx upstreams, which is more work than just using PM2.
Step 9: Nginx Reverse Proxy with TLS
Directus listens on 127.0.0.1:8055 and should never be exposed directly. Put Nginx in front to terminate TLS, support HTTP/2, and upgrade WebSocket connections.
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginxCreate /etc/nginx/sites-available/directus:
upstream directus_backend { server 127.0.0.1:8055; keepalive 32; }server { listen 80; server_name directus.yourdomain.com; return 301 https://$host$request_uri; }
server { listen 443 ssl http2; server_name directus.yourdomain.com;
# Certbot will fill these in ssl_certificate /etc/letsencrypt/live/directus.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/directus.yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off;
# Security headers add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; add_header Referrer-Policy strict-origin-when-cross-origin always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Uploads: bump for your largest expected asset client_max_body_size 200m;
# Gzip API responses gzip on; gzip_types application/json application/javascript text/css text/plain image/svg+xml; gzip_min_length 1024;
location / { proxy_pass http://directus_backend; proxy_http_version 1.1;
# WebSocket / realtime upgrade 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_set_header X-Forwarded-Host $host;
proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_buffering off; } }
Enable the site and obtain a certificate:
sudo ln -s /etc/nginx/sites-available/directus /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d directus.yourdomain.comCertbot installs a systemd timer that renews the certificate automatically every 60 days.
Visit https://directus.yourdomain.com/admin in a browser. Log in with the admin credentials from Step 5. You should see the Directus dashboard; create your first Collection to confirm writes reach Postgres.
Step 10: Configure S3 or MinIO Object Storage
By default Directus writes uploads to /opt/directus/app/uploads/. For production, store assets in S3, Cloudflare R2, Backblaze B2, Wasabi, or a self-hosted MinIO so the Directus VPS remains stateless and you can run multiple replicas.
Stop Directus:
sudo -iu directus
pm2 stop directus # or: sudo systemctl stop directusAppend this block to /opt/directus/app/.env for an S3-compatible backend (works for AWS S3, R2, Wasabi, Backblaze, MinIO):
# ---------- Storage ----------
STORAGE_LOCATIONS="s3"
STORAGE_S3_DRIVER="s3"
STORAGE_S3_KEY="<access-key-id>"
STORAGE_S3_SECRET="<secret-access-key>"
STORAGE_S3_BUCKET="your-directus-assets"
STORAGE_S3_REGION="us-east-1"
STORAGE_S3_ENDPOINT="https://s3.us-east-1.amazonaws.com"
STORAGE_S3_FORCE_PATH_STYLE="false"
STORAGE_S3_ACL="private"For MinIO running on the same VPS (or another host), use:
STORAGE_LOCATIONS="minio"
STORAGE_MINIO_DRIVER="s3"
STORAGE_MINIO_KEY="minioadmin"
STORAGE_MINIO_SECRET="<your-minio-password>"
STORAGE_MINIO_BUCKET="directus"
STORAGE_MINIO_REGION="us-east-1"
STORAGE_MINIO_ENDPOINT="https://minio.yourdomain.com"
STORAGE_MINIO_FORCE_PATH_STYLE="true"See the MinIO install guide if you need to stand up MinIO first.
Optionally migrate existing local files to the bucket before restarting. If the uploads/ directory has files:
# From a machine with AWS CLI configured
aws s3 sync /opt/directus/app/uploads s3://your-directus-assets/Restart Directus:
pm2 restart directus # or: sudo systemctl restart directusUpload a new file via the admin UI and confirm it lands in the bucket (aws s3 ls s3://your-directus-assets/). Directus stores only a reference; the asset itself is served through its /assets/<uuid> endpoint which proxies and transforms the object on the fly.
Step 11: Flows, Webhooks, and Extensions
The three mechanisms below cover 95% of "make Directus do something custom" scenarios.
Flows
Flows are Directus's no-code automation system (similar to Zapier, n8n, or GitHub Actions). They live in the admin UI under Settings -> Flows and consist of a trigger plus chained operations. Typical flows:
- Publish webhook on item create -- Trigger: Event Hook on
articles.items.create. Operation: Webhook (POST) to your frontend's ISR revalidate endpoint. - Send Slack notification on form submit -- Trigger: Event Hook on
leads.items.create. Operations: Condition (checkpriority == high) -> Webhook (Slack incoming webhook URL). - Nightly cleanup -- Trigger: Schedule (cron
0 3 *). Operation: Run Script -> delete items wherestatus == 'archived'andupdated > 90 days. - Approval workflow -- Trigger: Manual (button in admin UI). Operations: Condition (check user role) -> Update item (
status = 'approved') -> Send Email.
directus_flows and directus_operations tables. They run inside the Directus process, so long-running scripts should be offloaded to a webhook that fires a job on a worker.Webhooks
For simple fire-and-forget HTTP callbacks, use Settings -> Webhooks in the admin UI. Specify method, URL, headers, and which collections/actions trigger the call. Example: fire a POST https://yourapp.com/api/revalidate on every pages.items.update so your Next.js ISR cache invalidates.
Webhooks are synchronous by default. For high-traffic collections, wrap them in a Flow that uses the Run Script operation to enqueue to a proper message queue (RabbitMQ, Redis streams) and return immediately.
Extensions
Extensions are npm packages (or local folders) that extend Directus with custom code. They live in /opt/directus/app/extensions/ and ship with a package.json and a compiled dist/ folder.
Extension types:
- Endpoints -- new custom REST routes (e.g.
/custom/export-csv) - Hooks -- event subscribers (filter, action, init, schedule) running server-side
- Interfaces -- custom form field UIs (e.g. a color picker, map drawer, signature pad)
- Displays -- custom read-only renderers for list and detail views
- Layouts -- custom admin list views (kanban, calendar, map)
- Modules -- whole new admin UI sections
- Operations -- custom Flow steps
- Panels -- custom dashboard widgets
- Themes -- admin UI themes
- Bundles -- groups of the above
sudo -iu directus cd /opt/directus/app/extensions npx create-directus-extension@latestchoose: hook
name: my-audit-logger
lang: typescript
Build and reload:
cd my-audit-logger
npm install
npm run build
pm2 restart directusExtensions are auto-detected on startup. For production, set EXTENSIONS_AUTO_RELOAD="false" in .env and rebuild/restart manually.
Hardening and Backups
A few things to do before you call the deployment production-ready.
Rotate the admin user. The bootstrap admin created from .env is shared knowledge in your repo or deployment playbook. Log in, create a personal admin account, then demote or delete the bootstrap user.
Enforce 2FA. In Settings -> Roles & Permissions, enable "Require TFA" on the Administrator role. Every admin then sets up TOTP on next login.
Tighten Postgres listen addresses. Ensure /etc/postgresql/16/main/postgresql.conf has listen_addresses = 'localhost' so Postgres never accepts connections from the public internet.
Enable automated Postgres backups. A simple cron with pg_dump and offsite upload works for small sites:
sudo tee /etc/cron.daily/directus-backup > /dev/null <<'EOF'
#!/bin/bash
set -e
BACKUP_DIR=/var/backups/directus
mkdir -p "$BACKUP_DIR"
STAMP=$(date +%Y%m%d-%H%M)
sudo -u postgres pg_dump -Fc directus > "$BACKUP_DIR/directus-$STAMP.dump"
find "$BACKUP_DIR" -name 'directus-*.dump' -mtime +14 -delete
Optional: push to S3
aws s3 cp "$BACKUP_DIR/directus-$STAMP.dump" s3://your-backups/directus/
EOF
sudo chmod +x /etc/cron.daily/directus-backupMonitor PM2. Install pm2 install pm2-logrotate for automatic log rotation and look at pm2 monit for real-time CPU/memory per worker.
Set resource limits. In the systemd unit (or ecosystem.config.cjs), max_memory_restart: "1G" prevents a runaway worker from OOM'ing the box.
Keep Directus updated. Subscribe to the Directus GitHub releases page. Update with:
sudo -iu directus
cd /opt/directus/app
npm install directus@latest
npx directus database migrate:latest
pm2 restart directusAlways take a database dump before upgrading across major versions.
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
EADDRINUSE: address already in use 127.0.0.1:8055 | Another process is on port 8055 | sudo lsof -i :8055 -- kill or change PORT in .env |
password authentication failed for user "directus" | DB_PASSWORD mismatch | Reset: sudo -u postgres psql -c "ALTER USER directus WITH PASSWORD 'newpw';" then update .env |
relation "directus_users" does not exist | bootstrap was not run | cd /opt/directus/app && npx directus bootstrap |
| Admin UI shows "Network Error" when logging in | PUBLIC_URL doesn't match the URL in the browser | Set PUBLIC_URL="https://directus.yourdomain.com" in .env and restart |
| File uploads succeed but preview shows broken image | STORAGE_S3_ENDPOINT or ACL is wrong | Confirm the bucket policy allows Directus to GetObject; check CORS on the bucket |
| WebSocket subscriptions silently fail | Nginx is not upgrading the connection | Verify the Upgrade / Connection: upgrade headers in Nginx config |
JWT must be provided on every request | Browser is stripping the refresh cookie | Set REFRESH_TOKEN_COOKIE_SECURE=true and use HTTPS; same SAME_SITE on frontend and Directus |
| High memory on a single PM2 worker | Memory leak in a custom extension | pm2 logs directus --lines 500; the offender is usually a hook holding references to large objects |
| Migrations fail after upgrade | Schema drift from manual SQL | Restore the pre-upgrade dump, apply changes via Directus schema API instead of raw SQL |
413 Request Entity Too Large on upload | Nginx client_max_body_size too small | Bump to 500m or higher and sudo systemctl reload nginx |
Viewing logs
# PM2
sudo -u directus pm2 logs directus --lines 200systemd alternative
sudo journalctl -u directus -fNginx
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.logPostgreSQL
sudo tail -f /var/log/postgresql/postgresql-16-main.logFAQ
Is Directus really free for commercial use?
Directus is BSL-licensed today and transitions to GPLv3 four years after each release. For 99% of self-hosted deployments -- including commercial SaaS, internal tools, and client projects -- the BSL allows free use. The only restriction is that you cannot offer Directus itself as a managed service that competes with Directus Cloud. If you build products on top of Directus, host client sites, or run it internally, you pay nothing and get every feature.
Can Directus replace Strapi, Payload, or KeystoneJS?
Yes, for most use cases, and often more cleanly. Strapi generates a content-type-builder layer that owns its own JSON schema, making it awkward to point at existing databases. Payload is TypeScript-first and code-driven -- great for small teams but requires developers for every schema change. KeystoneJS is similar. Directus is database-first: editors and developers share the same schema defined in Postgres, so DBAs, analysts, and BI tools all work with the same tables. If your team is split between devs and non-devs editing content, Directus is usually the better fit. See our Strapi guide for the alternative.
How many concurrent users can a CloudCore Professional VPS handle?
A 6 vCPU / 12 GB VPS running Directus in cluster mode, Postgres 16 locally, and Redis caching comfortably serves 50-100 concurrent admin editors plus 2000-5000 API requests per minute (cache-hit heavy). When your frontend hits Directus for published content, add aggressive caching at the Nginx layer (or Cloudflare in front) and the VPS will easily serve millions of reads per day. Scale bottlenecks first appear in Postgres -- move the DB to a dedicated server when you outgrow a single box.
Does Directus have built-in GraphQL, REST, and real-time?
Yes, all three ship in the core. Every collection you create is automatically exposed at /items/<collection> (REST), /graphql (GraphQL), and /websocket (real-time subscriptions). Permissions are applied uniformly across all three APIs, so a REST role restriction automatically applies in GraphQL and WebSockets too.
How do I give content editors less-powerful permissions than admins?
In Settings -> Roles & Permissions, create a new role (e.g. "Editor"). By default the role has no permissions. Grant read, create, update, delete per collection, with optional field-level and item-level rules (using a JSON filter expression such as { "author": { "_eq": "$CURRENT_USER" } } to scope rows to the editor's own records). Assign users to the role. The full permission model is its own product -- budget an hour or two to model your first project carefully.
How do I connect a Next.js/Nuxt/SvelteKit frontend?
Directus provides official TypeScript SDKs (@directus/sdk) that work in any JavaScript environment. For Next.js, add the SDK, create a client in lib/directus.ts with your PUBLIC_URL and an access token (from a machine-to-machine role), and call directus.request(readItems('articles')) inside getStaticProps / Server Components. Set up a Directus Webhook pointing at your Vercel revalidate endpoint so content updates invalidate ISR. For auth, use the SDK's /auth/login and refresh tokens -- Directus becomes your auth provider too.
Can Directus work with an existing production database?
Yes, that is one of its superpowers. Point .env at a database with 200 existing tables and Directus instantly generates admin UIs for all of them. Be careful with two things: (1) Directus needs a handful of directus_* system tables -- if your database already has names colliding, use a schema or prefix; (2) the first-boot migrations only add Directus's own tables and never alter your existing ones. Always take a dump first and test in staging.
Next Steps
With Directus humming on your VPS, here is where to go next:
- Connect a frontend -- Start a Next.js, Nuxt, SvelteKit, or Astro project and fetch content with
@directus/sdk. Build your first editorial site in an afternoon. - Model your data carefully -- Spend time in the schema builder before launch. Add relational fields (M2O, O2M, M2M, M2A), translations (i18n), and custom validation. Changes post-launch are doable but disruptive.
- Wire up Flows for publishing -- Add a "publish -> revalidate frontend" flow so editors never see stale content. Pair with a staging + production branching workflow using the
statusfield. - Build a first extension -- Even a trivial hook that logs every edit to a Slack channel teaches you the extension API. From there you can move into custom layouts, interfaces, and endpoints.
- Explore the official docs -- Reference for every
.envvariable, every API endpoint, and every extension type. - Add full-text search -- Install the
pg_trgmextension in Postgres and define a composite search field, or front Directus with a Meilisearch/Typesense mirror for sub-10ms queries. - Plan for scale -- When you outgrow a single VPS, split the database onto a dedicated Postgres host, move assets to S3 (already covered in Step 10), and run Directus as multiple stateless replicas behind a load balancer.
Ready to Deploy?>
Directus runs comfortably on any modern VPS. Our CloudCore Professional plan is purpose-built for Node.js applications like Directus:>
- 6 vCPU / 12 GB RAM / 100 GB NVMe
- Unmetered bandwidth
- Ubuntu 24.04 LTS pre-installed
- Full root access
- EUR 19.99/month>
Launch your VPS now and have Directus live in under 30 minutes.