ximg.app — stack

A full technical teardown of the infrastructure, runtimes, and frontend stack powering every app on this domain. 30+ subdomains. Zero frameworks. 100% containers.

nginx:alpine node 22 docker compose vanilla js let's encrypt apache httpd websockets systemd canvas api python paramiko
at a glance
30+
subdomains
1
SSL cert
30+
containers
0
JS frameworks
1
host machine
chaos potential
system architecture

request flow — internet to container

graph TD A["🌐 Browser / Client"] -->|"HTTPS :443"| B["nginx:alpine\n(SSL termination\nreverse proxy)"] B -->|"HTTP :80\n(proxy_pass)"| C["Apache httpd:2.4-alpine\n(static files)"] B -->|"HTTP :3000\n/ws WebSocket upgrade"| D["Node.js 22 Alpine\n(log streaming server)"] B -->|"HTTP→HTTPS redirect"| A subgraph "Docker Compose Network" B C D E["Python 3.12 Alpine\n+ paramiko\n(SSH honeypot :22)"] end F["systemd\nximg-web.service"] -->|"manages lifecycle"| B G["Let's Encrypt\ncertbot HTTP-01"] -->|"wildcard cert\n*.ximg.app"| B style A fill:#0d0d1a,stroke:#00ff41,color:#00ff41 style B fill:#0d0d1a,stroke:#0af,color:#0af style C fill:#0d0d1a,stroke:#c084fc,color:#c084fc style D fill:#0d0d1a,stroke:#0af,color:#0af style E fill:#0d0d1a,stroke:#f87171,color:#f87171 style F fill:#0d0d1a,stroke:#f60,color:#f60 style G fill:#0d0d1a,stroke:#00ff41,color:#00ff41

container topology — one Apache per subdomain

graph LR N["nginx\n:80/:443"] --> W["web\nximg.app"] N --> LX["linux\nlinux.ximg.app"] N --> BF["butterfly\nbutterfly.ximg.app"] N --> AS["ascii\nascii.ximg.app"] N --> PK["poker\npoker.ximg.app"] N --> DM["doom\ndoom.ximg.app"] N --> DOT["···\n30+ apps"] N --> ND["node\nlogs.ximg.app\n(WebSocket)"] N -.->|"SSH :22\n(host port)"| SH["ssh-honeypot\n(isolated net)"] style N fill:#0d0d1a,stroke:#0af,color:#0af style ND fill:#0d0d1a,stroke:#0af,color:#0af style SH fill:#0d0d1a,stroke:#f87171,color:#f87171 style W fill:#0d0d1a,stroke:#00ff41,color:#00ff41 style LX fill:#0d0d1a,stroke:#c084fc,color:#c084fc style BF fill:#0d0d1a,stroke:#c084fc,color:#c084fc style AS fill:#0d0d1a,stroke:#c084fc,color:#c084fc style PK fill:#0d0d1a,stroke:#c084fc,color:#c084fc style DM fill:#0d0d1a,stroke:#c084fc,color:#c084fc style DOT fill:#0d0d1a,stroke:#555,color:#555

SSL lifecycle — Let's Encrypt HTTP-01

sequenceDiagram participant O as Operator participant CB as certbot participant LE as Let's Encrypt CA participant NG as nginx participant DNS as DNS (A record) O->>DNS: add A record → 172.238.205.61 O->>CB: certbot --expand -d ximg.app -d *.ximg.app CB->>NG: place HTTP-01 challenge token\n/.well-known/acme-challenge/ CB->>LE: notify ready LE->>NG: GET /.well-known/acme-challenge/{token} NG-->>LE: 200 OK (token) LE-->>CB: challenge passed CB-->>O: cert issued → /etc/letsencrypt/live/ximg.app/ O->>NG: reload (docker compose restart nginx) NG->>NG: serve fullchain.pem + privkey.pem\nfor ALL subdomains (single cert)

frontend data flow — WebSocket log streaming

sequenceDiagram participant B as Browser participant NG as nginx participant NS as Node.js server participant FS as Filesystem\n(nginx logs) B->>NG: GET https://logs.ximg.app/ NG->>NS: proxy_pass http://logs:3000/ NS-->>B: HTML page B->>NG: WebSocket ws://logs.ximg.app/ws?site=doom NG->>NS: upgrade → WebSocket /ws?site=doom NS->>FS: tail doom.access.log FS-->>NS: new log lines (inotify / poll) NS-->>B: JSON frames (real-time) B->>B: render in
 with\ncolor-coded status codes
    
