| The web container is plain Modal bd10e81 nandi 14h ago | 1 | """The web bundle CI built, served at a URL. |
| 2 | |
| 3 | FRQ_WEB_IMAGE=registry.rickub.com/nandi/frq-web:<sha> \ |
| 4 | modal deploy .modal/web/app.py |
| 5 | |
| 6 | Plain Modal, and no `container.toml` behind it. The `dev` container is |
| 7 | described by a spec because it is a sandbox with a volume, a toolchain and a |
| 8 | command that changes; this one is four constants and a `Popen`, and a spec |
| 9 | file for that was a second thing to read before the first one made sense. |
| 10 | |
| 11 | Nothing is built here. rickub builds the image — `Dockerfile` beside this |
| 12 | file, two stages, the second one just the bundle and a python — and pushes it |
| 13 | to `registry.rickub.com`; this deploys that exact tag. So the thing served is |
| 14 | the thing that was built and tested, and a deploy is a pull rather than a |
| 15 | compile. The workflow is `.rickub/workflows/web.yml`. |
| 16 | """ |
| 17 | |
| 18 | import os |
| 19 | import subprocess |
| 20 | |
| 21 | import modal |
| 22 | |
| 23 | # The tag CI just built and pushed. No default: unset, this fails here rather |
| 24 | # than deploying whatever was current the last time somebody ran it. |
| 25 | IMAGE = os.environ.get("FRQ_WEB_IMAGE", "") |
| 26 | if not IMAGE: |
| 27 | raise SystemExit( |
| 28 | "FRQ_WEB_IMAGE is unset. It is the image to serve, e.g.\n" |
| 29 | " FRQ_WEB_IMAGE=registry.rickub.com/nandi/frq-web:<sha> \\\n" |
| 30 | " modal deploy .modal/web/app.py" |
| 31 | ) |
| 32 | |
| 33 | # No `registry_secret`, which is a decision rather than an omission. Modal |
| 34 | # pulls on every cold start — not once at deploy time — so a private image |
| 35 | # would need a long-lived rickub deploy token kept as a Modal Secret, where |
| 36 | # the workflow's own token is short-lived by design. The image is public |
| 37 | # instead: it holds `build/web` and a python to serve it, and that bundle is |
| 38 | # what the URL hands to anyone who opens it, so a credential here would be |
| 39 | # guarding a copy of the public site. |
| 40 | # |
| 41 | # To go the other way, set the image private on rickub and pass |
| 42 | # `secret=modal.Secret.from_name("rickub-registry")` below, naming a Secret |
| 43 | # with REGISTRY_USERNAME / REGISTRY_PASSWORD for a pull-only token. |
| 44 | image = modal.Image.from_registry(IMAGE) |
| 45 | |
| 46 | # Named, and the name is what makes a second deploy replace the running one |
| 47 | # rather than stand another beside it. |
| 48 | app = modal.App("frq-web", image=image) |
| 49 | |
| 50 | PORT = 8000 |
| 51 | SERVE = f"python3 -m http.server {PORT} --directory /srv/web" |
| 52 | |
| 53 | |
| 54 | @app.function(cpu=1, memory=1024, timeout=3600, min_containers=1) |
| 55 | # One container answering many requests: a static bundle costs nothing per |
| 56 | # request, so scaling out on concurrency would buy cold starts and nothing |
| 57 | # else. |
| 58 | @modal.concurrent(max_inputs=100) |
| 59 | # `web_server` waits for the port to accept a connection and then proxies to |
| 60 | # it, so the command has to keep running — `Popen` and return, not `run`. |
| 61 | @modal.web_server(port=PORT, startup_timeout=60) |
| 62 | def serve(): |
| 63 | subprocess.Popen(SERVE, shell=True) |