Skip to content

RandomSynergy17/RanSynSrv

Repository files navigation

πŸŒ€ RanSynSrv

Batteries-included PHP 8.4 hosting container with real-time analytics, a web terminal, Claude Code CLI, and an optional pgvector-powered RAG sidecar.

GHCR Build Image Size Platforms License

Alpine Linux Nginx PHP GoAccess s6-overlay Claude Code pgvector TEI

Quick Start β€’ Features β€’ Configuration β€’ AI Sidecar β€’ Troubleshooting


🎯 Overview

RanSynSrv is a single-container PHP 8.4 hosting stack on Alpine Linux, wired with everything you need to run small-to-medium PHP apps in production or development:

  • Nginx fronting PHP-FPM 8.4 over a Unix socket, supervised by s6-overlay v3.
  • GoAccess writing a live analytics dashboard from the access log.
  • ttyd serving a browser-accessible zsh shell (optional, HTTP-Basic-auth protected).
  • Claude Code CLI baked in so you can docker exec into an AI-assisted workflow.
  • 45+ PHP extensions, NVM, Python, Ripgrep, FZF, git-delta, and the usual dev tools pre-installed.
  • An optional AI sidecar overlay (docker-compose.ai.yml) that adds pgvector-enabled Postgres 17 + a local text-embedding service (HuggingFace TEI serving BGE-small-en-v1.5) on the same compose network.

Designed to run behind a reverse proxy (Traefik, Caddy, Nginx Proxy Manager, etc.). HTTP only β€” the reverse proxy owns TLS.


✨ Features

Core stack

πŸ”οΈ Alpine 3.21, multi-arch (amd64 + arm64)
🚦 s6-overlay supervision, not systemd
🐘 PHP 8.4 with 45+ extensions
🌐 Nginx with Brotli / headers-more / fancyindex
πŸ“Š GoAccess 1.9.4 real-time dashboard
πŸ–₯️ ttyd web terminal (opt-in, Basic-Auth)
πŸ€– Claude Code CLI pre-installed
πŸ™ GitHub CLI (gh) pre-installed
πŸ”‘ HTTP Basic auth for /goaccess (env-toggled)

Dev & ops

🧩 Runtime apk + pip installs via env
πŸ“ Single /data volume β€” trivial backups
πŸ“¦ Optional pgvector + TEI AI sidecar overlay
🩺 Healthcheck at /health
πŸ”’ no-new-privileges, non-root workers, 2G mem-cap
πŸͺ΅ File-based or stdout logging
πŸ” Zero-downtime nginx reload
πŸ§‘β€πŸ’» PUID/PGID mapping, no root files in your volume

🏁 Quick Start

The repo ships two compose files: docker-compose.yml builds the image from source (use this if you're modifying the Dockerfile); docker-compose.deploy.yml pulls the pre-built image from GHCR (use this for everything else β€” 10Γ— faster first start).

From GHCR (recommended)

# 1. Grab the deploy compose + env template
mkdir ransynsrv && cd ransynsrv
curl -fsSLO https://raw.githubusercontent.com/RandomSynergy17/RanSynSrv/main/docker-compose.deploy.yml
curl -fsSL  https://raw.githubusercontent.com/RandomSynergy17/RanSynSrv/main/.env.example -o .env

# 2. Set your user/group and (optionally) an API key
printf "\nPUID=%s\nPGID=%s\n" "$(id -u)" "$(id -g)" >> .env
nano .env                    # set ANTHROPIC_API_KEY if you want Claude Code

# 3. Up
docker compose -f docker-compose.deploy.yml up -d

# 4. Visit
open http://localhost:8080           # welcome page
open http://localhost:8080/goaccess  # analytics

From source

git clone https://github.com/RandomSynergy17/RanSynSrv && cd RanSynSrv
cp .env.example .env && nano .env
docker compose up -d --build

With the AI sidecar

echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)" >> .env
docker compose -f docker-compose.yml -f docker-compose.ai.yml up -d

# Enable the vector extension once per database
docker exec ransynsrv-postgres psql -U ransynsrv -d ransynsrv \
  -c "CREATE EXTENSION IF NOT EXISTS vector;"

See AI Sidecar Overlay below for the PHP-side usage pattern.

