Files
voidea/VPS_GUIDE.md
T

8.6 KiB

VPS Infrastructure Guide for AI Agents

Overview

This document describes the VPS infrastructure at 81.177.141.34 to help AI agents migrate or deploy new projects (e.g., voideaai.ru).


1. VPS Specifications

Property Value
IP 81.177.141.34
User angel
Password .-SHGWa_
Sudo passwordless via /etc/sudoers.d/angel
SSH key added to ~/.ssh/authorized_keys
OS Ubuntu 24.04 (Noble)
Kernel OpenVZ/LXC container — limited kernel modules
Network interface venet0 (no bridge NAT support)
Disk 20GB total, ~13GB available
Docker 29.5.0, iptables in nft mode (iptables-nft)

2. Docker Constraints

Critical: network_mode: host only

Bridge networking (network_mode: bridge) does not work on this VPS. The OpenVZ/LXC kernel does not support masquerade for bridge interfaces through venet0. All containers must use:

network_mode: host

Docker daemon config (/etc/docker/daemon.json)

{"dns": ["8.8.8.8", "8.8.4.4"]}

Build constraint

All Docker builds require:

build:
  network: host

Without this, apt-get and pip cannot download packages (no bridge NAT).

Docker Hub connectivity issues

Pulling large images (>100MB) from Docker Hub may time out (failed to copy: read tcp... connection timed out). Smaller images (nginx:alpine, postgres:16-alpine) work. Gitea and Portainer images cannot be pulled reliably.

Solutions attempted:

  • Retry with --platform linux/amd64
  • Increase max-download-attempts (not supported in Docker 29.5)
  • Clean build cache with docker builder prune -af
  • All fail on large image layers

Workaround: Pull images locally and SCP (docker save → scp → docker load), or use a registry mirror.


3. Port Allocation (all on host)

Port Service Container
80 nginx-proxy HTTP nginx-proxy
443 nginx-proxy HTTPS nginx-proxy
8080 PHP nginx (aegisone.ru) aegisone-php-nginx
9000 PHP-FPM aegisone-php-fpm
8000 FastAPI (service.aegisone.ru) aegisone-app
5432 PostgreSQL (shared, aegisone) aegisone-postgres
5433 PostgreSQL (Gitea) gitea-db (planned)
3000 Gitea gitea (planned)
9001 Portainer HTTP portainer (planned)
9443 Portainer HTTPS portainer (planned)

Address resolution between containers

Since all containers use network_mode: host, they communicate via localhost:PORT. Docker DNS names do not resolve. In nginx.conf and app configs, use:

proxy_pass http://localhost:8080;  # NOT container_name:port

4. Project Structure

/opt/projects/
  nginx-proxy/          # Reverse proxy (nginx + certbot)
    docker-compose.yml
    nginx.conf
    conf.d/             # Per-domain configs
    certs/              # SSL certificates
    html/               # Webroot for certbot
  aegisone-php/         # PHP frontend (aegisone.ru)
    docker-compose.yml
    Dockerfile
    nginx.conf          # PHP nginx config (listen 8080)
    src/                # PHP source files
  aegisone-py/          # FastAPI backend (service.aegisone.ru)
    docker-compose.yml
    Dockerfile
    app/                # Python source
    sql/                # DB schema
  gitea/                # Git hosting (planned)
    docker-compose.yml
  portainer/            # Docker UI (planned)
    docker-compose.yml

5. nginx-proxy Configuration Pattern

Global config (nginx.conf)

  • resolver 127.0.0.11 — not used in host mode, kept for compatibility
  • SSL termination handled by nginx-proxy
  • Upstream protocols are HTTP (nginx-proxy terminates SSL and proxies via HTTP)

Conf.d file structure

Each domain has one .conf file with two server blocks:

# HTTP (no redirect — temporary, until LE certs)
server {
    listen 80;
    server_name example.ru;

    location / {
        proxy_pass http://localhost:PORT;
        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;
    }
}

# HTTPS (self-signed, for future LE)
server {
    listen 443 ssl;
    http2 on;
    server_name example.ru;

    ssl_certificate /etc/nginx/certs/live/aegisone.ru/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/live/aegisone.ru/privkey.pem;

    # ... same location block
}

