Deploy Directus on a VPS
Self-host the open-source headless CMS with Docker Compose, PostgreSQL, Redis caching, Nginx, and free SSL.
Prerequisites
- A RamNode VPS running Ubuntu 22.04 (the $4/month SSD VPS is sufficient for personal projects; 2 GB RAM recommended for production)
- SSH access as a non-root user with
sudoprivileges - A domain name with an A record pointing to your server's IP address
- Basic comfort with the Linux command line
Licensing note: Directus is free to self-host for any organization with less than $5 million in total annual revenue and funding. No license required for the vast majority of self-hosters.
Initial Server Setup
Log into your VPS and apply system updates:
sudo apt update && sudo apt upgrade -yCreate a dedicated deployment directory:
mkdir -p ~/directus && cd ~/directus
mkdir uploads extensionsInstall Docker and Docker Compose
Install Docker via the official convenience script:
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp dockerVerify the installation:
docker --version
docker compose versionGenerate a Secure Secret
Directus requires a SECRET environment variable to sign tokens. Generate a strong random value:
openssl rand -hex 32Copy this value — you'll use it in the next step.
Create the Docker Compose File
Inside ~/directus, create your docker-compose.yml:
services:
database:
image: postgres:15-alpine
restart: unless-stopped
volumes:
- ./data/database:/var/lib/postgresql/data
environment:
POSTGRES_USER: "directus"
POSTGRES_PASSWORD: "CHANGE_THIS_DB_PASSWORD"
POSTGRES_DB: "directus"
healthcheck:
test: ["CMD", "pg_isready", "--host=localhost", "--username=directus"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "[ $(redis-cli ping) = 'PONG' ]"]
interval: 10s
timeout: 5s
retries: 5
directus:
image: directus/directus:latest
restart: unless-stopped
ports:
- "127.0.0.1:8055:8055"
volumes:
- ./uploads:/directus/uploads
- ./extensions:/directus/extensions
depends_on:
database:
condition: service_healthy
cache:
condition: service_healthy
environment:
SECRET: "PASTE_YOUR_GENERATED_SECRET_HERE"
DB_CLIENT: "pg"
DB_HOST: "database"
DB_PORT: "5432"
DB_DATABASE: "directus"
DB_USER: "directus"
DB_PASSWORD: "CHANGE_THIS_DB_PASSWORD"
CACHE_ENABLED: "true"
CACHE_AUTO_PURGE: "true"
CACHE_STORE: "redis"
REDIS: "redis://cache:6379"
ADMIN_EMAIL: "admin@yourdomain.com"
ADMIN_PASSWORD: "CHANGE_THIS_ADMIN_PASSWORD"
PUBLIC_URL: "https://yourdomain.com"Important: Replace CHANGE_THIS_DB_PASSWORD, CHANGE_THIS_ADMIN_PASSWORD, PASTE_YOUR_GENERATED_SECRET_HERE, and yourdomain.com with your actual values before starting the stack.
Start the Stack
docker compose up -dCheck that all three containers are running:
docker compose psWatch the Directus startup logs to confirm the database bootstrapped cleanly:
docker compose logs -f directusYou should see a line similar to Server started at http://0.0.0.0:8055 when it's ready.
Install and Configure Nginx
Install Nginx:
sudo apt install nginx -yCreate a server block for your domain:
server {
listen 80;
server_name yourdomain.com;
client_max_body_size 100M;
location / {
proxy_pass http://127.0.0.1:8055;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}Enable the site and test:
sudo ln -s /etc/nginx/sites-available/directus /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxEnable SSL with Let's Encrypt
Install Certbot:
sudo apt install certbot python3-certbot-nginx -yObtain and install a certificate:
sudo certbot --nginx -d yourdomain.comVerify the renewal timer is active:
sudo systemctl status certbot.timerOpen Firewall Ports
If you have ufw enabled, allow HTTP and HTTPS traffic:
sudo ufw allow 'Nginx Full'
sudo ufw statusAccess the Admin Panel
Navigate to https://yourdomain.com in your browser. Log in with the ADMIN_EMAIL and ADMIN_PASSWORD you set in your Compose file. You'll land in the Directus Studio where you can start building your data model.
Configure Automatic Backups (Recommended)
Back up the PostgreSQL database on a daily schedule using a cron job:
mkdir -p ~/backups
crontab -eAdd this line to run a backup at 2 AM every day:
0 2 * * * docker exec directus-database-1 pg_dump -U directus directus | gzip > /home/$(whoami)/backups/directus-$(date +\%Y\%m\%d).sql.gzTo keep only the last 14 days of backups:
0 3 * * * find /home/$(whoami)/backups -name "*.sql.gz" -mtime +14 -deleteConfirm your container name with docker compose ps — it may differ slightly depending on your directory name.
Keeping Directus Updated
To pull the latest Directus image and restart the stack:
cd ~/directus
docker compose pull
docker compose up -dAlways review the Directus release notes before major version upgrades, and take a database backup first.
Resource Sizing Guide
| Plan | RAM | Use Case |
|---|---|---|
| SSD VPS 1 ($4/mo) | 512 MB | Development, low-traffic personal projects |
| SSD VPS 2 ($8/mo) | 1 GB | Small production sites, single team |
| SSD VPS 3 ($16/mo) | 2 GB | Recommended minimum for production |
| SSD VPS 4+ ($32/mo+) | 4 GB+ | Multi-user apps, heavy asset management |
Directus officially recommends a minimum of 2 vCPU / 2 GB RAM for production workloads. The Redis cache layer in this guide significantly reduces database load and improves response times, making smaller VPS tiers viable for moderate traffic.
Troubleshooting
Directus container won't start
Check logs with docker compose logs directus. The most common cause is a malformed environment variable or the database not being healthy before Directus tries to connect.
502 Bad Gateway from Nginx
Directus may still be initializing. Wait 30–60 seconds after docker compose up before testing, then check docker compose ps to confirm all containers are running.
Uploads not persisting
Confirm the ./uploads volume mount is correct in your Compose file and that the directory exists on the host with proper permissions.
Out of disk space
Docker image layers accumulate over time. Clean up unused images and containers periodically with docker system prune.
Next Steps
- Connect a frontend framework like Next.js, Nuxt, or SvelteKit to your Directus API
- Configure role-based access control to restrict what authenticated users can read and write
- Set up Directus Flows for webhooks and automations
- Explore S3-compatible object storage for uploads — RamNode's block storage keeps costs low as your media library grows
