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).
This commit is contained in:
Maximus Gorog 2026-05-23 18:45:05 -06:00
parent b52c1927cf
commit f239a939ce
6 changed files with 238 additions and 0 deletions

16
.dockerignore Normal file
View file

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

12
Caddyfile Normal file
View file

@ -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
}

113
DEPLOY.md Normal file
View file

@ -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 ~58 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``<VPS public IP>`, 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.

52
Dockerfile Normal file
View file

@ -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"]

27
docker-compose.prod.yml Normal file
View file

@ -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:

18
docker-compose.yml Normal file
View file

@ -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"