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 → 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 is12.2.0:
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
directustab - In the Domain field, enter your subdomain — e.g.
https://directus.yourdomain.com. Usehttps, 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:
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:
sudo docker ps
Install jq, so the JSON output we're about to read is actually readable:
sudo apt install jq
Check the health check failure log (grab your Directus container's name from the docker ps output above):
docker inspect --format='{{json .State.Health}}' <your-directus-container-name> | jq
The result looked like this:
{
"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:
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:
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:
docker logs <your-directus-container-name> --tail 150
Directus only bound to IPv4. The startup log says exactly that:
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:
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. If you get a "Not Secure" error in the browser, this is usually just a stale cache from visiting the subdomain before the fix. Try a hard refresh first (Ctrl+Shift+R). If that doesn't clear it, clear your browser's cache entirely (all time, not just a recent window) — a partial or time-bounded clear may not be enough.
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 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.
Top comments (0)