Local network: Technitium DNS (*.apps.elkayam.me → 192.168.131.134) → nginx:80 → containers
External: Cloudflare DNS (*.elkayam.fun) → cloudflared tunnel → nginx:80 → containers
Domains:
*.apps.elkayam.me— Local access via Technitium DNS + NPM (TLS)*.elkayam.fun— External access via Cloudflare DNS + tunnel (Cloudflare TLS)
Apps:
dev.apps.elkayam.me→ code-server (VS Code IDE, port 8080)clock.apps.elkayam.me→ Clock dashboard (static HTML, port 3002)arcade.apps.elkayam.me/arcade.elkayam.fun→ Family Arcade (React + Express, ports 3003/30031)dinnerplan.apps.elkayam.me/dinnerplan.elkayam.fun→ Dinner planner (React + Express, ports 3004/30041)saritelkayam.apps.elkayam.me/saritelkayam.com→ Sarit Elkayam cosmetician website (Next.js 15, ports 3006/30061, Cloudflare direct to port 3006)erez.apps.elkayam.me/erez.elkayam.fun→ Location Log PWA (React 19 + Vite + IndexedDB, port 3008)
Cloudflare tunnel (docker-compose.cloudflare.yml):
- Single
cloudflaredcontainer withnetwork_mode: hoston the Games server - Routes all
*.elkayam.fun→ server nginx port 80 - Token in
.env.cloudflare(git-ignored, env varTUNNEL_TOKEN) - Ingress rules managed via API (verified 2026-08-12 — the zone-scoped token CAN read/write tunnel config):
- Zone token:
./hl.sh cred api_tokens.cloudflare.token(from HomeLab) - Zone ID (
elkayam.fun):15da5ff2c486f443c62d849656221a43 - Account ID:
118da7b48771f69cb525e760f7224ce5(derive:GET /zones/<zone_id>→result.account.id) - Tunnel ID:
c2c04913-3ba8-4370-bed1-447b84404212 - GET/PUT:
/client/v4/accounts/{aid}/cfd_tunnel/{tunnel_id}/configurations
- Zone token:
- Each app needs an ingress rule ABOVE the
http_status:404catch-all:appname.elkayam.fun→http://192.168.131.134:80(or a direct port for special cases:www.saritelkayam.com→:3006,lingo→:3007) - Full recipe (DNS CNAME + ingress + policy question): load the
homelab-publishskill
- IP: 192.168.131.134
- SSH:
ssh -i ~/.ssh/dev-env-server naor@192.168.131.134(key-based auth) - Docker: 20.10.24 + compose plugin (v2.x) —
naoruser in docker group - nginx: 1.22.1 on port 80
- Domain: elkayam.me (Technitium DNS + NPM for TLS, local only)
- External domain: elkayam.fun (Cloudflare DNS + tunnel)
- Technitium: Wildcard A record
*.apps→ server's public IP - Nginx:
server_nameincludes both.apps.elkayam.meand.elkayam.fun(auto-generated) - Files owned by:
naor(wasroot/elkayam, fixed withsudo chown -R naor:naor)
Server: /home/elkayam/dev-env/ (owned by naor)
Local: /Users/elnaor/Environments/Zed/dev-env/
GitHub: github.com/chu11u/dev-env
dev-env/
├── deploy-all.sh # Main deploy script (self-updates, see below)
├── .env # code-server password
├── .env.cloudflare # Tunnel token (git-ignored)
├── docker-compose.cloudflare.yml # Cloudflare tunnel (cloudflared)
├── docker-compose.yml # code-server
├── nginx-dev.apps.elkayam.me.conf # code-server nginx config
├── nginx-project-template.conf # Template for single-service projects
├── project-data/ # PERSISTENT DATA (outside git, auto-migrated)
│ ├── arcade/data/data.json # Arcade player/score data
│ └── dinnerplan/data/data.json # Dinnerplan data
├── projects/
│ ├── clock/ # Static HTML clock ✅
│ │ ├── Dockerfile # nginx:alpine serving index.html
│ │ ├── docker-compose.yml # container_name: clock, port 3002
│ │ └── index.html # Clock dashboard
│ └── arcade/ # Family Arcade ✅
│ ├── docker-compose.yml # 2 services: frontend + backend
│ │ NOTE: backend volume = `../../project-data/arcade/data:/app/data`
│ │ (NOT `../project-data` — docker-compose resolves from projects/arcade/)
│ ├── nginx-api.conf # Custom nginx (API proxy)
│ ├── backend/
│ │ ├── Dockerfile
│ │ ├── server.js # Express + lowdb (players, scores, games)
│ │ └── package.json
│ └── frontend/
│ ├── Dockerfile # Node builder + preview
│ ├── vite.config.js
│ ├── package.json # React 18.3.1 + Vite 5.4.1
│ ├── src/App.jsx # Main app (routing between games)
│ ├── src/main.jsx
│ ├── src/index.css # Global styles + mobile media queries
│ └── src/components/
│ ├── PlayerSelect.jsx # Player list + registration + DELETE button
│ ├── GameLobby.jsx # Game selection + player stats + back button
│ ├── SkyJumper.jsx # Jumping game (canvas)
│ ├── MemoryMatch.jsx # Card matching game
│ ├── TetrisGame.jsx # Classic Tetris (canvas, mobile-responsive)
│ └── Leaderboard.jsx # Score board
│ └── dinnerplan/ # Family Dinner Planner ✅
│ ├── docker-compose.yml # 2 services: frontend + backend
│ ├── frontend/ # React + Vite (Hebrew RTL)
│ └── backend/ # Express + fs JSON
│ └── saritelkayam/ # Sarit Elkayam cosmetician website 🆕
│ ├── docker-compose.yml # 3 services: frontend + backend + postgres
│ ├── frontend/ # Next.js 15 + Tailwind + Framer Motion
│ ├── backend/ # Next.js API routes + Prisma + PostgreSQL
│ ├── content/ # Markdown blog posts
│ └── MEMORY.md # Project-specific memory dump
ssh -i ~/.ssh/dev-env-server naor@192.168.131.134 "cd /home/elkayam/dev-env && ./deploy-all.sh"- Create files in
projects/<name>/(must havedocker-compose.yml+Dockerfile) - Push:
cd dev-env && git add -A && git commit -m "msg" && git push origin main
Step 0: Move project-data outside projects/ if misplaced
Step 0.5: Migrate arcade data BEFORE git pull (prevents data loss)
Step 1: Git pull with untracked file cleanup. Self-updates: checks md5sum before/after pull and re-runs with exec "$0" "$@" if script changed
Step 2: Generate nginx configs via heredocs (no sed -i):
- Single-service projects: use generate_nginx_config() function
- Multi-service projects: use generate_nginx_api_config() function
- Auto-cleans stale configs for non-existent projects
- Write to /tmp/ first, then sudo cp + sudo ln to /etc/nginx/ (no permission issues)
- All configs include both .apps.elkayam.me and .elkayam.fun domains
- Sudoers:
naorhas NOPASSWD for/usr/sbin/nginx,/usr/bin/cp,/usr/bin/ln(needed for nginx config writes) Step 3: Docker cleanup:docker compose down --rmi local --volumes --remove-orphansdocker rm -ffor any remaining containersdocker network rmfor project-specific networksdocker image rmfor dangling images Step 4: Build & deploy each project Summary: Shows ✅ successes, ❌ failures,⚠️ warnings
-
Nested .git repos: Always
rm -rf projects/<name>/.gitbefore committing. -
Nginx config generation:
- Deploy script generates configs via heredocs (not
sed -ion template file) generate_nginx_config(name, port)for single-service projectsgenerate_nginx_api_config(name, frontend_port, api_port)for multi-service projects- Stale configs auto-removed based on whitelist of valid project names
- Deploy script generates configs via heredocs (not
-
API routing in nginx: The
proxy_passMUST NOT have trailing slash:# RIGHT (preserves /api/ prefix): proxy_pass http://127.0.0.1:PORT; # WRONG (strips /api/ prefix): proxy_pass http://127.0.0.1:PORT/;
This was the cause of "Unexpected token '<', "<!DOCTYPE "... is not valid JSON" errors.
-
Docker container name conflicts: Use
docker compose downBEFOREdocker rm -f. Don't try to remove containers before compose knows about them. -
Docker network ambiguity: "network X_default is ambiguous (2 matches found)" means duplicate networks exist. Fix with
docker network prune -for manually remove duplicates. -
Data persistence: Arcade data lives in
project-data/arcade/data/data.json. Deploy script auto-migrates this BEFORE git pull. Volume path in docker-compose:../../project-data/arcade/data:/app/data(was../which pointed wrong dir). -
Vite allowedHosts: Must have
allowedHosts: truein bothserverandpreviewsections of vite.config.js. -
Browser caching: Always hard refresh (Cmd+Shift+R) after frontend updates.
-
SSH access: Key at
~/.ssh/dev-env-server(ed25519). User:naor. Files owned bynaor(wasroot). -
File ownership: Server files must be owned by
naor. Fix withsudo chown -R naor:naor /home/elkayam/dev-env.
- ✅ Sky Jumper — Jumping game with keyboard (← →, A/D) + touch controls, high score
- ✅ Memory Match — Card matching game
- ✅ Tetris — Classic Tetris with:
- All 7 tetrominoes (I, O, T, S, Z, J, L) with distinct colors
- Ghost piece preview
- Next piece preview
- Score, Lines, Level tracking
- High score (localStorage)
- Keyboard: ← → move, ↑ rotate, ↓ soft drop, Space pause
- Touch: top=rotate, left/right=move, center=drop
- Game state in single mutable
gameobject, shared viagameRef.current - Mobile-responsive layout (canvas scales to viewport, compact overlays)
- Fixed: clearLines logic (was keeping full rows instead of removing them)
- ✅ Player registration (with error handling, username required)
- ✅ Player selection screen with avatars
- ✅ Player deletion — ✕ button on player cards (only when 2+ players)
- ✅ Game lobby with stats + back button (mobile-friendly flex layout)
- ✅ Leaderboard
- ✅ Backend API (players CRUD, scores, games)
- ✅ Data persistence (lowdb JSON in
project-data/) - Docker compose (2 containers: frontend + backend)
- nginx routing (frontend + API proxy)
- External access via
arcade.elkayam.fun(Cloudflare tunnel) - Mobile responsive — responsive canvas, compact overlays, media queries for 600px/400px
- Canvas:
width: 100%withmaxWidth: 500pxcontainer - Game overlays:
80%width, compact padding - Header:
flexWrapso buttons don't overflow on narrow screens - GameLobby: back button inline (not absolute),
minmax(200px)grid - Media queries at 600px (smaller buttons/padding) and 400px (extra compact)
| Port | Project | Purpose |
|---|---|---|
| 8080 | code-server | IDE |
| 3002 | clock | Frontend |
| 3003 | arcade | Frontend |
| 30031 | arcade | Backend API |
| 3004 | dinnerplan | Frontend |
| 30041 | dinnerplan | Backend API |
| 3005 | eventcorrelator | Frontend |
| 3006 | saritelkayam | Frontend |
| 30061 | saritelkayam | Backend API |
| 30062 | saritelkayam | PostgreSQL (internal, not exposed) |
| 3007 | languages | Frontend |
| 30071 | languages | Backend API |
| 3008 | erez | Location Log PWA (frontend) |
Next available: 3009, 30091
IMPORTANT: Arcade data lives in /home/elkayam/dev-env/project-data/arcade/data/data.json
This directory is OUTSIDE the git repo. The deploy script auto-migrates data from old location.
Volume mount: Must be ../../project-data/arcade/data:/app/data in docker-compose.
- Username:
chu11u - Repos:
chu11u/dev-env(main),chu11u/v0-family-arcade-app(old, deprecated)
- Brand: Sarit Elkayam (cosmetician)
- Domain:
saritelkayam.com(external),saritelkayam.apps.elkayam.me(local) - Tech: Next.js 15 + Tailwind CSS + Framer Motion + Prisma + PostgreSQL
- Design: Warm luxury (rose gold, cream, burgundy)
- Features: Services, Testimonials, Shop, Blog (CMS), Contact, Booking (on hold)
- Media: Draw Things API (Flux 2 Klein 9B) for royalty-free images
- Media API:
POST http://localhost:7860/sdapi/v1/txt2img(local Mac only, NOT on server) - Payment: Stripe (on hold)
- Status: 🆕 Planning phase — see
projects/saritelkayam/MEMORY.mdfor full details
- Sarit Elkayam Website — Cosmetician website (🆕 TOP PRIORITY)
- Weather Widget — Simple weather dashboard
- Home Dashboard — Monitor all homelab services
- More Arcade Games — Tic Tac Toe, Snake, etc.
- Create project in
projects/<name>/withdocker-compose.yml+ Dockerfiles - Push to git and run
./deploy-all.sh(generates dual-domain nginx config automatically) - Ask which access policy to use (public vs Cloudflare Access) — see
homelab-publishskill - Cloudflare DNS: CNAME
app.elkayam.fun→<tunnel-id>.cfargotunnel.com, proxied - Tunnel ingress: PUT
configurationsAPI with hostname ruleapp.elkayam.fun→http://192.168.131.134:80(before the 404 catch-all) - If restricted: configure Cloudflare Access policy (dashboard or account-scoped token — zone token is read-only for Access)
- Verify
https://app.elkayam.fun/→ 200 (and manifest/sw for PWAs)
Full recipe with exact curl commands: load the homelab-publish skill.
*/5 * * * * cd /home/elkayam/dev-env && /home/elkayam/dev-env/deploy-all.sh >> /var/log/dev-env-deploy.log 2>&1