From f239a939ce91a1ae28e2963a14da8ddc2846ce8f Mon Sep 17 00:00:00 2001 From: Maximus Gorog Date: Sat, 23 May 2026 18:45:05 -0600 Subject: [PATCH] Add Docker + Caddy deploy for voxel.mxvs.art Multi-stage Dockerfile compiles wasm client + axum server in one Rust builder and copies into a debian:bookworm-slim runtime (non-root uid). docker-compose.yml binds localhost:8080 by default; docker-compose.prod.yml replaces ports with a Caddy reverse proxy on host 80/443 that talks to the voxel container over the internal network. Caddy auto-issues Let's Encrypt certs. DEPLOY.md covers the three deployment modes (local-only, VPS with Cloudflare or Caddy, Cloudflare Tunnel from a workstation). --- .dockerignore | 16 ++++++ Caddyfile | 12 +++++ DEPLOY.md | 113 ++++++++++++++++++++++++++++++++++++++++ Dockerfile | 52 ++++++++++++++++++ docker-compose.prod.yml | 27 ++++++++++ docker-compose.yml | 18 +++++++ 6 files changed, 238 insertions(+) create mode 100644 .dockerignore create mode 100644 Caddyfile create mode 100644 DEPLOY.md create mode 100644 Dockerfile create mode 100644 docker-compose.prod.yml create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e5280e3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +# Keep build artifacts out of the build context so the image stays small. +target/ +**/target/ +.git/ +.idea/ +.vscode/ + +# Generated wasm in web/ — we want the *build* to produce these. +web/voxel_game.js +web/voxel_game_bg.wasm +web/voxel_game.d.ts + +# Misc +*.swp +.DS_Store +Thumbs.db diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..df2d217 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,12 @@ +# Public reverse-proxy front. Mirrors the `.wg` Caddy convention but +# uses real Let's Encrypt certs (the wg version uses `tls internal`). +# Caddy fronts the voxel container over the Docker network, so the +# voxel service no longer publishes a host port. +{ + email mgorog@gmail.com +} + +voxel.mxvs.art { + encode zstd gzip + reverse_proxy voxel:8080 +} diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..4980268 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,113 @@ +# Deploying the voxel game + +Three layers, pick the combination that fits. + +## Local-only (development) + +```sh +docker compose up --build +``` + +Serves on `http://localhost:8080`. `127.0.0.1`-bound — not reachable from +the network. Good for iterating on Docker without exposing anything. + +## Anywhere with a public IP (the real deploy) + +This assumes a cheap VPS with Docker + docker-compose-plugin installed. +Tested targets: Hetzner CPX11 (~$5/mo), DigitalOcean Basic Droplet ($4), +Vultr (~$2.50), Oracle Cloud Always Free tier (ARM Ampere instance — free +forever, just slow to provision). + +### 1. SSH in and install Docker + +Debian/Ubuntu host: + +```sh +curl -fsSL https://get.docker.com | sh +sudo usermod -aG docker $USER # log out + back in +``` + +### 2. Get the code on the box + +```sh +git clone https://maxgit.wg/max/terainia.git # or wherever you push it +cd terainia +``` + +### 3. Build + run + +```sh +docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build +``` + +The container binds to `0.0.0.0:8080`. The first build takes ~5–8 min on +a small VPS because it compiles Rust + the wasm client; subsequent +builds are fast due to layer caching. + +### 4. Put TLS in front + +You have two clean options here. + +**Option A — Cloudflare proxy (no cert on the VPS, simplest).** + +- In your Cloudflare dashboard for `mxvs.art`, add an A record: + `voxel` → ``, proxy status **Proxied** (orange cloud). +- Cloudflare → SSL/TLS → set encryption mode to **Flexible** + (Cloudflare terminates HTTPS, talks HTTP to the VPS). +- That's it. `https://voxel.mxvs.art` serves the game. + +**Option B — Caddy on the VPS for Let's Encrypt.** + +If you want real end-to-end TLS instead of Cloudflare-terminated: + +```yaml +# docker-compose.prod.yml addition + caddy: + image: caddy:2-alpine + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + +volumes: + caddy_data: + caddy_config: +``` + +```caddy +# Caddyfile +voxel.mxvs.art { + reverse_proxy voxel:8080 +} +``` + +Then keep `voxel` bound to `127.0.0.1:8080` (or use Compose's internal +network only — drop the `ports:` mapping on the `voxel` service and let +Caddy reach it via the service name). The DNS A record in Cloudflare +should be **DNS only** (grey cloud) in this case so Cloudflare doesn't +re-terminate TLS. + +### 5. Deploy updates + +```sh +git pull +docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build +``` + +Down-and-up time is a couple of seconds while the new container +swaps in. + +## Local + Cloudflare Tunnel (no VPS) + +If you don't want a VPS yet, the game can run on your machine and be +exposed through a Cloudflare named tunnel pointing at `voxel.mxvs.art`. +The Dockerfile is still useful for keeping the local environment +consistent — just `docker compose up` and then point a `cloudflared` +container at `host.docker.internal:8080`. See the Cloudflare Tunnel +docs for the named-tunnel setup; the credential file (`cert.pem` from +`cloudflared tunnel login`) needs to land at `~/.cloudflared/cert.pem` +on whichever host the tunnel runs on. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c4f9557 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +# Multi-stage build: compile the wasm client + the server in one Rust +# image, then copy the artifacts into a tiny Debian runtime. +FROM rust:1-bookworm AS builder + +# Pin wasm-bindgen-cli to the same version we use during development. If +# this number drifts from Cargo.lock the wasm load will fail. +ARG WASM_BINDGEN_VERSION=0.2.122 + +# Install the wasm32 target and wasm-bindgen-cli. +RUN rustup target add wasm32-unknown-unknown && \ + cargo install wasm-bindgen-cli --version ${WASM_BINDGEN_VERSION} --locked + +WORKDIR /build + +# Copy the whole project. The .dockerignore keeps target/ out so we get +# a clean release build inside the container. +COPY . . + +# Build the wasm client (release) and run wasm-bindgen to emit the +# JS glue + bg.wasm into web/. +RUN cargo build --target wasm32-unknown-unknown --release --lib && \ + wasm-bindgen --target web --out-dir web --no-typescript \ + target/wasm32-unknown-unknown/release/voxel_game.wasm + +# Build the multiplayer server. +RUN cd server && cargo build --release + +# ---- Runtime image ---- +FROM debian:bookworm-slim AS runtime + +# ca-certificates lets the server speak HTTPS if it ever needs to (it +# doesn't yet, but it's tiny and avoids surprises). tini handles signal +# forwarding so `docker stop` is clean. +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates tini && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY --from=builder /build/server/target/release/voxel-server /usr/local/bin/voxel-server +COPY --from=builder /build/web /app/web + +ENV STATIC_DIR=/app/web +ENV PORT=8080 +EXPOSE 8080 + +# Run as non-root for safety. +RUN useradd --create-home --shell /bin/false --uid 10001 voxel && \ + chown -R voxel:voxel /app +USER voxel + +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["/usr/local/bin/voxel-server"] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..24a2588 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,27 @@ +# Production overrides — Caddy on 80/443 terminates TLS and reverse +# proxies to the voxel container over the internal Docker network. +# The voxel service drops its host port mapping (was 127.0.0.1:8080:8080 +# in the base file); Caddy reaches it via the service name. Run with: +# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d +services: + voxel: + ports: !override [] + restart: always + + caddy: + image: caddy:2-alpine + container_name: voxel-caddy + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + depends_on: + - voxel + +volumes: + caddy_data: + caddy_config: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c74ac6b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + voxel: + build: . + image: voxel-game:latest + container_name: voxel-game + restart: unless-stopped + # Bind to 127.0.0.1 by default so the container isn't accidentally + # public when running on a workstation. Override the LHS to 0.0.0.0 + # in your deploy environment (or via docker-compose.override.yml) if + # you want the host's external interface to serve it. + ports: + - "127.0.0.1:8080:8080" + environment: + # voxel-server reads these. STATIC_DIR is baked in by the Dockerfile, + # but you can override them here if mounting a different web/ from + # the host. + PORT: "8080" + RUST_LOG: "info"