Skip to content

TutorialsCMS & websites

How to install Directus with Docker Compose, PostgreSQL and HTTPS

Self-host Directus with Docker Compose on Ubuntu or Debian: a pinned image, PostgreSQL, optional Redis cache, Caddy HTTPS, safe secrets, backups and updates.

  • Intermediate
  • 30 min read
  • Updated

Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

This guide is not available in your language yet, so it is shown in English.

On this page
  1. Prerequisites
  2. Step 1 — Create the project folder and secrets
  3. Step 2 — Write the Compose file
  4. Step 3 — Start Directus and check it
  5. Step 4 — Publish Directus over HTTPS with Caddy
  6. Step 5 — Secure the first administrator
  7. Back up and restore
  8. Update Directus
  9. Troubleshooting
  10. Uploads fail with a permission error
  11. Everyone is logged out after each restart
  12. Directus cannot connect to the database after you changed DB_PASSWORD
  13. The database image does not pull on an ARM server
  14. Links in emails or login redirects use the wrong address
  15. Next steps

Directus is an open data platform: it connects to a SQL database, gives you an instant REST and GraphQL API, and adds a no-code app where editors manage content, files and users. Teams use it as a headless CMS and as a backend for websites and apps. This guide runs Directus with Docker Compose, following the official deployment example: a pinned Directus image, PostgreSQL with PostGIS, a Redis cache, folders for uploads and extensions, and Caddy in front for automatic HTTPS. You then secure the first administrator, and learn how to back up, restore and update the installation.

Prerequisites

  • A server running Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 or Debian 13. Directus ships as a Docker image, so any release supported by Docker Engine works.
  • A non-root user with sudo rights and SSH key login: Secure a new Linux server and Set up SSH keys.
  • Docker Engine with the Compose plugin: Docker on Ubuntu or Docker on Debian. The commands below assume your user is in the docker group; otherwise prefix them with sudo.
  • A domain name such as directus.example.com with an A record (and optionally an AAAA record) pointing at the server, and Caddy from Caddy reverse proxy.

Directus publishes figures for its own container only:

ResourceMinimum (official)Suggested starting point
CPU0.25 vCPU for the Directus container2 vCPU for the whole stack
RAM512 MB for the Directus container4 GB for Directus, PostgreSQL and Redis
DiskNot published20 GB plus the size of your uploads

The requirements page lists "1x 0.25 vCPU / 512 MB" as the required minimum and "2x 1 vCPU / 2GB" as the recommended minimum for Directus container resources. It does not include the database or Redis, so the right-hand column is a conservative starting point for everything on one server, not an official number.

Step 1 — Create the project folder and secrets

Keep everything for this app in /opt/directus. Create the folders for the database, uploads and extensions before the first start, so they belong to your user instead of being created by Docker as root:

Bash
sudo mkdir -p /opt/directus
sudo chown $USER:$USER /opt/directus
cd /opt/directus
mkdir -p uploads extensions data/database

Generate a signing secret and a database password:

Bash
openssl rand -hex 32
openssl rand -hex 24

SECRET signs tokens. If you do not set it, Directus uses a random value on every start, which logs everyone out after each restart; its documentation says it must be set to a secure random value in production. Create /opt/directus/.env with nano .env:

.env
SECRET=paste-the-64-character-value
DB_PASSWORD=paste-the-48-character-value
ADMIN_EMAIL[email protected]
ADMIN_PASSWORD=change-me-to-a-long-unique-password
PUBLIC_URL=https://directus.example.com

ADMIN_EMAIL and ADMIN_PASSWORD create the first user during bootstrapping. PUBLIC_URL is the address where your API can be reached; Directus needs the full URL for features such as SSO. Protect the file:

Bash
chmod 600 .env

Step 2 — Write the Compose file

Create /opt/directus/compose.yaml. It follows the official example, with three production changes: Directus is published on 127.0.0.1 only, secrets come from .env, and every service restarts after a reboot.