Bootstrap a new project with an LLM

SpinUp_LLM_Prompt.md is a ready-made prompt you can share with any LLM (Claude Code, ChatGPT, etc.) to scaffold a new app on this stack. The LLM will ask you structured questions about your deployment β€” stack name, Portainer host, port, volume path, public URL, API key, app requirements β€” then generate:

  • A stack.env file for Portainer
  • A complete Portainer stack YAML
  • A DEPLOY.md written to your data volume (initial + update procedure, Portainer IDs auto-filled)
  • The initial PHP/HTML/CSS app scaffold written directly to the volume via ssh-manager
  • A deployment summary table at the end with all URLs, ports, env vars, and Portainer IDs

πŸ“– Configuration

All tunables live in .env. Every variable has a safe default unless explicitly marked required.

Core container

Variable Default Purpose
PUID / PGID 1000 Host user/group mapping (run id $USER to check)
TZ Asia/Dubai Timezone
HTTP_PORT 8080 Published HTTP port on the host
DATA_PATH ./data Host path for persistent data
COMPOSE_PROJECT_NAME folder name Unique name for multi-instance deployments
ANTHROPIC_API_KEY (empty) Required if you want Claude Code CLI

PHP settings (runtime, applied at each boot)

Variable Default Purpose
PHP_MEMORY_LIMIT 256M PHP memory_limit
PHP_MAX_UPLOAD 50M upload_max_filesize
PHP_MAX_POST 50M post_max_size
PHP_MAX_EXECUTION_TIME 300 max_execution_time (seconds)

Logging & analytics

Variable Default Purpose
DOCKER_LOGS false true routes nginx + PHP logs to docker logs (disables GoAccess real-time updates β€” pick one)
GOACCESS_ENABLED true Set false to disable the dashboard
GOACCESS_WS_URL (empty) Match your deployment URL β€” e.g. ws://localhost:8080/goaccess/ws locally or wss://yourdomain.com/goaccess/ws behind HTTPS
GOACCESS_AUTH_ENABLED false true gates /goaccess behind HTTP Basic auth
GOACCESS_USERNAME / GOACCESS_PASSWORD admin / β€” Basic auth credentials when auth is enabled

Optional services

Variable Default Purpose
TTYD_ENABLED false true enables the /ttyd web terminal
TTYD_USERNAME / TTYD_PASSWORD admin / β€” Required when ttyd is on
INSTALL_PACKAGES (empty) Space-separated apk packages installed at init (e.g. mc tig ncdu)
INSTALL_PIP_PACKAGES (empty) Space-separated pip packages installed at init

AI sidecar overlay

Variable Default Purpose
POSTGRES_USER ransynsrv Postgres superuser for the app DB
POSTGRES_PASSWORD β€” Required when the overlay is active
POSTGRES_DB ransynsrv Initial database
POSTGRES_HOST_PORT 5432 Host port (bound to 127.0.0.1 by default)
POSTGRES_BIND_ADDR 127.0.0.1 Set to 0.0.0.0 to expose Postgres on all interfaces
POSTGRES_MEM_LIMIT 1g Container memory cap
EMBEDDER_MODEL BAAI/bge-small-en-v1.5 HuggingFace model id served by TEI
EMBEDDER_HOST_PORT 7997 Host port for direct embedder access
EMBEDDER_BIND_ADDR 127.0.0.1 Set to 0.0.0.0 to expose the embedder
EMBEDDER_MEM_LIMIT 2g Container memory cap (BGE-small ~500MB, BGE-M3 ~4GB β€” size accordingly)

Full reference: .env.example.


πŸ—οΈ Architecture

