Skip to content

How to Deploy Directus on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host Directus, the headless CMS and data platform, on a VPS with Docker Compose, PostgreSQL, Redis and HTTPS. Pinned versions, generated secrets and real backups.

Before you start
  • A VPS with 1–2 GB RAM (upstream minimum 512 MB for the Directus container; 2 GB recommended)
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain you can point at the server
  • Docker Engine + Compose installed (see the base guide below)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Directus is

Directus wraps a SQL database with instant REST and GraphQL APIs and a web app called the Studio. It is often used as a headless CMS, as an internal data tool, or as the backend for an app. It doesn't import your data into its own format. It works on the tables in your database, so you can still query them directly.

That puts it in the same space as Appwrite and Supabase, but with a different focus: Directus vs Appwrite covers the trade-off. Read the licence before you commit. Directus is source-available, and the catalog lists it as MSCL-1.0, so check the current terms at directus.io if you run it commercially.

Server sizing

Directus's requirements page gives the minimum for the Directus container as 0.25 vCPU / 512 MB and a recommended minimum of 1 vCPU / 2 GB. On our test box (GCP e2-standard-2, Ubuntu 26.04), a Directus + PostgreSQL stack idled at about 313 MB of RAM and used about 2 GB of disk.

  • 1 GB RAM / 1 vCPU: works for a small project, with little headroom.
  • 2 GB RAM / 1–2 vCPU: the upstream recommendation. Choose this for real use.
  • Disk: 20 GB plus whatever you upload. Files go into the uploads volume unless you configure S3-compatible storage.

Prepare the server

This guide assumes Docker Engine and the Compose plugin are installed. If not, start with Docker & Compose on Ubuntu. Open SSH and the web ports only:

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose
Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Directus (Docker Compose)

Directus publishes a Compose example with PostgreSQL (the PostGIS image), Redis for caching, and Directus itself. The version below follows it, with these changes:

  • The image is pinned. Upstream recommends pinning a version for production, and 12.4.1 is the current release.
  • Secrets are generated instead of placeholders.
  • The port is bound to loopback.
  • Named volumes replace bind mounts, so the container's node user owns its upload directory.
  • The Directus health check calls 127.0.0.1 instead of localhost. Inside the 12.4.1 image, localhost resolved to the IPv6 address, where Directus wasn't listening. With upstream's line the container stayed unhealthy in our test even though it was serving requests.

First, the secrets. The if guard makes the block safe to run twice:

mkdir -p ~/directus && cd ~/directus
if [ ! -f .env ]; then
  cat > .env <<EOF
DB_PASSWORD=$(openssl rand -hex 24)
SECRET=$(openssl rand -hex 32)
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=$(openssl rand -base64 18 | tr -d '/+=')
PUBLIC_URL=https://directus.example.com
EOF
fi
chmod 600 .env
grep ADMIN_ .env

Write down the admin password it prints. Change ADMIN_EMAIL and PUBLIC_URL to your real values. SECRET signs access tokens, so treat it like a password.

cd ~/directus
cat > docker-compose.yml <<'YAML'
services:
  database:
    image: postgis/postgis:17-3.5
    restart: unless-stopped
    volumes:
      - db_data:/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:7
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "[ $$(redis-cli ping) = 'PONG' ]"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_interval: 5s
      start_period: 30s

  directus:
    image: directus/directus:12.4.1
    restart: unless-stopped
    ports:
      # Loopback only: Caddy is the public entry point.
      - "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://127.0.0.1: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}

volumes:
  db_data:
  uploads:
  extensions:
YAML

Bring it up and wait for the health checks:

cd ~/directus
docker compose up -d --wait
docker compose ps

The liveness endpoint answers pong once the HTTP server is running:

curl -s http://127.0.0.1:8055/server/ping; echo

ADMIN_EMAIL and ADMIN_PASSWORD are only used to create the first admin when the database is empty. Without them, Directus would show an onboarding screen for the first admin on first visit. Setting them means nobody else can claim the instance before you do. Changing them later has no effect, so change the password from the Studio instead.

HTTPS + domain

Point an A record for directus.example.com at the server, then proxy it with Caddy. The steps are in Automatic HTTPS with Caddy:

directus.example.com {
    reverse_proxy 127.0.0.1:8055
}

PUBLIC_URL must match this address exactly. Directus uses it for links in emails, for OAuth redirects and for asset URLs. Open https://directus.example.com and sign in with the admin credentials from .env.

Securing it

  • Change the admin password in the Studio after the first login, then remove ADMIN_PASSWORD from .env. It is only read at bootstrap.
  • Review the Public policy. Directus's access control has a Public role for unauthenticated requests. Grant it read access only to the collections you really want public, such as published articles for a CMS front end.
  • Use static tokens for machine clients. Give each integration its own user with the smallest set of permissions and a static token. Don't reuse your admin login.
  • Keep 8055 on loopback. Docker-published ports bypass ufw.
  • Health endpoint: /server/ping is unauthenticated and fine for an uptime monitor. /server/health checks dependencies, needs a token, and costs more to run.

Backups

Your content lives in PostgreSQL. Uploaded files live in the uploads volume. A logical dump is safe while the stack runs:

cd ~/directus
mkdir -p backups
docker compose exec -T database pg_dump -U directus -d directus | gzip > backups/directus-db-$(date +%F).sql.gz
docker run --rm -v directus_uploads:/data:ro -v "$PWD/backups":/backup alpine \
  tar czf /backup/directus-uploads-$(date +%F).tar.gz -C /data .
ls -lh backups/

Compose prefixes the volume name with the project directory. Check it with docker volume ls. Copy backups/ and .env off the server. Without SECRET and the database password, the dump restores data but not a working instance. If you install extensions, back up the extensions volume as well.

Upgrades

Upstream's instructions are short: change the image tag and restart. Directus runs all database migrations automatically on startup. Upstream also publishes a list of breaking changes with every release, and you should read it before every upgrade. Releases come out roughly once a month.

cd ~/directus
# after editing the directus/directus tag in docker-compose.yml:
docker compose pull
docker compose up -d --wait

Take the pg_dump backup first. Migrations run forward only, so the dump is your way back.

Troubleshooting

directus never turns healthy. Run docker compose logs directus --tail 100. The usual cause is a database authentication error. PostgreSQL reads POSTGRES_PASSWORD only when its volume is first created, so a later change in .env leaves the two out of sync.

Uploads fail with permission errors. You switched the volumes to bind mounts (./uploads). The container runs as the unprivileged node user and can't write to a directory owned by root. Use named volumes as above, or chown the directory to the container's UID.

Links in emails or file URLs point to the wrong host. PUBLIC_URL is wrong. Fix it in .env and run docker compose up -d.

The admin login fails on a fresh install. Check for special characters in ADMIN_PASSWORD that a shell or YAML parser might have changed. The generator above removes /, + and = for that reason.

Verification + next steps

You're done when:

  • https://directus.example.com loads over a valid certificate;
  • you can sign in and have changed the admin password;
  • a test collection survives docker compose restart;
  • a dated pg_dump exists off the server.

From there, model your first collections, set the Public policy, and connect a front end with the SDK. To compare backends, see Appwrite on a VPS and Supabase on a VPS. For a spreadsheet-style UI over data, see NocoDB on a VPS. For ranked hosts, see Best VPS for databases.

Next steps

How to self-host Directus →More self-hosted backend & baas tools →Best VPS for PocketBase →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.