nandi/frqpublic Fork 0
bd10e81
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

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>
nandi committed 2026-09-19T23:12:36-07:00 Browse files
bd10e81 parent: 5581b0f
modified .modal/web/README.md +13 -9
@@ -1,10 +1,12 @@
11 # `web`
22
33 FRQ_WEB_IMAGE=registry.rickub.com/nandi/frq-web:<sha> \
4- modal deploy .modal/web/container.py
4+ modal deploy .modal/web/app.py
55
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`.
810
911 Unlike `dev`, this container builds nothing. rickub builds the image
1012 -- `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
1315 tested, and a deploy is a pull rather than a compile. The workflow is
1416 `.rickub/workflows/web.yml`.
1517
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.
2024
2125 One credential and one setting live outside the repo, both one-time:
2226
@@ -27,9 +31,9 @@ One credential and one setting live outside the repo, both one-time:
2731 visibility. Private is the default, and the first push creates it
2832 private, so this is done once after the first green run.
2933
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`.
3135 Modal pulls on every cold start rather than once at deploy time, so a
3236 private image would want a long-lived rickub deploy token held as a
3337 Modal Secret -- and what it would be guarding is `build/web`, which
3438 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.py4+ modal deploy .modal/web/app.py
5 5
6-Defined by `container.toml`; `../_loader.py` is what reads it, and6+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 image11 Unlike `dev`, this container builds nothing. rickub builds the image
10 -- `Dockerfile` here, two stages, the second one just the bundle and12 -- `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 is15 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 one18+`@modal.web_server`: a Function whose command listens on a port,
17-[network] port, fronted by a stable https URL. `modal deploy` leaves19+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 is20+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 it31 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 a35 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 a36 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`, which37 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 down38 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:
5555 # ...and Modal serves it.
5656 #
5757 # `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
6060 # replaces the running one rather than standing a second one beside it.
6161 deploy:
6262 runs-on: ubuntu-latest
@@ -71,8 +71,8 @@ jobs:
7171 env:
7272 MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
7373 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.
7777 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 is57 # `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 by58+ # 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 again59+ # `.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-latest62 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 job74+ # The image `app.py` serves. The job above computed the tag;
75- # above computed it; passing it forward rather than recomputing it75+ # 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.py78+ 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
2828 nothing until the Sandbox starts — the image build streams to the client and
2929 nowhere else.
3030
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
3339 `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,
3541 building nothing. So `just build web` on a laptop and the thing
3642 on the internet come from the same two commands, run in different places —
3743 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
5662 afterwards:
5763
5864 ```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
6066 ```
6167
6268 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 and28 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 build31+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` into32+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`), and39 `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 thing41 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 ```bash64 ```bash
59-modal run .modal/web/container.py 2>&1 | tee /tmp/frq-build.log65+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":
157157 echo " e.g. registry.gitlab.com/<ns>/frq/web:\$(git rev-parse HEAD)" >&2
158158 exit 1
159159 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"
161165
162166 # The same core, compiled to JavaScript.
163167 #
@@ -157,7 +157,11 @@ deploy container="web":
157 echo " e.g. registry.gitlab.com/<ns>/frq/web:\$(git rev-parse HEAD)" >&2157 echo " e.g. registry.gitlab.com/<ns>/frq/web:\$(git rev-parse HEAD)" >&2
158 exit 1158 exit 1
159 fi159 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 #