This file is read twice, and the second time there is no environment
The deploy went green and the URL hung: it connected and then sent nothing, for as long as anything waited. Modal imports `app.py` again inside the container, to find `serve` by name. So everything at module level runs in both places — and the environment is not the same in both. `FRQ_WEB_IMAGE` is set by whoever deploys and by nobody in the container, so the `raise` that was there to stop a deploy without a tag killed every container on start instead. The port never opened, the proxy waited, and the deploy that reported success had deployed something that could not run. `modal.is_local()` is the distinction, and is exported for exactly this. The check stays where it was and now only fires where it means something; in the container the tag is beside the point, since the image is already the one that was deployed, so a placeholder stands in and is never built. This is the thing the loader avoided by pickling the function body instead of importing it — `serialized=True`, with a comment about the re-import — and the rewrite dropped the trick and then walked into what it was for. Deployed and checked rather than reasoned about: the URL answers 200 in 0.4s and serves the page, `frq_core.js` and `frq_host.js`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d0cf0eb parent: bd10e81 modified
.modal/web/app.py +13 -4 | @@ -20,10 +20,19 @@ import subprocess | ||
| 20 | 20 | |
| 21 | 21 | import modal |
| 22 | 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. | |
| 23 | +# This file is read twice: here, by `modal deploy`, and again inside the | |
| 24 | +# container, where Modal imports it to find `serve` by name. Everything at | |
| 25 | +# module level therefore runs in both places — and the environment is not the | |
| 26 | +# same in both. `FRQ_WEB_IMAGE` is set by whoever deploys and by nobody in the | |
| 27 | +# container, so a bare `raise` here killed every container on start, the port | |
| 28 | +# never opened, and the URL hung while the deploy reported success. | |
| 29 | +# | |
| 30 | +# The tag CI just built and pushed. No default when deploying: unset, this | |
| 31 | +# fails rather than serving whatever was current the last time somebody ran | |
| 32 | +# it. In the container the tag is beside the point — the image is already the | |
| 33 | +# one that was deployed — so a placeholder stands in and is never built. | |
| 25 | 34 | IMAGE = os.environ.get("FRQ_WEB_IMAGE", "") |
| 26 | -if not IMAGE: | |
| 35 | +if modal.is_local() and not IMAGE: | |
| 27 | 36 | raise SystemExit( |
| 28 | 37 | "FRQ_WEB_IMAGE is unset. It is the image to serve, e.g.\n" |
| 29 | 38 | " FRQ_WEB_IMAGE=registry.rickub.com/nandi/frq-web:<sha> \\\n" |
| @@ -41,7 +50,7 @@ if not IMAGE: | ||
| 41 | 50 | # To go the other way, set the image private on rickub and pass |
| 42 | 51 | # `secret=modal.Secret.from_name("rickub-registry")` below, naming a Secret |
| 43 | 52 | # with REGISTRY_USERNAME / REGISTRY_PASSWORD for a pull-only token. |
| 44 | -image = modal.Image.from_registry(IMAGE) | |
| 53 | +image = modal.Image.from_registry(IMAGE or "python:3.13-slim") | |
| 45 | 54 | |
| 46 | 55 | # Named, and the name is what makes a second deploy replace the running one |
| 47 | 56 | # rather than stand another beside it. |
| @@ -20,10 +20,19 @@ import subprocess | |||
| 20 | 20 | ||
| 21 | import modal | 21 | import modal |
| 22 | 22 | ||
| 23 | -# The tag CI just built and pushed. No default: unset, this fails here rather | 23 | +# This file is read twice: here, by `modal deploy`, and again inside the |
| 24 | -# than deploying whatever was current the last time somebody ran it. | 24 | +# container, where Modal imports it to find `serve` by name. Everything at |
| 25 | +# module level therefore runs in both places — and the environment is not the | ||
| 26 | +# same in both. `FRQ_WEB_IMAGE` is set by whoever deploys and by nobody in the | ||
| 27 | +# container, so a bare `raise` here killed every container on start, the port | ||
| 28 | +# never opened, and the URL hung while the deploy reported success. | ||
| 29 | +# | ||
| 30 | +# The tag CI just built and pushed. No default when deploying: unset, this | ||
| 31 | +# fails rather than serving whatever was current the last time somebody ran | ||
| 32 | +# it. In the container the tag is beside the point — the image is already the | ||
| 33 | +# one that was deployed — so a placeholder stands in and is never built. | ||
| 25 | IMAGE = os.environ.get("FRQ_WEB_IMAGE", "") | 34 | IMAGE = os.environ.get("FRQ_WEB_IMAGE", "") |
| 26 | -if not IMAGE: | 35 | +if modal.is_local() and not IMAGE: |
| 27 | raise SystemExit( | 36 | raise SystemExit( |
| 28 | "FRQ_WEB_IMAGE is unset. It is the image to serve, e.g.\n" | 37 | "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" | 38 | " FRQ_WEB_IMAGE=registry.rickub.com/nandi/frq-web:<sha> \\\n" |
| @@ -41,7 +50,7 @@ if not IMAGE: | |||
| 41 | # To go the other way, set the image private on rickub and pass | 50 | # 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 | 51 | # `secret=modal.Secret.from_name("rickub-registry")` below, naming a Secret |
| 43 | # with REGISTRY_USERNAME / REGISTRY_PASSWORD for a pull-only token. | 52 | # with REGISTRY_USERNAME / REGISTRY_PASSWORD for a pull-only token. |
| 44 | -image = modal.Image.from_registry(IMAGE) | 53 | +image = modal.Image.from_registry(IMAGE or "python:3.13-slim") |
| 45 | 54 | ||
| 46 | # Named, and the name is what makes a second deploy replace the running one | 55 | # Named, and the name is what makes a second deploy replace the running one |
| 47 | # rather than stand another beside it. | 56 | # rather than stand another beside it. |