How to Deploy a WASM Game + API

Static WASM game and Python API in a single app. Nginx serves the game and proxies /api to Flask.

Repo structure

my-game/
├── Dockerfile
├── nginx.conf
├── game/                   # WASM game (static files)
│   ├── index.html
│   ├── game.wasm
│   ├── game.js
│   └── assets/
├── api/                    # Python API (leaderboard, saves)
│   ├── requirements.txt
│   └── app.py
└── start.sh

Dockerfile

Nginx serves static files from /game and reverse-proxies /api to gunicorn running Flask.

FROM python:3.12-slim

RUN apt-get update && apt-get install -y nginx gettext-base && rm -rf /var/lib/apt/lists/* \
    && rm -f /etc/nginx/sites-enabled/default

# Install Python dependencies
WORKDIR /app
COPY api/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY api/ ./api/

# Copy game static files
COPY game/ /usr/share/nginx/html/

# Copy nginx config template and start script
COPY nginx.conf /etc/nginx/conf.d/default.conf.template
COPY start.sh /start.sh
RUN chmod +x /start.sh

EXPOSE $PORT
ENV PORT=8080
CMD ["/start.sh"]

nginx.conf

Serves the game at / and proxies /api to gunicorn on port 5000.

server {
    listen ${PORT};

    # Serve WASM game
    location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;
    }

    # Proxy API requests to gunicorn
    location /api/ {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

start.sh

Starts gunicorn in the background, then nginx in the foreground.

#!/bin/sh
# Start Flask API
cd /app && gunicorn --bind 127.0.0.1:5000 api.app:app &

# Render nginx config with $PORT and start nginx
envsubst '${PORT}' \
  < /etc/nginx/conf.d/default.conf.template \
  > /etc/nginx/conf.d/default.conf
nginx -g 'daemon off;'

Example API (api/app.py)

from flask import Flask, jsonify, request

app = Flask(__name__)

scores = []

@app.route("/api/scores", methods=["GET"])
def get_scores():
    return jsonify(sorted(scores, reverse=True)[:10])

@app.route("/api/scores", methods=["POST"])
def submit_score():
    data = request.get_json()
    scores.append(data["score"])
    return jsonify({"rank": sorted(scores, reverse=True).index(data["score"]) + 1})

Deploy

Step 1: Create the app

create_app(name: "mygame", github_repo: "github.com/me/my-game")

Step 2: Attach Postgres (for leaderboard, saves)

attach_postgres(app_name: "mygame", environment: "dev", tier: "pg-1")

Step 3: Deploy

deploy_app(app_name: "mygame", branch: "main")

Your game is live at https://mygame-<team>-dev.promptship.dev with the API at /api/.

How does the game talk to the API?

Since both are served from the same domain, use relative URLs:

async function submitScore(score) {
  const res = await fetch("/api/scores", {
    method: "POST",
    headers: {"Content-Type": "application/json"},
    body: JSON.stringify({score})
  });
  return res.json();
}

No CORS needed: Since the game and API share the same domain, there are no cross-origin issues.

Next steps