Deployment guide

Hosting Directus on a VPS

Deploy on a VPS Cloud →

Tutorial

Databases

Hosting Directus on a VPS

Directus turns any SQL database into an instant REST and GraphQL API, with a no-code administration interface on top. It's a headless CMS and a complete data platform, perfect to self-host on a VPS to manage the content of your sites and applications. Here's how to deploy it with PostgreSQL, Docker, and an SSL certificate.

11 min read11 steps
Contents· Why self-host Directus on a VPS1/13
  1. 01Why self-host Directus on a VPS
  2. 02The concrete benefits of self-hosting Directus
  3. 03Hardware and software prerequisites
  4. 04Deploying Directus step by step
  5. 05Serving files from S3: what it changes for memory
  6. 06Troubleshooting: memory climbs until the container is killed
  7. 07What the issue participants report
  8. 08Diagnosing without guessing
  9. 09Reported workarounds, none confirmed by the Directus team
  10. 10Leads reported for the memory problem
  11. 11What to collect for a useful bug report
  12. 12The file to attach
  13. 13One-click Directus deployment on ServOrbit

Why self-host Directus on a VPS

Directus plugs into your existing database (PostgreSQL, MySQL, SQLite, MariaDB) without imposing its own schema: it introspects your tables and generates an API and an admin interface on top. By self-hosting it on a VPS, you avoid the per-seat pricing of the official Cloud offering and you keep your content, your media, and your SQL database on your own infrastructure. It's a structuring choice for agencies that deliver several sites: a single Directus can serve as the back office for multiple front-ends (Nuxt, Next, mobile). Control of the database, extensions, and webhooks lets you integrate Directus into any data pipeline without depending on a third-party provider, with no third-party service sitting between your front-ends and your data.

The concrete benefits of self-hosting Directus

  • REST and GraphQL APIs generated automatically from your SQL schema, without writing code.
  • A no-code admin interface to model content, manage roles, and set granular permissions.
  • Compatible with an existing SQL database: you can expose it without migrating or altering it.
  • Media management, on-the-fly image transformations, and local or S3 storage of your choice.
  • Built-in Flows and webhooks to automate processing (notifications, synchronizations).
  • Pooling: a single Directus VPS can power several sites and front-end applications.

Hardware and software prerequisites

Directus is a Node.js application that relies on an external database. Count on a minimum of 2 GB of RAM to comfortably run Directus and PostgreSQL on the same VPS; 4 GB are recommended in production with image transformations and several simultaneous administrators. One to two vCPU are enough to start. Plan for a Redis cache (optional but recommended for sessions and rate limiting) and a 30 to 60 GB SSD volume depending on your media library. You'll need Docker and Docker Compose, a domain pointing to the VPS, and a robust secret key for SECRET and token hashing. Postgres remains the recommended database for advanced features.

Deploying Directus step by step

  1. Provision the VPS and Docker

    After connecting via SSH, install Docker and Docker Compose. Create an A DNS record for cms.mydomain.com pointing to the VPS IP. Open only ports 80 and 443 in the firewall (ufw allow 80,443/tcp).

  2. Define the stack in docker-compose.yml

    Declare three services: database (image postgres:16 with a persistent volume), cache (redis), and directus (directus/directus). Connect Directus to Postgres via the variables DB_CLIENT=pg, DB_HOST=database, DB_DATABASE, DB_USER, DB_PASSWORD.

  3. Configure the environment variables

    Fill in KEY and SECRET with random strings (openssl rand -hex 32), set ADMIN_EMAIL and ADMIN_PASSWORD for the first account, and PUBLIC_URL=https://cms.mydomain.com so that links and uploads are correct.

  4. Start and initialize the database

    Run docker compose up -d. On the first startup, Directus automatically creates the system tables and the admin account. Follow the initialization with docker compose logs -f directus.

  5. Connect the reverse proxy and TLS

    Put Caddy or Traefik in front of Directus to handle Let's Encrypt. With Traefik, add the routing labels on the directus service and the ACME certresolver. Make sure PUBLIC_URL exactly matches the HTTPS domain to avoid CORS and upload issues.

  6. Secure and back up

    Restrict the CORS origins to your front-ends' domains, disable public registration if unnecessary, then schedule a daily pg_dump plus a copy of the uploads folder to remote storage.