╔══════════════════════════════ ransynsrv container ═══════════════════════════════╗
β•‘                                                                                  β•‘
β•‘                       β”Œβ”€β”€[ s6-overlay v3 supervisor ]──┐                         β•‘
β•‘                       β””β”€β”€β”¬β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                         β•‘
β•‘                          β”‚    β”‚    β”‚    β”‚                                        β•‘
β•‘     β”Œβ”€β”€ init-ransynsrv β”€β”€β”˜    β”‚    β”‚    └── svc-ttyd ── lo:7681                  β•‘
β•‘     β”‚  (oneshot, blocks)      β”‚    β”‚        (no -c; auth at nginx layer)         β•‘
β•‘     β”‚                         β”‚    β”‚                                             β•‘
β•‘     β”‚  first-boot once:       β”‚    └── svc-php-fpm ── /run/php/php-fpm.sock     β•‘
β•‘     β”‚  β€’ mkdir /data tree     β”‚        pool: abc   clear_env=no                  β•‘
β•‘     β”‚  β€’ install defaults     β”‚        99-ransynsrv.ini regen from PHP_* env     β•‘
β•‘     β”‚  β€’ PUID/PGID chown -R   β”‚                                                  β•‘
β•‘     β”‚    β†’ sentinel .init-doneβ”‚    β”Œβ”€β”€ svc-nginx ── :80                          β•‘
β•‘     β”‚                         └─────   workers: nginx                            β•‘
β•‘     β”‚  every boot:                 β”‚   proxies /health  /goaccess  /ttyd         β•‘
β•‘     β”‚  β€’ INSTALL_PACKAGES (apk)    β”‚   nginx Basic-auth on /goaccess + /ttyd     β•‘
β•‘     β”‚  β€’ regen php-timeout.conf   β”€β”˜                                             β•‘
β•‘     β”‚  β€’ rewrite GOACCESS_AUTH                                                   β•‘
β•‘     β”‚    + TTYD_AUTH blocks        β”Œβ”€β”€ svc-goaccess ── :7890 (internal WS)       β•‘
β•‘     β”‚    in nginx.conf             β”‚   reads access.log β†’ writes index.html      β•‘
β•‘     β”‚  β€’ bcrypt htpasswd β†’ β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                             β•‘
β•‘     β”‚    /data/nginx/.{goaccess,ttyd}-htpasswd                                   β•‘
β•‘     β”‚  β€’ DOCKER_LOGS=true β†’ symlink logs to /proc/1/fd/{1,2}                     β•‘
β•‘     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                                         β•‘
β•‘                                                                                  β•‘
β•‘     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ /data volume (bind-mount) ───────────────────┐         β•‘
β•‘     β”‚  webroot/{public_html, src}          claude/.claude/             β”‚         β•‘
β•‘     β”‚  nginx/nginx.conf   (user-editable)  commandhistory/             β”‚         β•‘
β•‘     β”‚  nginx/.{goaccess,ttyd}-htpasswd     ssh/            (0700)      β”‚         β•‘
β•‘     β”‚  nginx/php-timeout.conf              log/{nginx,php} (0640)      β”‚         β•‘
β•‘     β”‚  databases/                          .ransynsrv-init-done        β”‚         β•‘
β•‘     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

      β–² http://host:8080       β—† /health     β—† /goaccess    β—† /ttyd
      β”‚
 β”Œβ”€β”€ optional AI sidecar overlay β€” docker-compose.ai.yml ──────────────────────┐
 β”‚                                                                             β”‚
 β”‚  ai-init ── oneshot ── chowns ./data/{postgres,tei-cache} to PUID:PGID      β”‚
 β”‚      β”‚                                                                      β”‚
 β”‚      β”œβ”€β–Ί postgres    pgvector/pgvector:0.8.0-pg17    dns: postgres         β”‚
 β”‚      β”‚               127.0.0.1:${POSTGRES_HOST_PORT:-5432}:5432             β”‚
 β”‚      β”‚               runs as ${PUID}:${PGID} β€” host-readable PGDATA         β”‚
 β”‚      β”‚                                                                      β”‚
 β”‚      └─► embedder    HF TEI cpu-1.5 / ${EMBEDDER_MODEL}    dns: embedder   β”‚
 β”‚                      127.0.0.1:${EMBEDDER_HOST_PORT:-7997}:80               β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Layer Process User Port
Init (oneshot) init-ransynsrv root β€”
Web server nginx master root 80
Web server nginx workers nginx β€”
Application php-fpm master root β€”
Application php-fpm pool abc unix socket
Analytics goaccess root 7890 (internal)
Terminal ttyd abc 7681 (loopback)

Services are defined under root/etc/s6-overlay/s6-rc.d/. All longruns depends_on the init oneshot so services never race ahead of first-boot setup.


πŸ€– AI Sidecar Overlay