Note: HTTP→HTTPS redirects (return 301 https://...) caused redirect loops externally due to provider/proxy intercepting port 443. If you add them, test thoroughly.

SSL certificates

Currently using self-signed certs generated via:

sudo openssl req -x509 -nodes -days 30 -newkey rsa:2048 \
    -keyout /opt/projects/nginx-proxy/certs/live/aegisone.ru/privkey.pem \
    -out /opt/projects/nginx-proxy/certs/live/aegisone.ru/fullchain.pem \
    -subj '/CN=aegisone.ru'

For Let's Encrypt:

cd /opt/projects/nginx-proxy
docker compose run --rm certbot certonly --webroot \
    --webroot-path=/var/www/certbot \
    -d example.ru \
    --email admin@example.ru \
    --agree-tos --non-interactive

6. Deployment Workflow

Initial setup

  1. Prepare files locally (this repo)
  2. Pack into tarball: tar czf project.tar.gz .
  3. SCP to VPS: scp project.tar.gz angel@81.177.141.34:/home/angel/
  4. SSH and extract: ssh angel@81.177.141.34 "tar xzf project.tar.gz -C /opt/projects/project-name/"
  5. Build and start: ssh angel@81.177.141.34 "cd /opt/projects/project-name && docker compose up -d --build"

Updating a running project

  1. Edit files locally
  2. SCP changed files to /home/angel/ on VPS
  3. SSH and copy to correct location: sudo cp /home/angel/file /opt/projects/project-name/
  4. Restart container: docker compose -p project-name restart service-name

Copying files into a running container (alternative to rebuild)

docker cp /home/angel/file.py container-name:/app/path/file.py
docker compose -p project-name restart app

7. Database

PostgreSQL (aegisone shared DB)

  • Host: localhost:5432
  • User: aegisone
  • Password: aegisone_pass
  • Database: aegisone
  • Container: aegisone-postgres (postgres:16-alpine)

Schema initialization

SQL files in /opt/projects/aegisone-py/sql/:

  • schema_postgresql.sql — full schema
  • seed_data.sql — seed data

Init scripts run only on first volume creation. To add missing tables later:

docker exec -i aegisone-postgres psql -U aegisone -d aegisone < /opt/projects/aegisone-py/sql/schema_postgresql.sql

Key schema pattern

  • All tables use BOOLEAN for is_active (not integer)
  • Timestamps use TIMESTAMP NOT NULL DEFAULT NOW()
  • updated_at triggers via update_updated_at_column() function

8. Useful Commands

# Check running containers
docker ps -a

# Check compose projects
docker compose ls

# View logs
docker logs container-name --tail 50

# Restart service
docker compose -p project-name restart service-name

# Rebuild and restart
docker compose -p project-name up -d --build

# Clean build cache
docker builder prune -af

# Test nginx config
docker compose -p nginx-proxy exec nginx nginx -t

# Reload nginx
docker compose -p nginx-proxy exec nginx nginx -s reload

# Check listening ports
ss -tlnp | grep -E ':(80|443|8080|8000|9000|5432)'

# Check disk
df -h /
docker system df

9. Adding a New Project (e.g., voideaai.ru)

Steps:

  1. Create directory: mkdir -p /opt/projects/voideaai
  2. Create docker-compose.yml with network_mode: host and build.network: host
  3. Create Dockerfile (use --only-binary :all: for pip if avoiding apt)
  4. Create nginx-proxy config: nginx/conf.d/voideaai.conf (listen 80, proxy_pass localhost:PORT)
  5. Generate cert if needed
  6. Restart nginx-proxy: docker compose -p nginx-proxy restart nginx
  7. Start project: cd /opt/projects/voideaai && docker compose up -d --build

Port allocation for new project

Check existing ports first (ss -tlnp). Choose an unused port for the app and add it to the nginx-proxy config.


10. Known Issues

  1. Docker Hub timeouts on large images — consider pulling on local machine and transferring with docker save/docker load
  2. HTTPS redirect loops externally — port 443 may be intercepted by provider/proxy; test HTTP first
  3. Bridge networking doesn't work — all containers must use network_mode: host
  4. Docker build needs network: host — without it, apt-get/pip fail
  5. Config files with $ in content — when editing via SSH from PowerShell, $variables get expanded; use SCP (write locally, then copy)