To serve images efficiently, enable media storage on an S3-compatible bucket rather than the local disk, and set ASSETS_CACHE_TTL: this variable sets the browser-side cache duration (the max-age value of the Cache-Control header, 30 days by default); it does not cache transformations on the server. Put a CDN in front of Directus to offload media requests from the VPS. Then bound on-the-fly transformations: ASSETS_TRANSFORM_MAX_OPERATIONS limits the number of operations per request (5 by default, excluding saved presets), ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION the size in pixels (6000 by default) and ASSETS_TRANSFORM_MAX_CONCURRENT the number of simultaneous transformations (25 by default).

Serving files from S3: what it changes for memory

When media storage points to an S3-compatible bucket, Directus does not redirect the visitor to the bucket: the /assets/FILE_ID request goes through the Node process, which reads the object from the provider and streams it back to the client. The driver is configured with variables: STORAGE_LOCATIONS=s3, then STORAGE_S3_DRIVER=s3, STORAGE_S3_KEY, STORAGE_S3_SECRET, STORAGE_S3_BUCKET, STORAGE_S3_REGION and, for compatible storage such as MinIO, STORAGE_S3_ENDPOINT with STORAGE_S3_FORCE_PATH_STYLE=true. In practice, the container's memory depends on the number of simultaneous downloads and the speed of each client, not only on the size of the database. This is the path where users report memory that never comes back down, especially with videos or large files.

Troubleshooting: memory climbs until the container is killed

The symptom is described in issue 26206 of the directus/directus repository, opened in November 2025 on a self-hosted Docker instance running 11.13.2 with PostgreSQL 16: the pod's memory rises steadily to the configured limit, then the pod is killed and restarts. The slope is lower at night and on weekends, which points to file traffic rather than an idle leak. At the time of writing (late October 2026), the issue is still open, labelled Bug, Performance and AWS S3. No Directus team member has confirmed a cause there: everything below is participants' hypotheses, not a maintainer diagnosis.

What the issue participants report

  • Heavy files — the problem is reproduced with a k6 load script on videos of 36 to 86 MB, and on twelve files of about 500 MB; the request goes through /assets.
  • Several storage backends — AWS S3 and MinIO are mentioned, as well as another S3-compatible service at an application host.
  • Slow growth with images — one participant goes from about 200 MB after a redeploy to 1 GB within a few days; another sees pods climb from 1.3 GB to over 2 GB in nearly two weeks.
  • Unclosed streams hypothesis — a fix cleans up streams when a client disconnects mid-download; a contributor places it in 11.17.2, and one participant sees an improvement without the growth disappearing.
  • Checksum hypothesis — in October 2026, a participant blames the response checksum validation of the AWS S3 SDK: the validation stream (ChecksumStream, package @smithy/util-stream) would ignore backpressure. Their test bench: a 1 GiB limit and a 150 MB object; one slow client takes external memory from 8 to 160 MB, three clients cause a kill within 15 seconds, and files uploaded as multipart do not trigger it. They add that this does not explain slow growth over several days without large downloads.
  • Report on version 12 — another participant says upgrading to version 12 fixed the problem for them, while the issue remains open.

