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
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
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.
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