Batteries-included PHP 8.4 hosting container with real-time analytics, a web terminal, Claude Code CLI, and an optional pgvector-powered RAG sidecar.
Quick Start β’ Features β’ Configuration β’ AI Sidecar β’ Troubleshooting
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 execinto 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.
|
|
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).
# 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 # analyticsgit clone https://github.com/RandomSynergy17/RanSynSrv && cd RanSynSrv
cp .env.example .env && nano .env
docker compose up -d --buildecho "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.
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.envfile for Portainer - A complete Portainer stack YAML
- A
DEPLOY.mdwritten 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
All tunables live in .env. Every variable has a safe default unless explicitly marked required.
| 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 |
| 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) |
| 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 |
| 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 |
| 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.
βββββββββββββββββββββββββββββββ 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.
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.
// 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.
- Workers run unprivileged. PHP-FPM pool is
abc:abc, nginx workers arenginx, ttyd isabc. Only the supervisor chain (s6, master processes) is root. no-new-privileges:trueon every compose service β blocks post-init sudo / setuid escalation.- Sidecar host ports bound to
127.0.0.1by default. Flip*_BIND_ADDR=0.0.0.0if 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. EXPOSEis only port 80 β internal services (GoAccess WebSocket, ttyd) aren't advertised, sodocker run -Pcan't publish them by accident.- Image-level ownership of service scripts: runtime
abccan'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
.tmpfile + 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-droppedconfig.envorapp.dbinpublic_htmlreturns 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.
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.
# 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).dumpMore scenarios: CLAUDE.md Β§ Common Development Workflows.
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
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
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 .).
MIT β see LICENSE.
Built with s6-overlay, PHP, Postgres, and too much coffee.
If this saved you a weekend of container plumbing, β the repo.