# 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.

Difficulty: Intermediate\
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

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](/guides/secure-a-new-linux-server) and [Set up SSH keys](/guides/ssh-keys).
- Docker Engine with the Compose plugin: [Docker on Ubuntu](/guides/install-docker-ubuntu) or [Docker on Debian](/guides/install-docker-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](/guides/caddy-reverse-proxy).

Directus publishes figures for its own container only:

| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | 0.25 vCPU for the Directus container | 2 vCPU for the whole stack |
| RAM | 512 MB for the Directus container | 4 GB for Directus, PostgreSQL and Redis |
| Disk | Not published | 20 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=admin@example.com
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](https://github.com/directus/directus/releases) 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.

> **Tip**
>
> Redis is optional on a single server. To run without it, delete the `cache` service, the `cache` entry under `depends_on` and the four `CACHE_` and `REDIS` variables.

## 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](/guides/nginx-reverse-proxy-certbot); [Traefik](/guides/traefik-reverse-proxy) 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.

> **Note**
>
> Outbound port 25 is closed by default on HyperDC VPS. For services bought for a term of 3 months or longer, it is opened on request: [open a support ticket](/guides/support-tickets). Until then, send mail through an SMTP relay on port 587.

## 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:

> **Warning**
>
> `dropdb` deletes the current Directus database. Make sure the backup you are about to restore is complete.

```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](https://github.com/directus/directus/releases), 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.

### Links in emails or login redirects use the wrong address

`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

- Compare a code-first headless CMS: [How to install Strapi](/guides/install-strapi).
- Refresh the Compose basics behind this setup in [Docker Compose basics](/guides/docker-compose-basics).
- See server options for Directus projects on the [Directus hosting](/directus-hosting) page.
- Read the official [Directus self-hosting documentation](https://directus.com/docs/self-hosting/deploying) for storage adapters, email and scaling.

## 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.

---

Source: <https://hyperdc.com/guides/tutorials/install-directus>\
Updated: 2026-10-09
