VPS
VPS Setup Guide (End-to-End)
Author: Ahmad Hafizh Muttaqi
Style: Senior Software Engineer Playbook
Scope: From clean VPS → production-ready setup with Ghost, RabbitMQ, Directus, Docker, Nginx, SSL, and shared databases.
0. High-Level Architecture
Internet
│
▼
Nginx (80 / 443, SSL by Certbot)
├── blog.ahmadswork.space → Ghost (Node 18)
├── cms-admin.ahmadswork.space → Directus (Docker)
└── mq.ahmadswork.space → RabbitMQ Management UI
Docker Network (devnet)
├── PostgreSQL 16
├── Redis 7
├── RabbitMQ
└── Directus
Key principles:
- One VPS, multiple services
- Nginx as a single entry point
- Docker for infra services
- Node version isolation (Node 18 only where required)
- Single Postgres instance reused
1. Base VPS Preparation
1.1 Update system
sudo apt update && sudo apt upgrade -y
1.2 Install core tools
sudo apt install -y \
curl ca-certificates gnupg lsb-release \
nginx git ufw
1.3 Firewall
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
2. Docker & Docker Compose
2.1 Install Docker
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker
2.2 Install Docker Compose plugin
sudo apt install -y docker-compose-plugin
Verify:
docker compose version
3. Shared Infrastructure Stack (Postgres + Redis)
This stack is reused by Directus and application services.
3.1 docker-compose.yml
services:
postgres:
image: postgres:16
container_name: postgres16
restart: unless-stopped
environment:
POSTGRES_USER: ahmad
POSTGRES_PASSWORD: supersecret_pg
POSTGRES_DB: appdb
ports:
- "127.0.0.1:5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- devnet
redis:
image: redis:7
container_name: redis7
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
ports:
- "127.0.0.1:6379:6379"
volumes:
- redis_data:/data
networks:
- devnet
volumes:
postgres_data:
redis_data:
networks:
devnet:
Start services:
docker compose up -d
4. Ghost Blog Setup (Node 18 Only)
4.1 Node isolation strategy
- System Node version untouched
- Node 18 scoped only to Ghost directory
- Avoid global conflicts
4.2 Install Node 18 locally
cd /var/www/blog
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs
4.3 Install Ghost CLI
sudo npm install -g ghost-cli
4.4 Install Ghost
sudo mkdir -p /var/www/blog
sudo chown $USER:$USER /var/www/blog
cd /var/www/blog
ghost install
Key choices:
- Database: MySQL 8
- URL:
https://blog.ahmadswork.space - Nginx: No (manual)
- SSL: Already handled by certbot
5. Nginx for Ghost
5.1 blog.ahmadswork.space
server {
listen 80;
server_name blog.ahmadswork.space;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name blog.ahmadswork.space;
ssl_certificate /etc/letsencrypt/live/blog.ahmadswork.space/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.ahmadswork.space/privkey.pem;
location / {
proxy_pass http://127.0.0.1:2368;
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;
}
}
6. RabbitMQ (Docker + Public URL)
6.1 Why RabbitMQ
- Reliable background job processing
- Durable queues
- Horizontal scale ready
6.2 docker-compose.yml
services:
rabbitmq:
image: rabbitmq:3-management
container_name: rabbitmq
restart: unless-stopped
environment:
RABBITMQ_DEFAULT_USER: ahmad
RABBITMQ_DEFAULT_PASS: supersecret_rabbit
ports:
- "127.0.0.1:5672:5672"
- "127.0.0.1:15672:15672"
networks:
- devnet
6.3 Connection URL
amqp://ahmad:supersecret_rabbit@mq.ahmadswork.space:5672
This is acceptable for a playground server.
7. RabbitMQ Queue Design (Important Concept)
x-queue-type: quorum
Why quorum queues?
- Data safety (replication)
- Crash-safe
- Recommended for modern RabbitMQ
Alternatives:
| Type | Pros | Cons |
|---|---|---|
| classic | Fast | Data loss risk |
| quorum | Safe | Slightly heavier |
Decision: Use quorum for critical jobs.
8. Directus (Docker, Reuse Existing Postgres)
8.1 Key Decision
- ❌ Do NOT create a new Postgres
- ✅ Reuse existing
postgres16
8.2 Docker Networking Rule (Critical)
Containers cannot use localhost to reach other containers.Correct host:
postgres
8.3 Directus docker-compose.yml
services:
directus:
image: directus/directus:latest
container_name: directus
restart: unless-stopped
ports:
- "127.0.0.1:8055:8055"
environment:
KEY: "supersecretkey"
SECRET: "supersecretsecret"
DB_CLIENT: "pg"
DB_HOST: "postgres"
DB_PORT: 5432
DB_DATABASE: "appdb"
DB_USER: "ahmad"
DB_PASSWORD: "supersecret_pg"
ADMIN_EMAIL: "admin@ahmadswork.space"
ADMIN_PASSWORD: "SuperStrongPassword"
networks:
- devnet
9. Nginx for Directus
cms-admin.ahmadswork.space
server {
listen 80;
server_name cms-admin.ahmadswork.space;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name cms-admin.ahmadswork.space;
ssl_certificate /etc/letsencrypt/live/cms-admin.ahmadswork.space/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cms-admin.ahmadswork.space/privkey.pem;
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-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
10. Certbot & DNS Lessons Learned
10.1 Wildcard DNS
*.ahmadswork.spacedoes NOT guarantee cert issuance- Certbot still requires DNS resolution
10.2 Correct flow
- DNS resolves subdomain
- Nginx responds on port 80
- Certbot validates
- SSL issued
11. Common Errors & Fixes
❌ ENOTFOUND host.docker.internal
Reason:
- Linux does NOT support this by default
Fix:
- Use Docker service name (
postgres)
❌ Knex Timeout / Pool Full
Cause:
- Directus cannot reach Postgres
Fix:
- Same Docker network
- Correct DB_HOST
12. Final Checklist
- Nginx reverse proxy
- SSL for all subdomains
- Ghost (Node 18 isolated)
- RabbitMQ (Docker)
- Directus (Docker)
- Shared Postgres & Redis
- Proper Docker networking