Self-hosted local services for development:
- PostgreSQL 18 with pgvector
- Redis 8
- MinIO
- ClamAV
- Ollama with
bge-m3embeddings
Services bind to 127.0.0.1 by default. Remote access is through Cloudflare
Tunnel and client-side cloudflared access tcp for TCP services.
cp .env.example .env
docker compose up -dCheck status:
docker compose ps- PostgreSQL:
127.0.0.1:5432 - Redis:
127.0.0.1:6379 - MinIO API:
127.0.0.1:9000 - MinIO Console:
127.0.0.1:9001 - ClamAV:
127.0.0.1:13310 - Ollama API:
127.0.0.1:11434
- PostgreSQL admin:
postgres / password - PostgreSQL app user:
xiro / xiro - PostgreSQL databases:
xiro,cyp_dev,taxeasy_dev - Redis password:
redis - MinIO:
minioadmin / minioadmin - MinIO bucket:
app - ClamAV: no username or password
- Ollama: no username or password
- Ollama embedding model:
bge-m3
Use cloudflared/config.yml as the server tunnel config.
pg.xirothedev.site->127.0.0.1:5432redis.xirothedev.site->127.0.0.1:6379clamav.xirothedev.site->127.0.0.1:13310minio.xirothedev.site->127.0.0.1:9000minio-console.xirothedev.site->127.0.0.1:9001ollama.xirothedev.site->127.0.0.1:11434
Ollama has no built-in API authentication. Keep ollama.xirothedev.site
behind Cloudflare Access if it is reachable from the public internet.
Apply the server tunnel config after editing it:
sudo cp cloudflared/config.yml /etc/cloudflared/config.yml
sudo systemctl restart cloudflaredCreate the Cloudflare DNS route if it does not exist yet:
cloudflared tunnel route dns plane-xirothedev ollama.xirothedev.sitePostgreSQL, Redis, and ClamAV are TCP services behind Cloudflare Access. Start local TCP tunnels on the client machine, then point projects at the local endpoints.
Plist templates are committed under clients/macos/plists. The installer
replaces the placeholders with your real cloudflared path and writes the
active plist files to ~/Library/LaunchAgents.
cd clients/macos
./install-cloudflared-access-launchagents.shIf cloudflared is not in PATH:
CLOUDFLARED_BIN=/opt/homebrew/bin/cloudflared ./install-cloudflared-access-launchagents.shOn a trusted Mac with Host minipc already configured in ~/.ssh/config, use
one SSH LaunchAgent instead of the PostgreSQL and Redis Cloudflare Access TCP
LaunchAgents. This keeps the local endpoints stable and also forwards the
OpenClaw UI/gateway running on the minipc.
Forwarded endpoints:
- PostgreSQL:
127.0.0.1:15432->minipc:5432 - Redis:
127.0.0.1:16379->minipc:6379 - OpenClaw:
127.0.0.1:18789->minipc:18789
Install the SSH plist:
mkdir -p ~/Library/LaunchAgents
cp clients/macos/plists/com.xiro.ssh.openclaw-minipc.plist ~/Library/LaunchAgents/
domain="gui/$(id -u)"
launchctl bootout "$domain" "$HOME/Library/LaunchAgents/com.xiro.cloudflared.pg.plist" 2>/dev/null || true
launchctl bootout "$domain" "$HOME/Library/LaunchAgents/com.xiro.cloudflared.redis.plist" 2>/dev/null || true
launchctl disable "$domain/com.xiro.cloudflared.pg" 2>/dev/null || true
launchctl disable "$domain/com.xiro.cloudflared.redis" 2>/dev/null || true
launchctl bootout "$domain" "$HOME/Library/LaunchAgents/com.xiro.ssh.openclaw-minipc.plist" 2>/dev/null || true
launchctl bootstrap "$domain" "$HOME/Library/LaunchAgents/com.xiro.ssh.openclaw-minipc.plist"
launchctl enable "$domain/com.xiro.ssh.openclaw-minipc"
launchctl kickstart -k "$domain/com.xiro.ssh.openclaw-minipc"Verify from the Mac:
launchctl print "gui/$(id -u)/com.xiro.ssh.openclaw-minipc"
nc -vz 127.0.0.1 15432
nc -vz 127.0.0.1 16379
curl -I http://127.0.0.1:18789Prefer 127.0.0.1 in project env files. If Docker Desktop also publishes
15432 or 16379, localhost may resolve to IPv6 ::1 and hit the local
Docker container instead of the SSH tunnel.
Systemd user service templates are committed under clients/linux/systemd.
cd clients/linux
./install-cloudflared-access-user-services.shScheduled Task XML templates are committed under clients/windows/tasks.
Run PowerShell:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
.\clients\windows\install-cloudflared-access-scheduled-tasks.ps1- PostgreSQL:
127.0.0.1:15432 - Redis:
127.0.0.1:16379 - ClamAV:
127.0.0.1:13310 - OpenClaw UI/gateway through the minipc SSH LaunchAgent:
http://127.0.0.1:18789 - MinIO API:
https://minio.xirothedev.site - MinIO Console:
https://minio-console.xirothedev.site - Ollama API:
https://ollama.xirothedev.site
If you prefer MinIO through a local cloudflared access tcp tunnel instead of
the direct HTTPS hostnames:
- MinIO API local tunnel:
http://127.0.0.1:19000 - MinIO Console local tunnel:
http://127.0.0.1:19001
DATABASE_URL=postgresql://xiro:xiro@127.0.0.1:15432/cyp_dev?schema=public
REDIS_URL=redis://default:redis@127.0.0.1:16379
CLAMAV_HOST=127.0.0.1
CLAMAV_PORT=13310
MINIO_ENDPOINT=https://minio.xirothedev.site
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=app
MINIO_REGION=us-east-1
OLLAMA_BASE_URL=https://ollama.xirothedev.site
OLLAMA_EMBEDDING_MODEL=bge-m3For taxeasy_dev:
DATABASE_URL=postgresql://xiro:xiro@127.0.0.1:15432/taxeasy_dev?schema=publicApplications should stream upload contents to clamd using the INSTREAM
command. Do not send local file paths from another machine.
Useful client libraries:
- Node.js:
clamscan,clamdjs,node-clam - Python:
clamd - PHP:
xenolope/quahog
Manual protocol test:
printf 'PING\n' | nc 127.0.0.1 13310Expected response: PONG.
The stack pulls bge-m3 automatically through the ollama_init service.
bge-m3 returns 1024-dimensional embeddings.
Local test:
curl http://127.0.0.1:11434/api/embed \
-H 'Content-Type: application/json' \
-d '{"model":"bge-m3","input":"Xin chao"}'Cloudflare Tunnel test:
curl https://ollama.xirothedev.site/api/embed \
-H 'Content-Type: application/json' \
-d '{"model":"bge-m3","input":"Xin chao"}'