technology breakdown every piece of the stack
🔀
nginx
alpine image
infrastructure
Single entry point for all traffic. Handles TLS termination using the Let's Encrypt wildcard cert, HTTP→HTTPS redirect, and reverse proxies to per-subdomain Apache containers via proxy_pass with dynamic $upstream variables. Also upgrades WebSocket connections for the logs server.
🪶
Apache httpd
2.4-alpine · one container per app
infrastructure
Pure static file serving. Each subdomain gets its own isolated Apache container with the app's *-html/ directory mounted at /usr/local/apache2/htdocs. The shared nav is volume-mounted read-only into every container at /htdocs/shared.
Node.js 22
Alpine · logs-server/server.js
runtime
The only dynamic backend. Streams live nginx access logs over WebSocket (one tab per subdomain), serves the SSH session browser, manages Mario high-score persistence to a JSON file, and handles the "all" tab that multiplexes log lines from every site simultaneously.
🐍
Python 3.12 + paramiko
Alpine · ssh-server/
security
SSH honeypot on host port 22. Accepts any password, drops the attacker into a real /bin/bash shell inside an isolated container with no outbound network access (blocked by iptables). Full session recordings saved to ssh-logs/ and browsable in the logs app.
🐳
Docker Compose
compose.yaml
devops
Orchestrates all 30+ containers as a single unit. Defines inter-container networking (default bridge + isolated ssh-net for the honeypot), volume mounts, restart policies, and the dependency graph (depends_on) ensuring nginx starts after all Apache containers.
⚙️
systemd
ximg-web.service
devops
Host-level service manager. The ximg-web.service unit runs docker compose up on boot and restarts it on failure — making the entire stack self-healing across reboots and crashes without any external orchestrator.
🔒
Let's Encrypt
certbot HTTP-01 challenge
infrastructure
Single wildcard-adjacent cert covering ximg.app and every *.ximg.app subdomain via explicit SANs. Renewed by certbot using the HTTP-01 challenge served through nginx's /var/www/certbot root. All new subdomains require certbot --expand.
🖼️
Canvas API
vanilla — no WebGL
frontend
Used across multiple apps for real-time rendering: DDA raycasting + texture mapping in Doom, particle systems in Butterfly, pixel-buffer manipulation for ASCII art demos, and probability bar charts in Poker. Raw ImageData + Uint32Array for high-throughput pixel writes.
WebSockets
native browser API
frontend
Real-time bidirectional channel between the browser and Node.js log server. nginx proxies /ws with proxy_http_version 1.1 and the Upgrade header. The client auto-reconnects on disconnect and renders color-coded log lines by HTTP status class.
💻
Vanilla JS
no build step · no framework
frontend
Zero framework policy. Every interactive app is authored in plain ES2022+ JavaScript — no React, no Vue, no bundler. The shared nav is an IIFE that surgically prepends itself to any page's <body>. Apps use requestAnimationFrame loops, DOM APIs, and browser-native everything.
📟
xterm.js
bundled vendor — linux.ximg.app
frontend
Full VT100/ANSI terminal emulator running entirely in the browser for the Linux Terminal app. Vendor-bundled locally (no CDN). Backed by a ~20-command mock shell with working ls, cat, man, ping, and a DVD-bouncing Tux easter egg.
📐
Mermaid.js
v11 · bundled vendor — this page
frontend
Diagram-as-code renderer used on this page. Renders flowcharts, sequence diagrams, and topology graphs from plain text markup. Vendored locally at ximg-html/vendor/mermaid.min.js — no CDN, consistent with the no-external-dependency policy.
layer model bottom to top
🖥️ Host OS (Linux)
Bare-metal or VPS running systemd. Manages the Docker daemon and the ximg-web.service unit. iptables rules block outbound traffic from the SSH honeypot network.
🐳 Docker Compose
Defines all containers, networks, volume mounts, and restart policies. Single compose.yaml is the source of truth for the entire runtime topology.
🔀 nginx (edge)
Terminates TLS, routes by server_name, proxies to backend containers. The only container exposed on host ports 80 and 443.
⬡ Node.js / 🪶 Apache
Application layer. Apache serves static HTML/JS/CSS per subdomain. Node handles dynamic WebSocket streaming and the Mario scores API. Both are internal — never directly reachable from outside.
🟣 Frontend (vanilla JS)
All interactivity runs client-side. Canvas API for games and animations. WebSockets for real-time logs. No build step, no transpilation, no framework dependencies.
compose pattern every static app looks like this
# compose.yaml — one entry per subdomain
  doom:
    image: httpd:2.4-alpine
    restart: unless-stopped
    volumes:
      - ./doom-html:/usr/local/apache2/htdocs
      - ./shared-html:/usr/local/apache2/htdocs/shared:ro
# nginx/nginx.conf — one server block per subdomain
server {
  listen 443 ssl;
  server_name doom.ximg.app;
  ssl_certificate /etc/letsencrypt/live/ximg.app/fullchain.pem;
  location / {
    set $upstream doom;
    proxy_pass http://$upstream:80;
  }
}
adding a new app the checklist

new subdomain provisioning flow

flowchart LR A["1. DNS\nA record → 172.238.205.61"] --> B["2. certbot --expand\n(add SAN to cert)"] B --> C["3. nginx.conf\nnew server block"] C --> D["4. compose.yaml\nnew Apache service"] D --> E["5. *-html/\nstatic files"] E --> F["6. nav.js\ndropdown entry"] F --> G["7. public-html/\nlanding page card"] G --> H["8. logs server\nLOG_FILES entry + tab"] H --> I["9. apps-html/\nAPPS array row"] I --> J["✅ docker compose up -d"] style A fill:#0d0d1a,stroke:#0af,color:#0af style B fill:#0d0d1a,stroke:#00ff41,color:#00ff41 style C fill:#0d0d1a,stroke:#0af,color:#0af style D fill:#0d0d1a,stroke:#f60,color:#f60 style E fill:#0d0d1a,stroke:#c084fc,color:#c084fc style F fill:#0d0d1a,stroke:#c084fc,color:#c084fc style G fill:#0d0d1a,stroke:#c084fc,color:#c084fc style H fill:#0d0d1a,stroke:#0af,color:#0af style I fill:#0d0d1a,stroke:#0af,color:#0af style J fill:#0d0d1a,stroke:#00ff41,color:#00ff41