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
Provision the VPS and Docker
After connecting via SSH, install Docker and Docker Compose. Create an A DNS record for
cms.mydomain.compointing to the VPS IP. Open only ports 80 and 443 in the firewall (ufw allow 80,443/tcp).Define the stack in docker-compose.yml
Declare three services:
database(imagepostgres:16with a persistent volume),cache(redis), anddirectus(directus/directus). Connect Directus to Postgres via the variablesDB_CLIENT=pg,DB_HOST=database,DB_DATABASE,DB_USER,DB_PASSWORD.Configure the environment variables
Fill in
KEYandSECRETwith random strings (openssl rand -hex 32), setADMIN_EMAILandADMIN_PASSWORDfor the first account, andPUBLIC_URL=https://cms.mydomain.comso that links and uploads are correct.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 withdocker compose logs -f directus.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
directusservice and the ACME certresolver. Make surePUBLIC_URLexactly matches the HTTPS domain to avoid CORS and upload issues.Secure and back up
Restrict the CORS origins to your front-ends' domains, disable public registration if unnecessary, then schedule a daily
pg_dumpplus a copy of theuploadsfolder 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
Confirm the out-of-memory kill
Look for the kernel trace with
dmesg -T | grep -i "out of memory", followdocker events --filter event=oomor read theOOMKilledfield ofdocker inspect. A kill of this kind leaves no Directus error message: the container simply restarts.Set an explicit memory limit
In the Compose file, give the
directusservice a limit (mem_limitordeploy.resources.limits.memory) below the VPS's free RAM, withrestart: unless-stopped. Without a limit, the host kernel picks which process to kill, sometimes PostgreSQL rather than Directus.Measure the curve
Log
docker stats --no-streamevery minute to a file, along with the number of/assetsrequests 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.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_IDfrom several terminals and watchdocker stats. A k6 script does the same more repeatably.Capture two heap profiles
Add
NODE_OPTIONS=--heap-prof --heap-prof-dir=/directus/profilesto the service environment, with a folder mounted as a volume. Node only writes the profile on a clean process exit: stop the container withdocker compose stop directusonce 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
| Lead | Where it was reported | Status and caveats |
|---|---|---|
| `AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED` | A participant, in October 2026, with a test bench | Not 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 12 | One participant says it fixed the problem; another puts a fix at 12.3.1 or later | Not 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 later | A contributor recommends it for cleaning up streams of disconnected clients | The fix is merged and closes a different issue (26991); on issue 26206, a participant describes an improvement but growth that persists. |
| Scheduled redeploy through cron | A participant, as a stopgap | Hides the symptom without fixing anything, and cuts downloads in progress. |
| Memory limit and automatic restart | This guide's operating recommendation | Contains 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_VALIDATIONif 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
/assetstraffic 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.