Diagnosing without guessing

  1. Confirm the out-of-memory kill

    Look for the kernel trace with dmesg -T | grep -i "out of memory", follow docker events --filter event=oom or read the OOMKilled field of docker inspect. A kill of this kind leaves no Directus error message: the container simply restarts.

  2. Set an explicit memory limit

    In the Compose file, give the directus service a limit (mem_limit or deploy.resources.limits.memory) below the VPS's free RAM, with restart: unless-stopped. Without a limit, the host kernel picks which process to kill, sometimes PostgreSQL rather than Directus.

  3. Measure the curve

    Log docker stats --no-stream every minute to a file, along with the number of /assets requests in flight. Note the value after a restart, the plateau, and whether the curve follows traffic or climbs while idle. A Node heap limit (--max-old-space-size) does not bound off-heap memory, the buffers, which is the area targeted by the checksum hypothesis.

  4. Reproduce with a slow client

    On a test environment, never in production, download a large file with curl --limit-rate 200k -o /dev/null https://cms.mydomain.com/assets/FILE_ID from several terminals and watch docker stats. A k6 script does the same more repeatably.

  5. Capture two heap profiles

    Add NODE_OPTIONS=--heap-prof --heap-prof-dir=/directus/profiles to the service environment, with a folder mounted as a volume. Node only writes the profile on a clean process exit: stop the container with docker compose stop directus once right after startup (baseline), then a second time once memory has swollen. An out-of-memory kill ends the process without writing a profile.

Reported workarounds, none confirmed by the Directus team

None of these leads has received official validation in the issue. Try them on a staging environment, measure before and after, and do not present them as fixes.

Leads reported for the memory problem

Scroll the table

LeadWhere it was reportedStatus and caveats
`AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED`A participant, in October 2026, with a test benchNot confirmed by the team. According to AWS documentation, download checksum validation only happens when the caller explicitly enables it: you lose that integrity check.
Upgrade to version 12One participant says it fixed the problem; another puts a fix at 12.3.1 or laterNot confirmed. The 12.3.1 release notes only mention updated storage driver dependencies. Major version change: read the notes and back up first.
Version 11.17.2 or laterA contributor recommends it for cleaning up streams of disconnected clientsThe fix is merged and closes a different issue (26991); on issue 26206, a participant describes an improvement but growth that persists.
Scheduled redeploy through cronA participant, as a stopgapHides the symptom without fixing anything, and cuts downloads in progress.
Memory limit and automatic restartThis guide's operating recommendationContains the damage: the container restarts cleanly instead of dragging the host down. Does not address the cause.

What to collect for a useful bug report

The team asked for heap profiles: without them, the issue cannot move forward. Before commenting or opening a new issue, gather the items below.

The file to attach

  • Exact version — the Directus image tag (not latest) and the date of the last restart.
  • Storage — driver, provider (AWS, MinIO, other) and region, without the key or secret, plus the value of AWS_RESPONSE_CHECKSUM_VALIDATION if it is set.
  • Limits — container memory, host RAM and restart policy.
  • Load — number and size of the files served, simultaneous downloads, presence of a CDN or proxy in front of Directus, slow or dropped clients.
  • Memory curve — total container memory (not just the process) over several days, with the /assets traffic alongside.
  • Two heap profiles — baseline and swollen state. The contributor who asks for them states they contain only stack traces and sampled allocation sizes, not file contents or secrets.
  • How files were uploaded — in one piece or as multipart, since a participant says the latter do not trigger the case.

Before upgrading to version 12, read the 12.3.0 release notes: it fixes storage connections that leaked when an asset request was cancelled or a transformation failed, but it also changes the behaviour of exists(), which now throws an error instead of returning false. With S3, a missing object can then produce a 403 response rather than a 404 if the credentials cannot list the bucket: grant s3:ListBucket. Back up the database and the files, then test the upgrade on a copy before touching production.

One-click Directus deployment on ServOrbit

Directus is available directly from the ServOrbit Marketplace. Select the application when ordering your VPS: Docker comes pre-installed, the Directus container starts automatically, and your admin credentials ([email protected] plus a generated password) are displayed in your ServOrbit dashboard. Access the admin interface via an SSH tunnel or by attaching a domain from your dashboard — without installing Docker or editing a Compose file by hand.

Deploy Directus on a VPS in one click

Order a ServOrbit VPS with Directus pre-installed: Docker configured, container running, admin credentials generated and shown in your dashboard. No manual configuration needed.