Optional compose overlay that adds a semantic-search-ready stack alongside ransynsrv:

Service Image Exposed on host
postgres pgvector/pgvector:0.8.0-pg17 127.0.0.1:${POSTGRES_HOST_PORT:-5432}
embedder ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 127.0.0.1:${EMBEDDER_HOST_PORT:-7997}
ai-init alpine:3.21 (oneshot) β€” (fixes bind-mount ownership)

The services share the compose network, so PHP apps inside ransynsrv reach them via DNS: postgres:5432 and embedder:80. Bind-mounted data lands under ${DATA_PATH}/postgres and ${DATA_PATH}/tei-cache, pre-chowned to your host UID/GID so backup scripts don't need sudo.

Sample RAG query from PHP

// 1. Embed a query via the internal embedder
$resp = file_get_contents('http://embedder/embed', false, stream_context_create([
    'http' => [
        'method'  => 'POST',
        'header'  => 'Content-Type: application/json',
        'content' => json_encode(['inputs' => $query]),
    ],
]));
$embedding = json_decode($resp, true);

// 2. Similarity search over documents
$pdo = new PDO(
    'pgsql:host=postgres;dbname=' . getenv('POSTGRES_DB'),
    getenv('POSTGRES_USER'),
    getenv('POSTGRES_PASSWORD')
);
$stmt = $pdo->prepare(
    'SELECT id, title FROM documents ORDER BY embedding <=> :q::vector LIMIT 5'
);
$stmt->execute([':q' => '[' . implode(',', $embedding) . ']']);

Full usage, model swap procedure, backup recipes and permissions notes: CLAUDE.md Β§ AI Sidecar Overlay.


πŸ”’ Security

  • Workers run unprivileged. PHP-FPM pool is abc:abc, nginx workers are nginx, ttyd is abc. Only the supervisor chain (s6, master processes) is root.
  • no-new-privileges:true on every compose service β€” blocks post-init sudo / setuid escalation.
  • Sidecar host ports bound to 127.0.0.1 by default. Flip *_BIND_ADDR=0.0.0.0 if you need them on the network.
  • GoAccess + ttyd Basic auth at the nginx layer β€” htpasswd files are generated from env at boot (bcrypt hash at rest). Credentials never land in service argv, so a compromised same-UID process can't read them from /proc.
  • EXPOSE is only port 80 β€” internal services (GoAccess WebSocket, ttyd) aren't advertised, so docker run -P can't publish them by accident.
  • Image-level ownership of service scripts: runtime abc can't overwrite /etc/s6-overlay/s6-rc.d/*/run (closes the common PHP-RCE β†’ root path).
  • Atomic config edits β€” init writes nginx.conf edits through a .tmp file + rename so a crashed init can't leave nginx syntactically broken.
  • Server: response header stripped (more_clear_headers Server) β€” no nginx fingerprint for scanners.
  • Editor-backup + database extensions denied at nginx (.bak, .swp, .sql, .db, .sqlite, .env, etc.) β€” an accidentally-dropped config.env or app.db in public_html returns 404 instead of leaking.
  • Log files mode 0640, SSH keys enforced to 0600, sentinel-guarded boot means a fresh deploy runs init once, not on every restart.

TLS stays at your reverse proxy. HSTS, rate limits, and WAF rules belong there.


🧰 Inside the container

PHP 8.4 extensions
bcmath   bz2       calendar  ctype    curl      dom       exif     fileinfo
ftp      gd        gettext   gmp      iconv     imap      intl     ldap
mbstring mysqli    mysqlnd   opcache  openssl   pcntl     pdo      pdo_mysql
pdo_pgsql pdo_sqlite pgsql     phar    posix     session   simplexml soap
sockets  sodium    sqlite3   tokenizer xml      xmlreader xmlwriter xsl
zip      zlib      apcu      igbinary  redis

Redis + APCu + igbinary included by default.

Dev toolchain

Shell: zsh + Oh-My-Zsh + Powerlevel10k (wizard auto-disabled). Plugins: git, docker, docker-compose, node, npm, fzf, rsync, sudo, zsh-autosuggestions, zsh-syntax-highlighting, zsh-completions.

