1018 lines
28 KiB
Markdown
1018 lines
28 KiB
Markdown
# IGNY8 Phase 0: VPS Provisioning (00B)
|
|
|
|
**Status:** In Development
|
|
**Date Created:** 2026-03-23
|
|
**Phase:** 0 (Infrastructure Setup)
|
|
**Document ID:** 00B
|
|
|
|
---
|
|
|
|
## 1. Current State
|
|
|
|
### 1.1 Migration Context
|
|
|
|
IGNY8 is transitioning from legacy infrastructure to a new KVM 4 VPS environment. The current production setup includes:
|
|
|
|
- **Production Domains:** `app.igny8.com`, `api.igny8.com`, `igny8.com` (marketing)
|
|
- **Staging Domains:** `staging.igny8.com`, `staging-api.igny8.com`, `staging-marketing.igny8.com`
|
|
- **Current DNS Provider:** NOT Cloudflare — domains currently point to the OLD VPS via existing DNS provider
|
|
- **Current Stack Health:** All services healthy on legacy infrastructure; zero downtime migration required
|
|
|
|
### 1.2 DNS Flow Clarification (CRITICAL)
|
|
|
|
**This is NOT a Cloudflare-first migration.** The correct approach:
|
|
|
|
1. **Current State:** Domains resolve via current DNS provider → OLD VPS (working, healthy)
|
|
2. **During 00B (this doc):**
|
|
- Provision new VPS infrastructure
|
|
- Create TEMPORARY TEST subdomains (`test-app.igny8.com`, `test-api.igny8.com`, `test-marketing.igny8.com`) at the CURRENT DNS provider pointing to NEW VPS IP
|
|
- Caddy on new server handles BOTH production domain names AND test subdomains (same containers, different hostnames)
|
|
- Real production domains still resolve to old VPS via existing DNS — no traffic shift yet
|
|
3. **After 00B (covered in 00C):** DNS cutover happens — production domains flipped to new VPS
|
|
4. **Cloudflare Migration:** Happens ONLY AFTER new server is fully healthy and production DNS has been migrated (end of 00C)
|
|
|
|
**SSL Certificates:** Caddy uses Let's Encrypt for auto-HTTPS on test subdomains. NO Cloudflare ACME tokens needed in this phase.
|
|
|
|
### 1.3 Previous Infrastructure Details
|
|
|
|
The legacy setup has these key characteristics:
|
|
|
|
- **Shared Services:** PostgreSQL, Redis, Caddy reverse proxy, PgAdmin, FileBrowser, Portainer
|
|
- **Application Services:** Defined in separate docker-compose.app.yml per environment
|
|
- **External Docker Network:** `igny8_net` (connects all services across compose files)
|
|
- **Directory Structure:** `/data/stack/`, `/data/app/`, `/data/logs/`, `/data/backups/`
|
|
- **Configuration:** Caddyfile for SSL/TLS termination, utility scripts in igny8-stack repo
|
|
- **Installation Script:** `install.sh` in igny8-stack repo handles automated setup
|
|
|
|
### 1.4 Known Constraints
|
|
|
|
- OS must be Ubuntu 24.04 LTS or later
|
|
- SSH access only (no root password login)
|
|
- Firewall restrictions (ports 80, 443, 22)
|
|
- Docker-first architecture (no bare-metal services)
|
|
- Test subdomains are temporary — removed after successful production DNS migration
|
|
|
|
---
|
|
|
|
## 2. Target Version Matrix
|
|
|
|
This section is the single source of truth for all target versions across the entire V2 build.
|
|
|
|
### 2.1 Infrastructure & OS
|
|
|
|
| Component | Version | Image/Package | Notes |
|
|
|-----------|---------|----------------|-------|
|
|
| Ubuntu | 24.04 LTS (Noble Numbat) | Official ISO | VPS base OS |
|
|
| Docker Engine | 29.x | docker-ce | Latest stable from Docker repo |
|
|
| Docker Compose | 2.x+ | docker-compose-plugin | Via Docker repo |
|
|
| UFW Firewall | Latest | ufw | System package |
|
|
|
|
### 2.2 Shared Infrastructure Services
|
|
|
|
| Service | Version | Image | Port (internal) | Port (public) | Volume |
|
|
|---------|---------|-------|-----------------|---------------|--------|
|
|
| PostgreSQL | 18 | `postgres:18-alpine` | 5432 | N/A | `/data/backups/postgres/` |
|
|
| Redis | 8.x | `redis:8-alpine` | 6379 | N/A | `/data/backups/redis/` |
|
|
| Caddy | 2.11.x | `caddy:2-alpine` | 80, 443 | 80, 443 | `/data/backups/caddy/` |
|
|
| Portainer CE | 2.27+ | `portainer/portainer-ce:lts` | 9000 | Via Caddy | `/var/run/docker.sock` |
|
|
| PgAdmin | Latest | `dpage/pgadmin4:latest` | 5050 | Via Caddy | N/A |
|
|
| FileBrowser | Latest | `filebrowser/filebrowser:latest` | 8080 | Via Caddy | `/data/` |
|
|
|
|
### 2.3 Application Stack (reference — installed during 00C/00D)
|
|
|
|
| Component | Version | Notes |
|
|
|-----------|---------|-------|
|
|
| Python | 3.14 | For backend Dockerfile |
|
|
| Node.js | 24 LTS | For frontend Dockerfile |
|
|
| Django | 6.0 | Backend framework |
|
|
| Django REST Framework | Latest | API serializers |
|
|
| Celery | 5.6 | Task queue |
|
|
| Gunicorn | 25 | WSGI application server |
|
|
| Vite | 8 | Frontend build tool |
|
|
| React | Latest | Frontend library |
|
|
|
|
---
|
|
|
|
## 3. What to Build
|
|
|
|
### 3.1 Deliverables
|
|
|
|
A fully provisioned, hardened KVM 4 VPS that:
|
|
|
|
1. Runs Ubuntu 24.04 LTS with essential system tools
|
|
2. Has Docker Engine 29.x and Docker Compose 2.x+ installed and ready
|
|
3. Implements UFW firewall with minimal port exposure (22, 80, 443)
|
|
4. Enforces SSH key-based authentication (no password login)
|
|
5. Hosts all shared infrastructure services (PostgreSQL 18, Redis 8, Caddy 2.11, Portainer 2.27+)
|
|
6. Maintains `/data/` directory structure for persistence
|
|
7. Caddy configured with routes for BOTH production AND test subdomains
|
|
8. Test subdomains resolve to new VPS and pass health checks
|
|
9. Passes health checks for all infrastructure services
|
|
10. Is ready for application deployment (see 00C for production, 00D for staging)
|
|
|
|
### 3.2 Out of Scope
|
|
|
|
- Application container deployment (covered in 00C/00D)
|
|
- Cloudflare DNS migration (covered at end of 00C)
|
|
- Backup automation (post-Phase 0)
|
|
- Monitoring/alerting setup (post-Phase 0)
|
|
- Custom application configuration
|
|
|
|
---
|
|
|
|
## 4. Data Models / APIs
|
|
|
|
### 4.1 Service Specifications
|
|
|
|
#### PostgreSQL 18
|
|
- **Port:** 5432 (internal, not exposed)
|
|
- **Image:** `postgres:18-alpine`
|
|
- **Volumes:** `/data/backups/postgres/` (data persistence)
|
|
- **Environment Variables:**
|
|
- `POSTGRES_USER=igny8`
|
|
- `POSTGRES_PASSWORD=${DB_PASSWORD}` (from .env)
|
|
- `POSTGRES_INITDB_ARGS=-c shared_preload_libraries=pg_stat_statements`
|
|
|
|
#### Redis 8.x
|
|
- **Port:** 6379 (internal, not exposed)
|
|
- **Image:** `redis:8-alpine`
|
|
- **Volumes:** `/data/backups/redis/` (persistence)
|
|
- **Command:** `redis-server --appendonly yes`
|
|
|
|
#### Caddy 2.11.x
|
|
- **Ports:** 80/443 (exposed)
|
|
- **Image:** `caddy:2-alpine`
|
|
- **Config:** `/data/stack/Caddyfile`
|
|
- **Volumes:** `/data/backups/caddy/` (SSL certs)
|
|
- **Auto-HTTPS:** Enabled (via Let's Encrypt, not Cloudflare)
|
|
- **Routes:** Production domains + test subdomains (same backend services)
|
|
|
|
#### Portainer CE 2.27+
|
|
- **Port:** 9000 (internal, not exposed; proxied via Caddy)
|
|
- **Image:** `portainer/portainer-ce:lts`
|
|
- **Volumes:** `/var/run/docker.sock` (Docker daemon access)
|
|
|
|
#### PgAdmin
|
|
- **Port:** 5050 (internal, not exposed; proxied via Caddy)
|
|
- **Image:** `dpage/pgadmin4:latest`
|
|
- **Environment:**
|
|
- `PGADMIN_DEFAULT_EMAIL=admin@igny8.com`
|
|
- `PGADMIN_DEFAULT_PASSWORD=${PGADMIN_PASSWORD}` (from .env)
|
|
|
|
#### FileBrowser
|
|
- **Port:** 8080 (internal, not exposed; proxied via Caddy)
|
|
- **Image:** `filebrowser/filebrowser:latest`
|
|
- **Root:** `/data/`
|
|
|
|
### 4.2 Docker Network
|
|
|
|
```
|
|
Network Name: igny8_net
|
|
Type: External bridge network
|
|
Purpose: Allows both docker-compose.infra.yml and docker-compose.app.yml to communicate
|
|
All infrastructure services connected to igny8_net
|
|
```
|
|
|
|
### 4.3 Environment Variables
|
|
|
|
Create `.env` file at `/data/stack/.env`:
|
|
|
|
```
|
|
# PostgreSQL
|
|
DB_PASSWORD=<strong-random-password>
|
|
DB_USER=igny8
|
|
|
|
# PgAdmin
|
|
PGADMIN_PASSWORD=<strong-random-password>
|
|
|
|
# Portainer
|
|
PORTAINER_PASSWORD=<strong-random-password>
|
|
|
|
# Application Domains (for reference in Caddyfile)
|
|
PROD_DOMAIN=igny8.com
|
|
PROD_APP_DOMAIN=app.igny8.com
|
|
PROD_API_DOMAIN=api.igny8.com
|
|
|
|
STAGING_DOMAIN=staging.igny8.com
|
|
STAGING_APP_DOMAIN=staging.igny8.com
|
|
STAGING_API_DOMAIN=staging-api.igny8.com
|
|
|
|
# Test Domains (temporary, point to new VPS during migration)
|
|
TEST_DOMAIN=test-marketing.igny8.com
|
|
TEST_APP_DOMAIN=test-app.igny8.com
|
|
TEST_API_DOMAIN=test-api.igny8.com
|
|
```
|
|
|
|
**NOTE:** No `CF_API_TOKEN` or `CF_ZONE_ID` in this phase. Cloudflare integration happens in 00C.
|
|
|
|
### 4.4 Docker Compose Files
|
|
|
|
**docker-compose.infra.yml** (shared infrastructure):
|
|
- PostgreSQL 18
|
|
- Redis 8.x
|
|
- Caddy 2.11.x
|
|
- Portainer CE 2.27+
|
|
- PgAdmin
|
|
- FileBrowser
|
|
|
|
**docker-compose.app.yml** (application services, per environment):
|
|
- Production application containers (deployed in 00C)
|
|
- Staging application containers (deployed in 00D)
|
|
- Linked to `igny8_net` external network
|
|
|
|
---
|
|
|
|
## 5. Implementation Steps
|
|
|
|
### 5.1 Pre-Provisioning Checklist
|
|
|
|
Before starting, have ready:
|
|
|
|
- [ ] VPS login credentials (IP address, initial SSH access)
|
|
- [ ] Current DNS provider credentials (to add test subdomains)
|
|
- [ ] Strong passwords for DB_PASSWORD, PGADMIN_PASSWORD, PORTAINER_PASSWORD
|
|
- [ ] SSH public key for hardening
|
|
- [ ] igny8-stack repository cloned locally or accessible
|
|
- [ ] Plan for test subdomain creation at current DNS provider
|
|
|
|
### 5.2 Phase 0: System Provisioning
|
|
|
|
#### Step 0.1: Initial Access & Updates
|
|
|
|
```bash
|
|
# SSH into VPS as root (initial access)
|
|
ssh root@<VPS_IP>
|
|
|
|
# Update system packages
|
|
apt-get update
|
|
apt-get upgrade -y
|
|
|
|
# Install essential utilities
|
|
apt-get install -y \
|
|
curl \
|
|
wget \
|
|
git \
|
|
vim \
|
|
htop \
|
|
net-tools \
|
|
ufw \
|
|
openssh-server \
|
|
openssh-client
|
|
|
|
# Set timezone
|
|
timedatectl set-timezone UTC
|
|
|
|
# Verify Ubuntu 24.04 LTS
|
|
lsb_release -a
|
|
# Should output: Release: 24.04
|
|
|
|
# Verify kernel
|
|
uname -r
|
|
```
|
|
|
|
#### Step 0.2: Create Non-Root User
|
|
|
|
```bash
|
|
# Create new user (replace 'igny8' with preferred username)
|
|
adduser --disabled-password --gecos "" igny8
|
|
|
|
# Add user to sudoers
|
|
usermod -aG sudo igny8
|
|
|
|
# Create SSH directory for user
|
|
mkdir -p /home/igny8/.ssh
|
|
chmod 700 /home/igny8/.ssh
|
|
```
|
|
|
|
#### Step 0.3: SSH Hardening
|
|
|
|
```bash
|
|
# (From local machine) Copy public SSH key to VPS
|
|
ssh-copy-id -i ~/.ssh/id_rsa.pub igny8@<VPS_IP>
|
|
|
|
# SSH into VPS as new user
|
|
ssh igny8@<VPS_IP>
|
|
|
|
# Edit SSH daemon config (as igny8 with sudo)
|
|
sudo vi /etc/ssh/sshd_config
|
|
|
|
# Make these changes:
|
|
# PermitRootLogin no
|
|
# PasswordAuthentication no
|
|
# PubkeyAuthentication yes
|
|
# X11Forwarding no
|
|
# PrintMotd no
|
|
# AcceptEnv LANG LC_*
|
|
# Subsystem sftp /usr/lib/openssh/sftp-server
|
|
|
|
# Restart SSH daemon
|
|
sudo systemctl restart sshd
|
|
|
|
# Test from another terminal to ensure key-based auth works
|
|
ssh igny8@<VPS_IP> # Should connect without password
|
|
```
|
|
|
|
#### Step 0.4: Configure UFW Firewall
|
|
|
|
```bash
|
|
# Enable UFW
|
|
sudo ufw enable
|
|
|
|
# Set default policies
|
|
sudo ufw default deny incoming
|
|
sudo ufw default allow outgoing
|
|
|
|
# Allow SSH (critical before locking down)
|
|
sudo ufw allow 22/tcp
|
|
|
|
# Allow HTTP/HTTPS (for Caddy)
|
|
sudo ufw allow 80/tcp
|
|
sudo ufw allow 443/tcp
|
|
|
|
# Verify rules
|
|
sudo ufw status verbose
|
|
|
|
# Output should show:
|
|
# To Action From
|
|
# -- ------ ----
|
|
# 22/tcp ALLOW Anywhere
|
|
# 80/tcp ALLOW Anywhere
|
|
# 443/tcp ALLOW Anywhere
|
|
# 22/tcp (v6) ALLOW Anywhere (v6)
|
|
# 80/tcp (v6) ALLOW Anywhere (v6)
|
|
# 443/tcp (v6) ALLOW Anywhere (v6)
|
|
```
|
|
|
|
#### Step 0.5: Install Docker & Docker Compose
|
|
|
|
```bash
|
|
# Add Docker repository
|
|
sudo apt-get update
|
|
sudo apt-get install -y \
|
|
apt-transport-https \
|
|
ca-certificates \
|
|
curl \
|
|
gnupg \
|
|
lsb-release
|
|
|
|
# Add Docker GPG key
|
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
|
|
sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
|
|
|
# Add Docker repository
|
|
echo \
|
|
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
|
|
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
|
|
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
|
|
|
# Install Docker Engine 29.x and Docker Compose 2.x
|
|
sudo apt-get update
|
|
sudo apt-get install -y \
|
|
docker-ce \
|
|
docker-ce-cli \
|
|
containerd.io \
|
|
docker-compose-plugin
|
|
|
|
# Verify Docker installation
|
|
sudo docker --version
|
|
# Should show: Docker version 29.x.x
|
|
|
|
sudo docker compose version
|
|
# Should show: Docker Compose version 2.x.x
|
|
|
|
# Add user to docker group (logout & login to take effect)
|
|
sudo usermod -aG docker igny8
|
|
|
|
# Test Docker without sudo (after logout/login)
|
|
docker run hello-world
|
|
```
|
|
|
|
#### Step 0.6: Create Directory Structure
|
|
|
|
```bash
|
|
# Create /data hierarchy
|
|
sudo mkdir -p /data/stack
|
|
sudo mkdir -p /data/app
|
|
sudo mkdir -p /data/logs
|
|
sudo mkdir -p /data/backups/postgres
|
|
sudo mkdir -p /data/backups/redis
|
|
sudo mkdir -p /data/backups/caddy
|
|
|
|
# Set ownership and permissions
|
|
sudo chown -R igny8:igny8 /data
|
|
sudo chmod -R 755 /data
|
|
```
|
|
|
|
#### Step 0.7: Clone igny8-stack Repository
|
|
|
|
```bash
|
|
# Navigate to /data/stack
|
|
cd /data/stack
|
|
|
|
# Clone repository (adjust URL as needed)
|
|
git clone https://github.com/igny8/igny8-stack.git .
|
|
|
|
# Verify repository contents
|
|
ls -la /data/stack
|
|
# Should contain: docker-compose.infra.yml, Caddyfile, install.sh, etc.
|
|
```
|
|
|
|
### 5.3 Phase 1: Docker Network & Infrastructure Setup
|
|
|
|
#### Step 1.1: Create External Docker Network
|
|
|
|
```bash
|
|
# Create the igny8_net external bridge network
|
|
docker network create igny8_net
|
|
|
|
# Verify network creation
|
|
docker network ls | grep igny8_net
|
|
docker network inspect igny8_net
|
|
```
|
|
|
|
#### Step 1.2: Configure Environment Variables
|
|
|
|
```bash
|
|
# Create .env file at /data/stack/.env
|
|
cd /data/stack
|
|
cat > .env << 'EOF'
|
|
# PostgreSQL
|
|
DB_PASSWORD=<GENERATE_STRONG_PASSWORD>
|
|
DB_USER=igny8
|
|
|
|
# PgAdmin
|
|
PGADMIN_PASSWORD=<GENERATE_STRONG_PASSWORD>
|
|
|
|
# Portainer
|
|
PORTAINER_PASSWORD=<GENERATE_STRONG_PASSWORD>
|
|
|
|
# Application Domains (for reference in Caddyfile)
|
|
PROD_DOMAIN=igny8.com
|
|
PROD_APP_DOMAIN=app.igny8.com
|
|
PROD_API_DOMAIN=api.igny8.com
|
|
|
|
STAGING_DOMAIN=staging.igny8.com
|
|
STAGING_APP_DOMAIN=staging.igny8.com
|
|
STAGING_API_DOMAIN=staging-api.igny8.com
|
|
|
|
# Test Domains (temporary, point to new VPS during migration)
|
|
TEST_DOMAIN=test-marketing.igny8.com
|
|
TEST_APP_DOMAIN=test-app.igny8.com
|
|
TEST_API_DOMAIN=test-api.igny8.com
|
|
EOF
|
|
|
|
# Secure .env file permissions
|
|
chmod 600 .env
|
|
```
|
|
|
|
#### Step 1.3: Configure Caddyfile
|
|
|
|
```bash
|
|
# Edit /data/stack/Caddyfile
|
|
cat > /data/stack/Caddyfile << 'EOF'
|
|
# Global settings
|
|
{
|
|
email admin@igny8.com
|
|
}
|
|
|
|
# Production domains (DNS still on old VPS — these won't receive traffic yet)
|
|
app.igny8.com {
|
|
reverse_proxy igny8_backend:8010
|
|
}
|
|
|
|
api.igny8.com {
|
|
reverse_proxy igny8_backend:8010
|
|
}
|
|
|
|
igny8.com, www.igny8.com {
|
|
reverse_proxy igny8_marketing:3000
|
|
}
|
|
|
|
# Test domains (DNS points to THIS new VPS — use these for validation)
|
|
test-app.igny8.com {
|
|
reverse_proxy igny8_backend:8010
|
|
}
|
|
|
|
test-api.igny8.com {
|
|
reverse_proxy igny8_backend:8010
|
|
}
|
|
|
|
test-marketing.igny8.com {
|
|
reverse_proxy igny8_marketing:3000
|
|
}
|
|
|
|
# Staging domains
|
|
staging.igny8.com, www.staging.igny8.com {
|
|
reverse_proxy igny8_staging_marketing:3000
|
|
}
|
|
|
|
staging-app.igny8.com {
|
|
reverse_proxy igny8_staging_backend:8010
|
|
}
|
|
|
|
staging-api.igny8.com {
|
|
reverse_proxy igny8_staging_backend:8010
|
|
}
|
|
|
|
# Management services
|
|
portainer.igny8.com {
|
|
reverse_proxy portainer:9000
|
|
}
|
|
|
|
pgadmin.igny8.com {
|
|
reverse_proxy pgadmin:5050
|
|
}
|
|
|
|
filebrowser.igny8.com {
|
|
reverse_proxy filebrowser:8080
|
|
}
|
|
EOF
|
|
|
|
# Verify Caddyfile syntax
|
|
docker run --rm -v /data/stack/Caddyfile:/etc/caddy/Caddyfile caddy:2-alpine caddy validate
|
|
```
|
|
|
|
**Key Points:**
|
|
- Production domains (app.igny8.com, api.igny8.com, igny8.com) are configured but DNS still points to old VPS
|
|
- Test subdomains (test-app.igny8.com, test-api.igny8.com, test-marketing.igny8.com) route to SAME backend containers — they're just different hostnames
|
|
- Caddy uses Let's Encrypt for automatic HTTPS on test domains
|
|
- No Cloudflare ACME tokens required in this phase
|
|
|
|
#### Step 1.4: Run Docker Compose (Infrastructure)
|
|
|
|
```bash
|
|
# Navigate to stack directory
|
|
cd /data/stack
|
|
|
|
# Start infrastructure services
|
|
docker compose -f docker-compose.infra.yml up -d
|
|
|
|
# Verify containers are running
|
|
docker ps
|
|
|
|
# Expected output should show:
|
|
# - postgres (postgres:18-alpine)
|
|
# - redis (redis:8-alpine)
|
|
# - caddy (caddy:2-alpine)
|
|
# - portainer (portainer/portainer-ce:lts)
|
|
# - pgadmin (dpage/pgadmin4:latest)
|
|
# - filebrowser (filebrowser/filebrowser:latest)
|
|
|
|
# Check container logs for errors
|
|
docker compose -f docker-compose.infra.yml logs -f
|
|
|
|
# Common issues:
|
|
# - Port 5432 already in use: Check existing postgres instances
|
|
# - Network error: Ensure igny8_net was created (Step 1.1)
|
|
# - Permission denied on volumes: Check /data ownership (Step 0.6)
|
|
```
|
|
|
|
#### Step 1.5: Test Subdomain DNS Configuration
|
|
|
|
At your CURRENT DNS provider (NOT Cloudflare):
|
|
|
|
1. Create A records for test subdomains pointing to NEW VPS IP:
|
|
- `test-app.igny8.com` → `<NEW_VPS_IP>` (not proxied, DNS only)
|
|
- `test-api.igny8.com` → `<NEW_VPS_IP>` (not proxied, DNS only)
|
|
- `test-marketing.igny8.com` → `<NEW_VPS_IP>` (not proxied, DNS only)
|
|
|
|
2. Leave production domains unchanged:
|
|
- `app.igny8.com` → OLD VPS IP (still healthy)
|
|
- `api.igny8.com` → OLD VPS IP (still healthy)
|
|
- `igny8.com` → OLD VPS IP (still healthy)
|
|
|
|
3. Verify DNS propagation:
|
|
```bash
|
|
nslookup test-app.igny8.com
|
|
nslookup test-api.igny8.com
|
|
nslookup test-marketing.igny8.com
|
|
# All should resolve to NEW_VPS_IP
|
|
```
|
|
|
|
### 5.4 Phase 2: Verification & Health Checks
|
|
|
|
#### Step 2.1: Container Health Verification
|
|
|
|
```bash
|
|
# Check all infrastructure containers
|
|
docker ps --format "table {{.Names}}\t{{.Status}}"
|
|
|
|
# Expected output:
|
|
# NAMES STATUS
|
|
# caddy Up X minutes
|
|
# portainer Up X minutes
|
|
# pgadmin Up X minutes
|
|
# filebrowser Up X minutes
|
|
# postgres Up X minutes
|
|
# redis Up X minutes
|
|
|
|
# Check logs for errors
|
|
docker compose -f docker-compose.infra.yml logs | grep -i "error"
|
|
# Should return no results or only informational messages
|
|
```
|
|
|
|
#### Step 2.2: Network Connectivity Tests
|
|
|
|
```bash
|
|
# Verify igny8_net connectivity
|
|
docker network inspect igny8_net
|
|
|
|
# Test PostgreSQL connectivity from another container
|
|
docker run --rm --network igny8_net postgres:18-alpine \
|
|
psql -h postgres -U igny8 -c "SELECT version();"
|
|
# Prompt for password (DB_PASSWORD from .env)
|
|
|
|
# Test Redis connectivity
|
|
docker run --rm --network igny8_net redis:8-alpine \
|
|
redis-cli -h redis PING
|
|
# Should return "PONG"
|
|
```
|
|
|
|
#### Step 2.3: Test Subdomain HTTPS Tests
|
|
|
|
```bash
|
|
# Test Let's Encrypt certificate provisioning on test domains
|
|
curl -I https://test-app.igny8.com
|
|
# Expected: 200 or 502 (container not deployed yet, which is fine)
|
|
# Important: Should NOT show certificate errors
|
|
|
|
curl -I https://test-api.igny8.com
|
|
# Expected: 200 or 502
|
|
|
|
curl -I https://test-marketing.igny8.com
|
|
# Expected: 200 or 502
|
|
|
|
# Verify certificate details
|
|
openssl s_client -connect test-app.igny8.com:443 -servername test-app.igny8.com < /dev/null | grep -A 2 "Issuer"
|
|
# Should show: Issuer: C = US, O = Let's Encrypt, CN = R3 (or similar)
|
|
|
|
# Check Caddy logs for certificate provisioning
|
|
docker logs caddy | grep -i "test-app\|test-api\|test-marketing"
|
|
```
|
|
|
|
#### Step 2.4: Production Domain Verification
|
|
|
|
```bash
|
|
# Verify production domains still resolve to OLD VPS (they should not change yet)
|
|
nslookup app.igny8.com
|
|
nslookup api.igny8.com
|
|
nslookup igny8.com
|
|
# Should show OLD VPS IP, not NEW VPS IP
|
|
|
|
# This confirms zero impact on production during provisioning
|
|
```
|
|
|
|
#### Step 2.5: System Resource Verification
|
|
|
|
```bash
|
|
# Check disk usage
|
|
df -h /data
|
|
|
|
# Check memory usage
|
|
free -h
|
|
|
|
# Check Docker disk usage
|
|
docker system df
|
|
|
|
# Monitor running processes
|
|
top -b -n 1 | head -n 20
|
|
```
|
|
|
|
#### Step 2.6: Backup & Persistence Verification
|
|
|
|
```bash
|
|
# Verify database file exists
|
|
ls -lh /data/backups/postgres/
|
|
|
|
# Verify Redis persistence
|
|
ls -lh /data/backups/redis/
|
|
|
|
# Check Caddy certificate storage
|
|
ls -lh /data/backups/caddy/
|
|
|
|
# All directories should contain actual data files
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Acceptance Criteria
|
|
|
|
The VPS provisioning is complete when ALL of the following are satisfied:
|
|
|
|
### 6.1 System Requirements
|
|
|
|
- [ ] Ubuntu 24.04 LTS confirmed via `lsb_release -a`
|
|
- [ ] SSH access via key-based authentication only
|
|
- [ ] Root password login disabled
|
|
- [ ] UFW firewall enabled with only ports 22, 80, 443 open
|
|
- [ ] System time synchronized (UTC timezone)
|
|
- [ ] Non-root user (`igny8`) created with sudo access
|
|
|
|
### 6.2 Docker & Containerization
|
|
|
|
- [ ] Docker Engine 29.x running (`docker --version`)
|
|
- [ ] Docker Compose 2.x+ installed (`docker compose version`)
|
|
- [ ] User `igny8` can run docker commands without sudo
|
|
- [ ] External network `igny8_net` exists and is accessible
|
|
|
|
### 6.3 Directory Structure
|
|
|
|
- [ ] `/data/stack/` contains igny8-stack repository with docker-compose.infra.yml and Caddyfile
|
|
- [ ] `/data/app/` exists and is owned by igny8:igny8
|
|
- [ ] `/data/logs/` exists and is writable
|
|
- [ ] `/data/backups/postgres/` contains PostgreSQL data
|
|
- [ ] `/data/backups/redis/` contains Redis persistence files
|
|
- [ ] `/data/backups/caddy/` contains Caddy certificates and Let's Encrypt data
|
|
|
|
### 6.4 Infrastructure Services
|
|
|
|
- [ ] PostgreSQL 18 container running and healthy
|
|
- Verification: `docker exec postgres psql -U igny8 -c "SELECT 1"`
|
|
- [ ] Redis 8.x container running and healthy
|
|
- Verification: `docker exec redis redis-cli PING` returns `PONG`
|
|
- [ ] Caddy 2.11.x container running and listening on 80/443
|
|
- Verification: `curl -I https://test-app.igny8.com` succeeds (no cert errors)
|
|
- [ ] Portainer container running and accessible
|
|
- Verification: `curl -I https://portainer.igny8.com` succeeds
|
|
- [ ] PgAdmin container running and accessible
|
|
- Verification: `curl -I https://pgadmin.igny8.com` succeeds
|
|
- [ ] FileBrowser container running and accessible
|
|
- Verification: `curl -I https://filebrowser.igny8.com` succeeds
|
|
|
|
### 6.5 DNS & Test Subdomains
|
|
|
|
- [ ] Test subdomains created at CURRENT DNS provider pointing to NEW VPS IP
|
|
- [ ] Production domains UNCHANGED at DNS provider (still point to OLD VPS)
|
|
- [ ] DNS propagates successfully: `nslookup test-app.igny8.com` resolves to NEW VPS IP
|
|
- [ ] HTTPS certificates auto-provisioned by Caddy via Let's Encrypt on test subdomains
|
|
- [ ] Zero impact on production: all production domains still accessible on old VPS
|
|
|
|
### 6.6 Security & Hardening
|
|
|
|
- [ ] SSH daemon configured for key-only authentication
|
|
- [ ] Root account password login disabled
|
|
- [ ] UFW firewall enforced with minimal rule set
|
|
- [ ] `.env` file has 600 permissions (readable only by owner)
|
|
- [ ] `/data/` directory owned by igny8:igny8
|
|
- [ ] No exposed ports beyond 22, 80, 443
|
|
|
|
### 6.7 Documentation & Handoff
|
|
|
|
- [ ] This document (00B) completed and validated
|
|
- [ ] Next phases (00C production migration, 00D staging setup) have actionable prerequisites
|
|
- [ ] All credentials stored securely (not in git, only in .env with 600 permissions)
|
|
- [ ] Docker Compose files checked into source control
|
|
|
|
---
|
|
|
|
## 7. Claude Code Instructions
|
|
|
|
These instructions enable Claude Code to provision the VPS or verify existing provisioning.
|
|
|
|
### 7.1 Provisioning Automation
|
|
|
|
**Objective:** Automate VPS setup from SSH access
|
|
|
|
**Files to Create/Modify:**
|
|
- `/data/stack/.env` (environment variables — NO Cloudflare tokens)
|
|
- `/data/stack/docker-compose.infra.yml` (infrastructure services)
|
|
- `/data/stack/Caddyfile` (reverse proxy config with test subdomains)
|
|
|
|
**Shell Scripts to Execute:**
|
|
1. System update & security setup (Steps 0.1-0.4)
|
|
2. Docker installation (Step 0.5)
|
|
3. Directory structure creation (Step 0.6)
|
|
4. Repository clone (Step 0.7)
|
|
5. Network & Docker Compose startup (Steps 1.1-1.4)
|
|
|
|
**Inputs Needed:**
|
|
- VPS IP address
|
|
- SSH key path
|
|
- Strong random passwords for services
|
|
- Current DNS provider credentials (for test subdomain creation)
|
|
|
|
**Expected Outputs:**
|
|
- Fully running infrastructure stack with PostgreSQL 18, Redis 8, Caddy 2.11
|
|
- All containers healthy
|
|
- Test subdomains accessible via HTTPS (Let's Encrypt certs)
|
|
- Production domains unchanged on old VPS
|
|
- No errors in logs
|
|
- Passes all acceptance criteria (Section 6)
|
|
|
|
### 7.2 Verification Automation
|
|
|
|
**Objective:** Verify existing provisioning is healthy
|
|
|
|
**Checks to Perform:**
|
|
1. SSH connectivity to VPS
|
|
2. UFW firewall rules (22, 80, 443 only)
|
|
3. Docker services running count
|
|
4. Network igny8_net exists
|
|
5. PostgreSQL 18 responsive
|
|
6. Redis 8 responsive
|
|
7. Caddy 2.11 listening on 80/443
|
|
8. DNS resolution for test subdomains (NEW VPS)
|
|
9. DNS resolution for production domains (OLD VPS — should not change)
|
|
10. HTTPS certificate provisioning on test subdomains
|
|
11. No Cloudflare API tokens in .env (should not exist yet)
|
|
|
|
**Expected Outcomes:**
|
|
- All checks pass
|
|
- No orphaned containers
|
|
- No disk space warnings
|
|
- Logs show no errors
|
|
- Test subdomains fully functional
|
|
- Production domains unaffected
|
|
|
|
### 7.3 Integration with Downstream Phases
|
|
|
|
**Dependencies for 00C (Production Migration):**
|
|
- Infrastructure provisioning complete (this phase)
|
|
- docker-compose.app.yml ready in `/data/app/`
|
|
- Production application images built and available
|
|
- Test subdomains validated and working
|
|
- Plan for DNS cutover (moving production domains to new VPS)
|
|
|
|
**Dependencies for 00D (Staging Setup):**
|
|
- Infrastructure provisioning complete (this phase)
|
|
- Staging application images available
|
|
- docker-compose.app.yml configured for staging
|
|
- Staging domains configured in Caddyfile (already done in this phase)
|
|
|
|
### 7.4 Troubleshooting Runbook
|
|
|
|
**If PostgreSQL 18 fails to start:**
|
|
```bash
|
|
# Check volume permissions
|
|
ls -lh /data/backups/postgres/
|
|
|
|
# Check logs
|
|
docker logs postgres | tail -20
|
|
|
|
# Verify image version
|
|
docker images | grep postgres
|
|
# Should show postgres:18-alpine
|
|
|
|
# Recreate volume if corrupted
|
|
docker compose -f docker-compose.infra.yml down
|
|
docker compose -f docker-compose.infra.yml up -d postgres
|
|
```
|
|
|
|
**If Caddy fails to provision Let's Encrypt certificates on test subdomains:**
|
|
```bash
|
|
# Verify DNS resolution for test subdomains
|
|
nslookup test-app.igny8.com
|
|
# Should resolve to NEW_VPS_IP
|
|
|
|
# Check Caddy logs
|
|
docker logs caddy | grep -i "test-app\|error\|acme"
|
|
|
|
# Verify test subdomain A records exist at current DNS provider
|
|
# (NOT Cloudflare — your current DNS provider)
|
|
|
|
# Force certificate renewal
|
|
docker compose -f docker-compose.infra.yml restart caddy
|
|
```
|
|
|
|
**If Docker network unreachable:**
|
|
```bash
|
|
# Verify network exists
|
|
docker network ls | grep igny8_net
|
|
|
|
# Recreate if missing
|
|
docker network create igny8_net
|
|
|
|
# Restart services
|
|
docker compose -f docker-compose.infra.yml restart
|
|
```
|
|
|
|
**If SSH key auth fails:**
|
|
```bash
|
|
# Verify SSH config
|
|
sudo cat /etc/ssh/sshd_config | grep -i "pubkey\|password"
|
|
|
|
# Check authorized_keys
|
|
cat ~/.ssh/authorized_keys
|
|
|
|
# Ensure correct permissions
|
|
chmod 700 ~/.ssh
|
|
chmod 600 ~/.ssh/authorized_keys
|
|
```
|
|
|
|
**If production domains become inaccessible:**
|
|
```bash
|
|
# Verify production domains still resolve to OLD VPS IP
|
|
nslookup app.igny8.com
|
|
nslookup api.igny8.com
|
|
nslookup igny8.com
|
|
|
|
# These should NOT resolve to NEW VPS IP at this stage
|
|
# If they do, reverse the DNS change immediately
|
|
|
|
# Verify old VPS is still running (contact infrastructure team)
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Cross-References
|
|
|
|
- **00A:** IGNY8 Phase 0 Overview (prerequisites, timeline, team)
|
|
- **00C:** Production Migration (deploying production apps + DNS cutover)
|
|
- **00D:** Staging Setup (deploying staging environment)
|
|
- **Appendix:** igny8-stack repository documentation
|
|
|
|
---
|
|
|
|
## 9. Sign-Off
|
|
|
|
**Document Owner:** Infrastructure Team
|
|
**Last Updated:** 2026-03-23
|
|
**Approval Status:** Pending Review
|
|
|
|
| Role | Name | Signature | Date |
|
|
|------|------|-----------|------|
|
|
| Infrastructure Lead | — | — | — |
|
|
| DevOps Engineer | — | — | — |
|
|
| Project Manager | — | — | — |
|
|
|
|
---
|
|
|
|
## Appendix: Quick Reference Commands
|
|
|
|
### Essential Docker Commands
|
|
|
|
```bash
|
|
# View all services
|
|
docker ps -a
|
|
|
|
# View infrastructure logs
|
|
docker compose -f docker-compose.infra.yml logs -f
|
|
|
|
# Restart a service
|
|
docker compose -f docker-compose.infra.yml restart <service_name>
|
|
|
|
# Execute command in container
|
|
docker exec <container_name> <command>
|
|
|
|
# Connect to PostgreSQL
|
|
docker exec -it postgres psql -U igny8 -d igny8
|
|
|
|
# Check network connectivity
|
|
docker network inspect igny8_net
|
|
```
|
|
|
|
### Essential System Commands
|
|
|
|
```bash
|
|
# Check disk usage
|
|
df -h /data
|
|
|
|
# Check memory
|
|
free -h
|
|
|
|
# Monitor processes
|
|
htop
|
|
|
|
# View firewall rules
|
|
sudo ufw status verbose
|
|
|
|
# Check SSH connectivity
|
|
ssh -v igny8@<VPS_IP>
|
|
|
|
# DNS propagation check
|
|
dig test-app.igny8.com +short
|
|
dig app.igny8.com +short
|
|
```
|
|
|
|
### Backup & Recovery
|
|
|
|
```bash
|
|
# Backup PostgreSQL data
|
|
docker exec postgres pg_dump -U igny8 igny8 > backup.sql
|
|
|
|
# Backup Redis data
|
|
docker exec redis redis-cli --rdb /data/backups/redis/dump-$(date +%s).rdb
|
|
|
|
# List all backups
|
|
ls -lh /data/backups/
|
|
```
|
|
|
|
### DNS Verification (Critical During Migration)
|
|
|
|
```bash
|
|
# Verify test subdomains point to NEW VPS (should work)
|
|
nslookup test-app.igny8.com
|
|
nslookup test-api.igny8.com
|
|
nslookup test-marketing.igny8.com
|
|
|
|
# Verify production domains point to OLD VPS (should NOT change yet)
|
|
nslookup app.igny8.com
|
|
nslookup api.igny8.com
|
|
nslookup igny8.com
|
|
|
|
# Test HTTPS on test subdomains
|
|
curl -I https://test-app.igny8.com
|
|
|
|
# Do NOT test production domains on new VPS yet (they're still on old VPS)
|
|
```
|