YAML
services:
  database:
    image: postgis/postgis:17-3.5
    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
    restart: unless-stopped

  cache:
    image: redis:7
    healthcheck:
      test: ["CMD-SHELL", "[ $$(redis-cli ping) = 'PONG' ]"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_interval: 5s
      start_period: 30s
    restart: unless-stopped

  directus:
    image: directus/directus:12.5.0
    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}"
    restart: unless-stopped

A few notes on the file:

  • Pinned version. Directus recommends pinning a specific version in production, because a pinned tag is never replaced by a newer release when the container restarts. 12.5.0 was the newest release when this guide was checked in October 2026. Look up the current tag on the Directus releases page or on Docker Hub, and read its breaking-change notes before you use it.
  • Database. The official example uses the PostGIS image, which is PostgreSQL with geospatial extensions. PostgreSQL data lives in ./data/database; it is never published to the host.
  • Uploads and extensions. Uploaded files are stored in ./uploads and custom extensions in ./extensions, so both survive container updates.
  • Port. 127.0.0.1:8055 keeps Directus off the public interface. Docker-published ports bypass ufw rules, so binding to loopback is what actually protects the port.

Step 3 — Start Directus and check it

Bash
docker compose up -d
docker compose ps
docker compose logs -f directus

On the first start, Directus installs its schema in PostgreSQL and creates the admin user from .env. Stop following the logs with Ctrl+C once it reports that the server has started. All three services should then show as healthy in docker compose ps. Test the liveness endpoint, which needs no login:

Bash
curl -s http://127.0.0.1:8055/server/ping

You should see pong. Directus runs as the unprivileged node user inside the container; check its user ID and compare it with the owner of the uploads folder:

Bash
docker compose exec directus id
ls -ln /opt/directus

If the uid printed by id differs from the owner of uploads and extensions, give the folders to that ID, for example sudo chown -R 1000:1000 uploads extensions when the container reports uid=1000.

Step 4 — Publish Directus over HTTPS with Caddy

Add a site block to /etc/caddy/Caddyfile:

Caddyfile
directus.example.com {
    reverse_proxy 127.0.0.1:8055
}

Reload Caddy and allow only SSH and web traffic through the firewall:

Bash
sudo systemctl reload caddy
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Check the public endpoint with curl -s https://directus.example.com/server/ping. Caddy requests the certificate on the first visit and renews it automatically. To use Nginx instead, see Nginx with Certbot; Traefik also works.

Step 5 — Secure the first administrator

Open https://directus.example.com. By default the root address redirects to the app at /admin. Log in with ADMIN_EMAIL and ADMIN_PASSWORD, then:

  1. Open your user profile, set a new password that exists only in your password manager, and turn on two-factor authentication.
  2. Create named accounts with suitable roles for the other people who need access, instead of sharing the admin login.

The two ADMIN_ variables only matter when Directus bootstraps an empty database; changing them later does not change the account. Remove both lines from .env and from the environment section of compose.yaml, then recreate the container:

Bash
docker compose up -d

Directus emails password resets and user invitations; to send them through a relay, set EMAIL_TRANSPORT to smtp and add EMAIL_FROM, EMAIL_SMTP_HOST (for example smtp.example.com), EMAIL_SMTP_PORT (587, which uses STARTTLS), EMAIL_SMTP_USER and EMAIL_SMTP_PASSWORD to the environment section of the directus service, then run docker compose up -d again.

Back up and restore

State lives in three places: the PostgreSQL database, the uploads and extensions folders, and your compose.yaml and .env (which hold SECRET and the database password). Dump the database with PostgreSQL's own tool, which takes a consistent snapshot while Directus keeps running, and archive the files with dated names:

Bash
sudo mkdir -p /opt/backups
sudo chown $USER:$USER /opt/backups
chmod 700 /opt/backups
cd /opt/directus
docker compose exec -T database pg_dump -U directus -Fc directus > /opt/backups/directus-db-$(date +%F).dump
tar czf /opt/backups/directus-files-$(date +%F).tar.gz -C /opt/directus uploads extensions compose.yaml .env