Tools: git, git-delta, github-cli, fzf, ripgrep, rsync, rclone, jq, yq, mariadb-client, postgresql-client, redis-cli, ffmpeg, imagemagick, graphicsmagick, sqlite, ttyd.

Languages: PHP 8.4, Python 3 (+pip, cryptography, requests, yaml, jinja2), Node via system + NVM (nvm install, nvm use).

Nginx modules

Brotli, headers-more, fancyindex, image-filter. All loaded via /etc/nginx/modules/*.conf.


πŸƒ Common tasks

# Shell into the container
docker exec -it ransynsrv zsh

# Tail logs
docker logs -f ransynsrv
docker exec ransynsrv tail -f /data/log/nginx/error.log

# Reload nginx after editing /data/nginx/nginx.conf
docker exec ransynsrv nginx -t && docker exec ransynsrv nginx -s reload

# Install a package at runtime (persists because INSTALL_PACKAGES re-runs on boot)
echo 'INSTALL_PACKAGES="mc ncdu"' >> .env
docker compose restart ransynsrv

# Back up the /data volume
tar -czf ransynsrv-$(date +%Y%m%d).tar.gz ./data

# Back up Postgres (AI sidecar)
docker exec ransynsrv-postgres pg_dump -U ransynsrv -Fc ransynsrv \
  > backups/db-$(date +%Y%m%d).dump

More scenarios: CLAUDE.md Β§ Common Development Workflows.


πŸ› Troubleshooting

The in-repo troubleshooting guide covers the usual culprits β€” permission issues, nginx config errors, PHP-FPM not starting, GoAccess empty dashboard, sudo under no-new-privileges, DOCKER_LOGS=true + GoAccess interaction.

β†’ Full troubleshooting in CLAUDE.md


πŸ—‚οΈ Directory layout

ransynsrv/
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ docker-compose.yml          # local dev (build from source)
β”œβ”€β”€ docker-compose.deploy.yml   # pull from GHCR
β”œβ”€β”€ docker-compose.ai.yml       # optional AI sidecar overlay
β”œβ”€β”€ .env.example
β”œβ”€β”€ root/                       # files copied into the image
β”‚   β”œβ”€β”€ etc/
β”‚   β”‚   β”œβ”€β”€ nginx/nginx.conf    # image-level default (fixes first-boot race)
β”‚   β”‚   └── s6-overlay/s6-rc.d/{init-ransynsrv,svc-*}
β”‚   └── defaults/               # user-overridable templates
β”‚       β”œβ”€β”€ nginx/nginx.conf
β”‚       β”œβ”€β”€ CLAUDE.md           # seed Claude Code with container-aware context
β”‚       └── webroot/…
β”œβ”€β”€ data/                       # created at runtime, your persistent volume
β”‚   β”œβ”€β”€ webroot/public_html/    # your PHP app root
β”‚   β”œβ”€β”€ webroot/src/            # your PHP library tree
β”‚   β”œβ”€β”€ nginx/nginx.conf        # user-editable (symlinked to /etc/nginx)
β”‚   β”œβ”€β”€ databases/              # SQLite files
β”‚   β”œβ”€β”€ log/                    # nginx + php logs
β”‚   β”œβ”€β”€ claude/.claude/         # Claude Code config
β”‚   β”œβ”€β”€ ssh/                    # SSH keys (0700)
β”‚   └── commandhistory/         # zsh/bash history
β”œβ”€β”€ README.md                   # you are here
β”œβ”€β”€ CLAUDE.md                   # deep architecture + ops reference
β”œβ”€β”€ SpinUp_LLM_Prompt.md        # LLM prompt to bootstrap a new project on this stack
└── changelog.md

🀝 Contributing

PRs welcome β€” especially for new s6-overlay modules, PHP/Alpine extension requests, or sidecar-overlay compose files (e.g. ollama, redis, opensearch).

Please run docker compose config against any compose-file changes, and rebuild locally before pushing (docker build -t ransynsrv:test .).


πŸ“ License

MIT β€” see LICENSE.


Built with s6-overlay, PHP, Postgres, and too much coffee.

If this saved you a weekend of container plumbing, ⭐ the repo.

About

RanSynSrv is a single-container PHP 8.4 hosting stack on Alpine Linux, wired with everything you need to run small-to-medium PHP apps in production or development:

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors