The web container is plain Modal
`.modal/web/app.py`, and no spec behind it. Sixty-three lines, most of them comments, against a stub that told you to go and read a `container.toml` which told `_loader.py` how to build the same four constants and a `Popen`. `dev` keeps its spec, and the difference between them is the point: a sandbox with a volume, a toolchain and a command that changes is worth describing. A static bundle behind a `web_server` is not — there was a file to read before the first file made sense, and now there is one file that says what it does. Nothing else changes. The app is still named `frq-web`, so a deploy still replaces the running one in place; the image is still whatever `FRQ_WEB_IMAGE` names, and still fails loudly when it names nothing; there is still no registry secret, for the reason written beside the call that would take one. `just deploy` looks for `app.py` and falls back to `container.py`, so it works for either kind. Checked against the modal in `$PATH` rather than by eye: `web_server`, `concurrent`, `from_registry` and the function kwargs all match 1.5.4, and loading the file with the CLI's own site-packages gives one app named `frq-web` with `serve` registered as a web endpoint. There is no dry run for a deploy, so that is as close as this gets without one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bd10e81 parent: 5581b0f modified
.modal/web/README.md +13 -9 | @@ -1,10 +1,12 @@ | ||
| 1 | 1 | # `web` |
| 2 | 2 | |
| 3 | 3 | FRQ_WEB_IMAGE=registry.rickub.com/nandi/frq-web:<sha> \ |
| 4 | - modal deploy .modal/web/container.py | |
| 4 | + modal deploy .modal/web/app.py | |
| 5 | 5 | |
| 6 | -Defined by `container.toml`; `../_loader.py` is what reads it, and | |
| 7 | -its comments are the spec. | |
| 6 | +Plain Modal, in `app.py`. There is no `container.toml` here and no | |
| 7 | +`_loader.py` behind it: `dev` has a spec because it is a sandbox | |
| 8 | +with a volume, a toolchain and a command that changes, and this is | |
| 9 | +four constants and a `Popen`. | |
| 8 | 10 | |
| 9 | 11 | Unlike `dev`, this container builds nothing. rickub builds the image |
| 10 | 12 | -- `Dockerfile` here, two stages, the second one just the bundle and |
| @@ -13,10 +15,12 @@ exact tag. So the thing served is the thing that was built and | ||
| 13 | 15 | tested, and a deploy is a pull rather than a compile. The workflow is |
| 14 | 16 | `.rickub/workflows/web.yml`. |
| 15 | 17 | |
| 16 | -`runtime = "web"`: a Function whose [run] command listens on the one | |
| 17 | -[network] port, fronted by a stable https URL. `modal deploy` leaves | |
| 18 | -it up, and deploying again replaces it in place because the app is | |
| 19 | -named by `[container] name`. | |
| 18 | +`@modal.web_server`: a Function whose command listens on a port, | |
| 19 | +fronted by a stable https URL. Modal waits for the port to accept a | |
| 20 | +connection and then proxies to it, which is why the command is a | |
| 21 | +`Popen` that keeps running rather than a `run` that finishes. | |
| 22 | +`modal deploy` leaves it up, and deploying again replaces it in | |
| 23 | +place because `modal.App("frq-web")` names it. | |
| 20 | 24 | |
| 21 | 25 | One credential and one setting live outside the repo, both one-time: |
| 22 | 26 | |
| @@ -27,9 +31,9 @@ One credential and one setting live outside the repo, both one-time: | ||
| 27 | 31 | visibility. Private is the default, and the first push creates it |
| 28 | 32 | private, so this is done once after the first green run. |
| 29 | 33 | |
| 30 | -The second is why there is no `registry_secret` in `container.toml`. | |
| 34 | +The second is why `from_registry` is called without a `secret`. | |
| 31 | 35 | Modal pulls on every cold start rather than once at deploy time, so a |
| 32 | 36 | private image would want a long-lived rickub deploy token held as a |
| 33 | 37 | Modal Secret -- and what it would be guarding is `build/web`, which |
| 34 | 38 | the URL hands to anyone who opens it. The alternative is written down |
| 35 | -in `container.toml` for whoever wants it. | |
| 39 | +in `app.py` for whoever wants it. | |
| @@ -1,10 +1,12 @@ | |||
| 1 | # `web` | 1 | # `web` |
| 2 | 2 | ||
| 3 | FRQ_WEB_IMAGE=registry.rickub.com/nandi/frq-web:<sha> \ | 3 | FRQ_WEB_IMAGE=registry.rickub.com/nandi/frq-web:<sha> \ |
| 4 | - modal deploy .modal/web/container.py | 4 | + modal deploy .modal/web/app.py |
| 5 | 5 | ||
| 6 | -Defined by `container.toml`; `../_loader.py` is what reads it, and | 6 | +Plain Modal, in `app.py`. There is no `container.toml` here and no |
| 7 | -its comments are the spec. | 7 | +`_loader.py` behind it: `dev` has a spec because it is a sandbox |
| 8 | +with a volume, a toolchain and a command that changes, and this is | ||
| 9 | +four constants and a `Popen`. | ||
| 8 | 10 | ||
| 9 | Unlike `dev`, this container builds nothing. rickub builds the image | 11 | Unlike `dev`, this container builds nothing. rickub builds the image |
| 10 | -- `Dockerfile` here, two stages, the second one just the bundle and | 12 | -- `Dockerfile` here, two stages, the second one just the bundle and |
| @@ -13,10 +15,12 @@ exact tag. So the thing served is the thing that was built and | |||
| 13 | tested, and a deploy is a pull rather than a compile. The workflow is | 15 | tested, and a deploy is a pull rather than a compile. The workflow is |
| 14 | `.rickub/workflows/web.yml`. | 16 | `.rickub/workflows/web.yml`. |
| 15 | 17 | ||
| 16 | -`runtime = "web"`: a Function whose [run] command listens on the one | 18 | +`@modal.web_server`: a Function whose command listens on a port, |
| 17 | -[network] port, fronted by a stable https URL. `modal deploy` leaves | 19 | +fronted by a stable https URL. Modal waits for the port to accept a |
| 18 | -it up, and deploying again replaces it in place because the app is | 20 | +connection and then proxies to it, which is why the command is a |
| 19 | -named by `[container] name`. | 21 | +`Popen` that keeps running rather than a `run` that finishes. |
| 22 | +`modal deploy` leaves it up, and deploying again replaces it in | ||
| 23 | +place because `modal.App("frq-web")` names it. | ||
| 20 | 24 | ||
| 21 | One credential and one setting live outside the repo, both one-time: | 25 | One credential and one setting live outside the repo, both one-time: |
| 22 | 26 | ||
| @@ -27,9 +31,9 @@ One credential and one setting live outside the repo, both one-time: | |||
| 27 | visibility. Private is the default, and the first push creates it | 31 | visibility. Private is the default, and the first push creates it |
| 28 | private, so this is done once after the first green run. | 32 | private, so this is done once after the first green run. |
| 29 | 33 | ||
| 30 | -The second is why there is no `registry_secret` in `container.toml`. | 34 | +The second is why `from_registry` is called without a `secret`. |
| 31 | Modal pulls on every cold start rather than once at deploy time, so a | 35 | Modal pulls on every cold start rather than once at deploy time, so a |
| 32 | private image would want a long-lived rickub deploy token held as a | 36 | private image would want a long-lived rickub deploy token held as a |
| 33 | Modal Secret -- and what it would be guarding is `build/web`, which | 37 | Modal Secret -- and what it would be guarding is `build/web`, which |
| 34 | the URL hands to anyone who opens it. The alternative is written down | 38 | the URL hands to anyone who opens it. The alternative is written down |
| 35 | -in `container.toml` for whoever wants it. | 39 | +in `app.py` for whoever wants it. |
added
.modal/web/app.py +63 -0 | new file mode 100644 | ||
| @@ -0,0 +1,63 @@ | ||
| 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) | |
| new file mode 100644 | |||
| @@ -0,0 +1,63 @@ | |||
| 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) | ||
deleted
.modal/web/container.py +0 -19 | deleted file mode 100644 | ||
| @@ -1,19 +0,0 @@ | ||
| 1 | -"""Generated stub -- the container is defined by container.toml. | |
| 2 | - | |
| 3 | -Edit container.toml, not this file. | |
| 4 | -""" | |
| 5 | - | |
| 6 | -import os | |
| 7 | -import sys | |
| 8 | - | |
| 9 | -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| 10 | - | |
| 11 | -from _loader import Container # noqa: E402 | |
| 12 | - | |
| 13 | -c = Container.from_toml(__file__) | |
| 14 | -image, app = c.image, c.app | |
| 15 | - | |
| 16 | -# A web container's Function is not vestigial the way a sandbox's is: it *is* | |
| 17 | -# the container. Registered at import time so that `modal deploy` finds it, | |
| 18 | -# and so that the re-import inside the container binds the same body. | |
| 19 | -serve = c.register_web() | |
| deleted file mode 100644 | |||
| @@ -1,19 +0,0 @@ | |||
| 1 | -"""Generated stub -- the container is defined by container.toml. | ||
| 2 | - | ||
| 3 | -Edit container.toml, not this file. | ||
| 4 | -""" | ||
| 5 | - | ||
| 6 | -import os | ||
| 7 | -import sys | ||
| 8 | - | ||
| 9 | -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
| 10 | - | ||
| 11 | -from _loader import Container # noqa: E402 | ||
| 12 | - | ||
| 13 | -c = Container.from_toml(__file__) | ||
| 14 | -image, app = c.image, c.app | ||
| 15 | - | ||
| 16 | -# A web container's Function is not vestigial the way a sandbox's is: it *is* | ||
| 17 | -# the container. Registered at import time so that `modal deploy` finds it, | ||
| 18 | -# and so that the re-import inside the container binds the same body. | ||
| 19 | -serve = c.register_web() | ||
deleted
.modal/web/container.toml +0 -52 | deleted file mode 100644 | ||
| @@ -1,52 +0,0 @@ | ||
| 1 | -[container] | |
| 2 | -name = "frq-web" | |
| 3 | -description = "the web bundle CI built, served at a URL" | |
| 4 | -# Nothing is built here. `registry` is an image CI already made -- the | |
| 5 | -# Dockerfile beside this file -- and `${FRQ_WEB_IMAGE}` is expanded when this | |
| 6 | -# spec is read, so the tag is whatever commit the deploy job is deploying. | |
| 7 | -# Unset, it fails loudly rather than deploying something stale. | |
| 8 | -registry = "${FRQ_WEB_IMAGE}" | |
| 9 | -# No registry_secret, which is a decision and not an omission. Images on | |
| 10 | -# registry.rickub.com are private by default and Modal pulls on every cold | |
| 11 | -# start -- not once at deploy time -- so a private image here would need a | |
| 12 | -# long-lived rickub deploy token kept as a Modal Secret, since the workflow's | |
| 13 | -# own registry token is short-lived by design. The image is made public | |
| 14 | -# instead. It holds `build/web` and a python to serve it, and that bundle is | |
| 15 | -# what the URL hands to anyone who opens it, so there is nothing in there to | |
| 16 | -# keep: a credential to rotate would be guarding a copy of the public site. | |
| 17 | -# | |
| 18 | -# To go the other way, set the image private on rickub and add: | |
| 19 | -# registry_secret = "rickub-registry" | |
| 20 | -# naming a Modal Secret with REGISTRY_USERNAME / REGISTRY_PASSWORD for a | |
| 21 | -# pull-only deploy token (rickub Settings -> Packages; the username is any | |
| 22 | -# label). | |
| 23 | -# Not a Sandbox: a Sandbox is a run that ends, and this is meant to stay up | |
| 24 | -# between pushes. `web` is a Function that Modal fronts with a stable https | |
| 25 | -# URL and `modal deploy` leaves running. | |
| 26 | -runtime = "web" | |
| 27 | - | |
| 28 | -[build] | |
| 29 | -# Empty, and that is the whole point of this container: the image is already | |
| 30 | -# what it should be. A copy here would add a layer to CI's image -- rebuilt | |
| 31 | -# and re-pushed at deploy time -- and the tree it copied would be the | |
| 32 | -# deploying machine's rather than the one that was built and tested. | |
| 33 | -include = [] | |
| 34 | - | |
| 35 | -[network] | |
| 36 | -ports = [8000] | |
| 37 | - | |
| 38 | -[resources] | |
| 39 | -# A static bundle over http.server. The cost is the pull, not the serving. | |
| 40 | -cpu = 1 | |
| 41 | -memory = 1024 | |
| 42 | -timeout = 3600 | |
| 43 | -# One container kept warm, so the URL answers without a cold pull after a | |
| 44 | -# quiet spell. Set it to 0 to pay that second or two instead. | |
| 45 | -min_containers = 1 | |
| 46 | - | |
| 47 | -[run] | |
| 48 | -workdir = "/srv" | |
| 49 | -# The image's own CMD, said again here because `web` starts the [run] command | |
| 50 | -# and not the CMD: one place says what runs, and `--shell` into this | |
| 51 | -# container gets the same line. | |
| 52 | -command = "python3 -m http.server 8000 --directory /srv/web" | |
| deleted file mode 100644 | |||
| @@ -1,52 +0,0 @@ | |||
| 1 | -[container] | ||
| 2 | -name = "frq-web" | ||
| 3 | -description = "the web bundle CI built, served at a URL" | ||
| 4 | -# Nothing is built here. `registry` is an image CI already made -- the | ||
| 5 | -# Dockerfile beside this file -- and `${FRQ_WEB_IMAGE}` is expanded when this | ||
| 6 | -# spec is read, so the tag is whatever commit the deploy job is deploying. | ||
| 7 | -# Unset, it fails loudly rather than deploying something stale. | ||
| 8 | -registry = "${FRQ_WEB_IMAGE}" | ||
| 9 | -# No registry_secret, which is a decision and not an omission. Images on | ||
| 10 | -# registry.rickub.com are private by default and Modal pulls on every cold | ||
| 11 | -# start -- not once at deploy time -- so a private image here would need a | ||
| 12 | -# long-lived rickub deploy token kept as a Modal Secret, since the workflow's | ||
| 13 | -# own registry token is short-lived by design. The image is made public | ||
| 14 | -# instead. It holds `build/web` and a python to serve it, and that bundle is | ||
| 15 | -# what the URL hands to anyone who opens it, so there is nothing in there to | ||
| 16 | -# keep: a credential to rotate would be guarding a copy of the public site. | ||
| 17 | -# | ||
| 18 | -# To go the other way, set the image private on rickub and add: | ||
| 19 | -# registry_secret = "rickub-registry" | ||
| 20 | -# naming a Modal Secret with REGISTRY_USERNAME / REGISTRY_PASSWORD for a | ||
| 21 | -# pull-only deploy token (rickub Settings -> Packages; the username is any | ||
| 22 | -# label). | ||
| 23 | -# Not a Sandbox: a Sandbox is a run that ends, and this is meant to stay up | ||
| 24 | -# between pushes. `web` is a Function that Modal fronts with a stable https | ||
| 25 | -# URL and `modal deploy` leaves running. | ||
| 26 | -runtime = "web" | ||
| 27 | - | ||
| 28 | -[build] | ||
| 29 | -# Empty, and that is the whole point of this container: the image is already | ||
| 30 | -# what it should be. A copy here would add a layer to CI's image -- rebuilt | ||
| 31 | -# and re-pushed at deploy time -- and the tree it copied would be the | ||
| 32 | -# deploying machine's rather than the one that was built and tested. | ||
| 33 | -include = [] | ||
| 34 | - | ||
| 35 | -[network] | ||
| 36 | -ports = [8000] | ||
| 37 | - | ||
| 38 | -[resources] | ||
| 39 | -# A static bundle over http.server. The cost is the pull, not the serving. | ||
| 40 | -cpu = 1 | ||
| 41 | -memory = 1024 | ||
| 42 | -timeout = 3600 | ||
| 43 | -# One container kept warm, so the URL answers without a cold pull after a | ||
| 44 | -# quiet spell. Set it to 0 to pay that second or two instead. | ||
| 45 | -min_containers = 1 | ||
| 46 | - | ||
| 47 | -[run] | ||
| 48 | -workdir = "/srv" | ||
| 49 | -# The image's own CMD, said again here because `web` starts the [run] command | ||
| 50 | -# and not the CMD: one place says what runs, and `--shell` into this | ||
| 51 | -# container gets the same line. | ||
| 52 | -command = "python3 -m http.server 8000 --directory /srv/web" | ||
modified
.rickub/workflows/web.yml +6 -6 | @@ -55,8 +55,8 @@ jobs: | ||
| 55 | 55 | # ...and Modal serves it. |
| 56 | 56 | # |
| 57 | 57 | # `modal deploy` and not `modal run`: a run is a job that ends, and this is |
| 58 | - # a URL that should still be there on the next push. The app is named by | |
| 59 | - # `[container] name` in `.modal/web/container.toml`, so deploying again | |
| 58 | + # a URL that should still be there on the next push. The app is named in | |
| 59 | + # `.modal/web/app.py` — `modal.App("frq-web")` — so deploying again | |
| 60 | 60 | # replaces the running one rather than standing a second one beside it. |
| 61 | 61 | deploy: |
| 62 | 62 | runs-on: ubuntu-latest |
| @@ -71,8 +71,8 @@ jobs: | ||
| 71 | 71 | env: |
| 72 | 72 | MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} |
| 73 | 73 | MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} |
| 74 | - # What `${FRQ_WEB_IMAGE}` in container.toml expands to. The job | |
| 75 | - # above computed it; passing it forward rather than recomputing it | |
| 76 | - # keeps one place saying what the tag is. | |
| 74 | + # The image `app.py` serves. The job above computed the tag; | |
| 75 | + # passing it forward rather than recomputing it keeps one place | |
| 76 | + # saying what it is. | |
| 77 | 77 | FRQ_WEB_IMAGE: ${{ needs.image.outputs.image }} |
| 78 | - run: modal deploy .modal/web/container.py | |
| 78 | + run: modal deploy .modal/web/app.py | |
| @@ -55,8 +55,8 @@ jobs: | |||
| 55 | # ...and Modal serves it. | 55 | # ...and Modal serves it. |
| 56 | # | 56 | # |
| 57 | # `modal deploy` and not `modal run`: a run is a job that ends, and this is | 57 | # `modal deploy` and not `modal run`: a run is a job that ends, and this is |
| 58 | - # a URL that should still be there on the next push. The app is named by | 58 | + # a URL that should still be there on the next push. The app is named in |
| 59 | - # `[container] name` in `.modal/web/container.toml`, so deploying again | 59 | + # `.modal/web/app.py` — `modal.App("frq-web")` — so deploying again |
| 60 | # replaces the running one rather than standing a second one beside it. | 60 | # replaces the running one rather than standing a second one beside it. |
| 61 | deploy: | 61 | deploy: |
| 62 | runs-on: ubuntu-latest | 62 | runs-on: ubuntu-latest |
| @@ -71,8 +71,8 @@ jobs: | |||
| 71 | env: | 71 | env: |
| 72 | MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} | 72 | MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} |
| 73 | MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} | 73 | MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} |
| 74 | - # What `${FRQ_WEB_IMAGE}` in container.toml expands to. The job | 74 | + # The image `app.py` serves. The job above computed the tag; |
| 75 | - # above computed it; passing it forward rather than recomputing it | 75 | + # passing it forward rather than recomputing it keeps one place |
| 76 | - # keeps one place saying what the tag is. | 76 | + # saying what it is. |
| 77 | FRQ_WEB_IMAGE: ${{ needs.image.outputs.image }} | 77 | FRQ_WEB_IMAGE: ${{ needs.image.outputs.image }} |
| 78 | - run: modal deploy .modal/web/container.py | 78 | + run: modal deploy .modal/web/app.py |
modified
CLAUDE.md +10 -4 | @@ -28,10 +28,16 @@ deployed apps by name, not the ephemeral one a `modal run` creates, and carries | ||
| 28 | 28 | nothing until the Sandbox starts — the image build streams to the client and |
| 29 | 29 | nowhere else. |
| 30 | 30 | |
| 31 | -Two containers, and they are not the same kind of thing. `dev` is a build | |
| 32 | -that ends. `web` is a deploy: rickub builds `.modal/web/Dockerfile` into | |
| 31 | +Two containers, and they are not the same kind of thing — which is why they | |
| 32 | +are not written the same way. `dev` is a build that ends, and is a | |
| 33 | +`container.toml` read by `_loader.py`: a sandbox with a volume, a toolchain | |
| 34 | +and a command that changes. `web` is a deploy, and is plain Modal in | |
| 35 | +`.modal/web/app.py`, because four constants and a `Popen` did not need a spec | |
| 36 | +file to be read before the file itself made sense. | |
| 37 | + | |
| 38 | +`web` is a deploy: rickub builds `.modal/web/Dockerfile` into | |
| 33 | 39 | `registry.rickub.com` (`.rickub/workflows/web.yml`), and |
| 34 | -`modal deploy .modal/web/container.py` serves that exact tag at a URL, | |
| 40 | +`modal deploy .modal/web/app.py` serves that exact tag at a URL, | |
| 35 | 41 | building nothing. So `just build web` on a laptop and the thing |
| 36 | 42 | on the internet come from the same two commands, run in different places — |
| 37 | 43 | and a deploy is a pull rather than a compile. |
| @@ -56,7 +62,7 @@ Let them write to the terminal, or `tee` them if you want a copy to grep | ||
| 56 | 62 | afterwards: |
| 57 | 63 | |
| 58 | 64 | ```bash |
| 59 | -modal run .modal/web/container.py 2>&1 | tee /tmp/frq-build.log | |
| 65 | +modal run .modal/dev/container.py 2>&1 | tee /tmp/frq-build.log | |
| 60 | 66 | ``` |
| 61 | 67 | |
| 62 | 68 | Trim afterwards, on the file, where the whole run is still there to re-read. |
| @@ -28,10 +28,16 @@ deployed apps by name, not the ephemeral one a `modal run` creates, and carries | |||
| 28 | nothing until the Sandbox starts — the image build streams to the client and | 28 | nothing until the Sandbox starts — the image build streams to the client and |
| 29 | nowhere else. | 29 | nowhere else. |
| 30 | 30 | ||
| 31 | -Two containers, and they are not the same kind of thing. `dev` is a build | 31 | +Two containers, and they are not the same kind of thing — which is why they |
| 32 | -that ends. `web` is a deploy: rickub builds `.modal/web/Dockerfile` into | 32 | +are not written the same way. `dev` is a build that ends, and is a |
| 33 | +`container.toml` read by `_loader.py`: a sandbox with a volume, a toolchain | ||
| 34 | +and a command that changes. `web` is a deploy, and is plain Modal in | ||
| 35 | +`.modal/web/app.py`, because four constants and a `Popen` did not need a spec | ||
| 36 | +file to be read before the file itself made sense. | ||
| 37 | + | ||
| 38 | +`web` is a deploy: rickub builds `.modal/web/Dockerfile` into | ||
| 33 | `registry.rickub.com` (`.rickub/workflows/web.yml`), and | 39 | `registry.rickub.com` (`.rickub/workflows/web.yml`), and |
| 34 | -`modal deploy .modal/web/container.py` serves that exact tag at a URL, | 40 | +`modal deploy .modal/web/app.py` serves that exact tag at a URL, |
| 35 | building nothing. So `just build web` on a laptop and the thing | 41 | building nothing. So `just build web` on a laptop and the thing |
| 36 | on the internet come from the same two commands, run in different places — | 42 | on the internet come from the same two commands, run in different places — |
| 37 | and a deploy is a pull rather than a compile. | 43 | and a deploy is a pull rather than a compile. |
| @@ -56,7 +62,7 @@ Let them write to the terminal, or `tee` them if you want a copy to grep | |||
| 56 | afterwards: | 62 | afterwards: |
| 57 | 63 | ||
| 58 | ```bash | 64 | ```bash |
| 59 | -modal run .modal/web/container.py 2>&1 | tee /tmp/frq-build.log | 65 | +modal run .modal/dev/container.py 2>&1 | tee /tmp/frq-build.log |
| 60 | ``` | 66 | ``` |
| 61 | 67 | ||
| 62 | Trim afterwards, on the file, where the whole run is still there to re-read. | 68 | Trim afterwards, on the file, where the whole run is still there to re-read. |
modified
justfile +5 -1 | @@ -157,7 +157,11 @@ deploy container="web": | ||
| 157 | 157 | echo " e.g. registry.gitlab.com/<ns>/frq/web:\$(git rev-parse HEAD)" >&2 |
| 158 | 158 | exit 1 |
| 159 | 159 | fi |
| 160 | - exec modal deploy ".modal/{{container}}/container.py" | |
| 160 | + # `app.py` for `web`, which is plain Modal; a `container.py` for anything | |
| 161 | + # described by a `container.toml`. | |
| 162 | + spec=".modal/{{container}}/app.py" | |
| 163 | + [ -f "$spec" ] || spec=".modal/{{container}}/container.py" | |
| 164 | + exec modal deploy "$spec" | |
| 161 | 165 | |
| 162 | 166 | # The same core, compiled to JavaScript. |
| 163 | 167 | # |
| @@ -157,7 +157,11 @@ deploy container="web": | |||
| 157 | echo " e.g. registry.gitlab.com/<ns>/frq/web:\$(git rev-parse HEAD)" >&2 | 157 | echo " e.g. registry.gitlab.com/<ns>/frq/web:\$(git rev-parse HEAD)" >&2 |
| 158 | exit 1 | 158 | exit 1 |
| 159 | fi | 159 | fi |
| 160 | - exec modal deploy ".modal/{{container}}/container.py" | 160 | + # `app.py` for `web`, which is plain Modal; a `container.py` for anything |
| 161 | + # described by a `container.toml`. | ||
| 162 | + spec=".modal/{{container}}/app.py" | ||
| 163 | + [ -f "$spec" ] || spec=".modal/{{container}}/container.py" | ||
| 164 | + exec modal deploy "$spec" | ||
| 161 | 165 | ||
| 162 | # The same core, compiled to JavaScript. | 166 | # The same core, compiled to JavaScript. |
| 163 | # | 167 | # |