To restore, stop Directus, recreate the database, load the dump and unpack the files. Replace the dates with those of your backup:

Bash
cd /opt/directus
docker compose stop directus
docker compose exec -T database dropdb -U directus --force directus
docker compose exec -T database createdb -U directus directus
docker compose exec -T database pg_restore -U directus -d directus < /opt/backups/directus-db-2026-10-09.dump
tar xzf /opt/backups/directus-files-2026-10-09.tar.gz -C /opt/directus
docker compose up -d

On a new server, install Docker, create /opt/directus, unpack the files archive there first (it contains compose.yaml and .env), start only the database with docker compose up -d database, then run the restore commands. Copy your backups off the server as well and test a restore from time to time.

Update Directus

Directus handles database migrations automatically when a newer image starts, which also means a downgrade needs a database restore. Before every update:

  1. Read the release notes on the releases page, especially the "Potential Breaking Changes" section; Directus calls this step critically important, because changes can affect environment variables, the APIs, the SDK and extensions.
  2. Take a backup as shown above.

Then change the tag on the image: directus/directus: line in compose.yaml to the new version and apply it:

Bash
cd /opt/directus
nano compose.yaml
docker compose pull
docker compose up -d
docker compose logs -f directus

Watch the logs until the migrations finish and the server starts, then log in and test your main collections and extensions. Keep the PostgreSQL major version in the postgis/postgis tag unchanged: moving to a new major needs a dump and restore, not just a new image. docker compose pull also refreshes the redis:7 image within its major version.

Troubleshooting

Uploads fail with a permission error

The uploads folder on the host belongs to a different user ID than the node user inside the container, often because Docker created it as root. Compare docker compose exec directus id with ls -ln /opt/directus and fix the owner with sudo chown -R as shown in Step 3.

Everyone is logged out after each restart

SECRET is not set, so Directus generates a new random value on every start. Add a fixed value to .env as in Step 1 and recreate the container with docker compose up -d.

Directus cannot connect to the database after you changed DB_PASSWORD

PostgreSQL applies POSTGRES_PASSWORD only when it initialises an empty data folder, so changing .env later leaves the old password in the database. Put the original password back, or change it inside PostgreSQL first with docker compose exec database psql -U directus -c "ALTER USER directus PASSWORD 'new-password';" and then update .env.

The database image does not pull on an ARM server

Docker Hub lists the postgis/postgis image for amd64 only, so an arm64 server reports that no matching manifest exists. Use an x86-64 server for this stack, or, if you never need geospatial fields, replace the image with the official postgres:17 image before the first start.

PUBLIC_URL is missing, still set to a placeholder, or uses http://. Set it to the full https:// address of your site and run docker compose up -d.

Next steps

Frequently asked questions

Do I need Redis to run Directus?

No. Directus's requirements call Redis optional but recommended for caching, and required only when you run several Directus containers side by side. You can remove the cache service and the CACHE and REDIS variables from the Compose file if you do not want it.

Why pin the Directus image to an exact version?

Directus recommends pinning a specific version in production. With a pinned tag, a restart never pulls a new release by surprise, and you choose when to read the breaking-change notes, back up and upgrade.

Can Directus use MySQL or another database instead of PostgreSQL?

Yes. Directus supports the long-term support versions of PostgreSQL, MySQL, MariaDB, SQLite, MS SQL Server, CockroachDB and OracleDB. Choose the database before the first start with DB_CLIENT and the matching DB_ connection variables; this guide uses PostgreSQL with PostGIS, as the official Compose example does.

Can Directus store uploads in object storage instead of a local folder?

Yes. Directus stores files on the local file system by default and supports external storage locations through its STORAGE settings. This guide keeps uploads in a local folder, which is simplest to back up on a single server.

Which HyperDC servers can run Directus?

Any HyperDC Linux VPS, VDS or dedicated server with root access where Docker Engine is supported. Size it for Directus, PostgreSQL and Redis together, and add disk space for the files your editors upload.

Wachtwoord genereren

Please confirm