# Setting Up Directus on a Coolify VPS (And Fixing the "Unhealthy" Error)

* * *

%[https://www.youtube.com/watch?v=OMX1Rk1HTZo] 

## Introduction

Directus is a backend for developers. It can connect to any SQL database and asset storage, and it provides developer tooling — which the Directus team calls the "Data Engine" — alongside a web application that lets both developers and non-developers manipulate data and assets through a no-code interface called the "Data Studio."

Directus also offers granular access control, meaning end users can only see, manipulate, and interact with the data allowed by their role and access policy — enforced consistently by both the engine and the studio. It's extensible through its own marketplace, and it's completely free, with a free license available.

Directus is also known as a **headless CMS** — meaning it doesn't ship with a front end attached out of the box, the way WordPress traditionally did. You can pair it with whichever front end you prefer; TanStack Start is my front end of choice.

In this post, I'll self-host Directus on my Coolify-managed VPS, walk through both setup methods, and — since nothing ever works perfectly on the first try — fix the actual errors I hit along the way.

> 📌 **New here?** I'd recommend checking out my earlier posts where I set up the VPS itself and installed Coolify on it, before continuing with this one.

## Let's Get Started

There are two ways to set up Directus on a Coolify-managed VPS. I'll cover the first one here.

### 1\. Log in to your Coolify Dashboard

*   In the sidebar menu, go to **Projects → +Add**
    
*   Give your project a name and description, and press continue
    

### 2\. Add a Resource

After naming your project, you should be redirected to the project's page, where you can add a resource. If that redirect doesn't happen, click **Projects** in the sidebar, then click into your project, and you should see **+Add Resource**.

*   Click **+Add Resource**
    
*   Under Applications, select **Docker Compose Empty**
    
*   A blank Docker Compose file will open
    
*   Go to [directus.com](https://directus.com) → **Docs** → **Hosting** → **Deployment**
    
*   Scroll down until you find the example Docker Compose YAML file, and copy it
    
*   Paste it into your blank Coolify Compose file
    
*   Find the `directus:` service, and under it, `image:` — update this to the version you want. As of this post, the latest version is `12.2.0`:
    
    ```yaml
    image: directus/directus:12.2.0
    ```
    
*   Scroll to the end and set your own admin email and password — these become your Data Studio login credentials
    
*   Click **Save** at the top
    

### 3\. Configure the Service

You should now be on the **Configuration** screen, with a **Deploy** button (yellow outline arrow) in the top right.

*   Under **Network**, check the box
    
*   Under **Services**, click **Settings** on the `directus` tab
    
*   In the **Domain** field, enter your subdomain — e.g. `https://directus.yourdomain.com`. **Use** `https`, so Traefik knows to issue a Let's Encrypt certificate for it
    
*   Click **Save**, then **Back**
    
*   Click **Edit Compose File** at the top, scroll down to `PUBLIC_URL`, and set it to match your subdomain:
    
    ```yaml
    PUBLIC_URL: https://directus.yourdomain.com
    ```
    
*   Click **Save**, close the modal
    
*   Click **Deploy**, then **Confirm** on the popup
    
*   Wait for the deployment to finish
    

### 4\. Launch Directus

On the Configuration screen, click **Links**, then click your new subdomain.

## The Errors

At this point, your browser will show two errors: a **"Not Secure"** warning in the address bar, and a **"no available server"** page.

Back on the Coolify dashboard, you'll notice the Directus instance is showing as **unhealthy**. Traefik will not route traffic to a service that's failing its health check — which is exactly what's happening here. So the first job is figuring out *why* the health check is failing.

## Diagnosing the Problem

SSH into your VPS to start digging.

**Check your running containers:**

```bash
sudo docker ps
```

**Install** `jq`, so the JSON output we're about to read is actually readable:

```bash
sudo apt install jq
```

**Check the health check failure log** (grab your Directus container's name from the `docker ps` output above):

```bash
docker inspect --format='{{json .State.Health}}' <your-directus-container-name> | jq
```

The result looked like this:

```json
{
  "Status": "unhealthy",
  "FailingStreak": 79,
  "Log": [
    {
      "Start": "2026-07-30T18:12:21.445964902-04:00",
      "End": "2026-07-30T18:12:21.502447668-04:00",
      "ExitCode": 1,
      "Output": "wget: can't connect to remote host: Connection refused\n"
    },
    {
      "Start": "2026-07-30T18:12:31.505411263-04:00",
      "End": "2026-07-30T18:12:31.556307544-04:00",
      "ExitCode": 1,
      "Output": "wget: can't connect to remote host: Connection refused\n"
    }
  ]
}
```

The first clue: `"wget: can't connect to remote host: Connection refused"`. Whatever endpoint the health check is trying to reach, it's being actively refused — not timing out, refused. That distinction matters, and it's the thread that leads to the actual fix.

## Inspecting the Docker Compose File

*   From the Coolify dashboard, select **Projects** in the sidebar
    
*   Click your project
    
*   Click the service, then **Edit Compose File** next to **Service Stack**
    

Here's the relevant section:

```yaml
directus:
  image: 'directus/directus:12.2.0'
  ports:
    - '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
```

The problem is in the `wget` line — specifically, `http://localhost:8055`.

### Why `localhost` Breaks This

Inside a Linux container, `localhost` isn't an address — it's a hostname that has to be resolved first, and it typically maps to **two** addresses at once:

```plaintext
127.0.0.1   localhost   # IPv4
::1         localhost   # IPv6
```

When `wget` looks up `localhost`, the system's resolver hands back both addresses, and `wget` tries them in whatever order it receives them — which, on many Linux/Alpine base images, tends to prefer **IPv6 first**.

Checking the container's logs confirms what's actually listening:

```bash
docker logs <your-directus-container-name> --tail 150
```

Directus only bound to IPv4. The startup log says exactly that:

```plaintext
Server started at http://0.0.0.0:8055
```

`0.0.0.0` means "listen on every IPv4 interface" — Directus never opened an IPv6 socket at all. So when `wget` tries `::1:8055` first, there's genuinely nothing listening there, and the OS doesn't wait around wondering — it immediately sends back a rejection (a TCP RST). That's exactly why the log shows an instant **"Connection refused"** rather than a slow timeout. A timeout would mean something was reachable but not responding; a refusal means the OS said "nothing's here" right away.

`127.0.0.1` sidesteps the whole problem, because it's already a literal IP address — no hostname lookup, no ambiguity about which protocol family to try, no chance of picking the wrong one. It goes straight to the one address where Directus is actually listening.

## The Fix

Change `localhost` to `127.0.0.1` in the health check:

```yaml
directus:
  image: 'directus/directus:12.2.0'
  ports:
    - '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
```

Save the change, close the modal, and click **Restart** in the top right. Wait for the restart to complete — it may look like nothing's happening for a bit, so give it a moment. Once it's finished, close the logs modal, and Directus should show as **healthy**.

Click **Links**, then your Directus subdomain, and you should land on the Data Studio login screen.

## Logging In & Licensing

Enter the credentials you set earlier in the Compose file. You'll be asked whether you have a license or want to install the Core — choose **Core** for now, and follow the flow to complete sign-up.

At this stage, your instance isn't fully unlocked — you'll need a license. Good news: it's completely free if your business's revenue is under $5M and your team is under 50 people.

Head to [directus.com/oig](https://directus.com/oig) to apply for your key. Once you have it, go to your Directus Studio, click the gear icon (Settings) in the sidebar, click **License**, and add your key there to unlock all features. Your key is valid for a year.

* * *

Got questions, or hit a different error setting this up? Drop a comment below — happy to help troubleshoot.
