feat(docker): default app image, standalone by docker run - #281
Merged
Conversation
The repo shipped a Celery worker image but nothing that ran the app
itself, so "run the default app in Docker" meant writing a Dockerfile by
hand — even though every `smpy new` scaffold gets one.
Adds a root `Dockerfile` (the conventional place: `docker build .` and
most PaaS auto-detect it) building host + every bundled module in one
uv+Node builder stage, because the Vite build imports
`modules.generated.{ts,css}` that `gen-pages` emits from the *installed
Python modules*. Runtime is `python:3.12-slim`, non-root, healthchecked.
Standalone means standalone: SQLite under /app/data, no Postgres and no
Redis needed to boot. `docker/entrypoint.sh` applies `alembic upgrade
heads` and generates ephemeral values for the three secrets production
refuses to start without, so a bare `docker run -p 8000:8000` works.
Two fixes were load-bearing for that:
- `BackgroundTasksSettings` read neither `SM_BG_TASKS_BROKER_URL` nor
`SM_BG_TASKS_RESULT_BACKEND`, so the production validator only ever
saw the localhost defaults it rejects — no container with the module
installed could boot, and the compose worker/beat services silently
used localhost instead of the `redis` hostname they set. Both fields
now resolve from env at construction; DB hydration still wins after.
- worker/beat declared a required `env_file: .env`, which is gitignored
— a fresh clone couldn't `docker compose` anything at all, the new app
service included.
Verified locally: `docker run` and `docker compose up app` both boot
healthy with no other services, admin bootstrap + browser login work,
the built bundle hydrates (no Vite dev-server tags), precompressed
assets serve from /static, and data survives a restart on the volume.
Claude-Session: https://claude.ai/code/session_01MKtkDrsfDCwZtXxbPGK3Tv
Deploying simple-module-python with
|
| Latest commit: |
75180d3
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4af27c96.simple-module-python.pages.dev |
| Branch Preview URL: | https://worktree-docker-default-app.simple-module-python.pages.dev |
A background queue means a second process and a broker — the opposite of what a standalone image is for. The default image was setting SM_BG_TASKS_BROKER_URL to the compose `redis` hostname purely to satisfy the production validator, advertising a dependency it never used. The build now passes `--no-install-package simple-module-background-tasks`, so the module has no entry point to discover: no broker env, no Celery settings, no admin page, and none of its pages in the bundle. Background jobs stay where they belong — the worker/beat services built from docker/worker.Dockerfile. Verified: image boots healthy with zero SM_BG_TASKS_* vars set, login and /dashboard/ work, /admin/background-tasks/ 404s, the admin settings list and sidebar no longer mention it, and `modules.generated.ts` has no background_tasks entry. Claude-Session: https://claude.ai/code/session_01MKtkDrsfDCwZtXxbPGK3Tv
A fresh container served a login page nobody held credentials for: the users bootstrap needs both an email and a password, and unset meant no account at all. The compose file and `make docker-app` papered over it with `admin`/`admin`, which is a bad thing to bake into an image that is also meant to run on real hosts. The entrypoint now defaults `SM_USERS_BOOTSTRAP_EMAIL` to admin@example.com and, when no password is given, generates one and prints it once. Explicit values still win, and compose/make just pass the vars through instead of forcing weak ones. Safe on a reused volume by construction: the users module applies the seed only while its table is empty, so the printed password is a first-boot value and an existing account is never touched. The banner says so, and points at `smpy users create-admin --force` for recovery. Verified on a fresh volume: printed credentials log in (204), `admin`/`admin` is rejected (400), the password survives a restart unchanged, and passing both env vars logs no banner and uses them. Claude-Session: https://claude.ai/code/session_01MKtkDrsfDCwZtXxbPGK3Tv
The image previously generated a random first-boot password and printed it once. A fixed, well-known default is the better fit for a starter image you are meant to `docker run` and log straight into: no log-scraping step, and it matches the `changeme` that `.env.example` already uses for local dev, so the container and `make dev` behave the same. The seed still only lands while the users table is empty, so a public default cannot overwrite an existing account on a reused volume. The banner is loud about the tradeoff and names the two env vars that replace it. Claude-Session: https://claude.ai/code/session_01MKtkDrsfDCwZtXxbPGK3Tv
antosubash
marked this pull request as ready for review
August 24, 2026 09:20
|
You have reached your Codex usage limits for security reviews. Please try again later. |
antosubash
added a commit
that referenced
this pull request
Aug 28, 2026
* docs: catch the reference docs up to v0.0.32 Thirteen commits landed since the docs were last swept (admin section, Inertia cache guard, maintenance mode, the i18n gate, git module sources, the default Docker image). The PRs updated the pages they touched; this covers what they left behind. Broken links first. The admin move relocated every admin *view* URL and the reference docs largely still pointed at the old ones: /settings/modules, /users/admin, /audit_log/, /feature_flags/, /dashboard/doctor, /permissions/. Retargeted against each module's actual view_prefix/admin_view_prefix. The /api/* paths are a separate contract and did not move, so they are left alone. Three of those were wrong in a second way that a URL rewrite alone would have preserved: - /settings/modules/<package> names a route that does not exist at all — /admin/settings/ is one master/detail page, with no per-package path. - framework-conventions' MenuItem example used group_key "ui.nav_groups.administration"; the vocabulary is access|appearance| content|system, so the example shipped a key with no catalog entry. - fixtures.md posted form data to /users/admin/invite; the endpoint is /api/users/admin/invite and takes JSON. Then the features with no docs at all: - middleware.md listed 9 of 14 middlewares. Adds ProxyHeaders, GZip, InertiaCache, Maintenance and CommitBeforeResponse to both order blocks, with a section each, and states why the innermost three are ordered as they are — Maintenance inside InertiaCache so its short-circuited 503 does not ship a per-user payload past the cache guard, CommitBeforeResponse innermost so its send-wrapper sees the response first. - lifecycle.md was missing register_admin_routes and register_audit_links, and still showed register_event_handlers' one-arg signature. - i18n.md gains label_key/group_key, the cross-locale fallback, the ci-check-untranslated gate with its three exemption forms and its documented blind spot, and the installed-vs-active type generation split. - Maintenance mode had no operator documentation anywhere; deployment.md now covers what stays reachable, that it fails open, and that it is not a security boundary. - The CLI reference never listed smpy add / update / module verify / build. - pages.md gains AdminLayout, PageShell's section prop, and the topbar / breadcrumb / palette chrome. Two factual corrections beyond the missing pieces. env-vars.md claimed task_always_eager was "the one field still read from the environment" — #281 made broker_url and result_backend env-readable too, which is what lets a container boot before any DB row exists. And i18n.md documented SM_I18N_* as app configuration; those are read only by the standalone diagnostics runner, while the app takes its locale set from DB-backed HostSettings. Also adds SM_TRUSTED_PROXY, previously undocumented in the reference despite being required behind a TLS-terminating proxy. Verified: vitepress build clean (no dead links), check_readmes and check_metadata pass. Every prefix, hook, field and route name in the diff was read out of the source rather than inferred. Claude-Session: https://claude.ai/code/session_01JJtbN97VhtDr28Fuy5JKEF * style(docs): satisfy ruff format in two markdown code blocks `ruff format --check` covers Python fenced blocks inside markdown, and the aligned inline comments in the new MenuItem and ModuleMeta examples used column padding where ruff wants exactly two spaces before `#`. Caught by `make lint`, which is what CI runs. Whitespace only — no wording or code changes. Claude-Session: https://claude.ai/code/session_01JJtbN97VhtDr28Fuy5JKEF * docs: correct which host settings are env-backed and which are DB-backed A review flag on the "Tenant (only if SM_MULTI_TENANT=true)" line prompted checking it. The line is right — but checking it surfaced that the surrounding story was wrong in both directions, including a claim this branch had just introduced. `Settings` is `HostSettings` + `BootstrapSettings` and inherits the latter's `env_prefix="SM_"`, so every `HostSettings` field resolves from `SM_*` at boot. That instance lands on `app.state.sm.settings` and is what configures middleware at construction. The DB-hydrated instance is a plain `HostSettings` on `app.state.host.settings`, which declares no prefix. Verified by construction, not by reading: SM_I18N_DEFAULT_LOCALE=es -> Settings().i18n_default_locale == 'es' SM_MULTI_TENANT=true -> Settings().multi_tenant is True SM_MAINTENANCE_MODE=true -> Settings().maintenance_mode is True HostSettings().maintenance_mode is False Three corrections follow: - i18n.md claimed `SM_I18N_*` were read only by the diagnostics runner and that setting them "does not change what the running app serves". False, and introduced by this branch. They are the app's actual source: LocaleMiddleware, the i18n manifest, the shared-props builder and i18n_deps all read them off `app.state.sm.settings`. Editing those rows in the admin UI is what does nothing — the middleware captured its locale set at boot, and a settings save swaps a different object. - env-vars.md's "Host settings (DB-backed, not env)" said the same thing of tenancy and i18n. Now a per-field table naming the env var and which instance each field is read from, since the answer differs per field. - The maintenance fields have no working env var, which is worth stating because the code's shape suggests otherwise: `SM_MAINTENANCE_MODE=true` sets the boot object, nothing reads maintenance from there, and the hydrated `HostSettings` never sees the prefix. It looks plausible and silently does nothing. Also fixes middleware.md claiming `X-Tenant-ID` is the default tenant header. `TenantMiddleware.__init__` takes `header: str | None = None` and `tenant_header` defaults to `""`, so header lookup is off until a name is set; `X-Tenant-ID` is a suggested constant, not a default. Verified: make lint clean, 2152 passed / 2 skipped, 122 JS passed, vitepress build clean (new cross-page anchor resolves). Claude-Session: https://claude.ai/code/session_01JJtbN97VhtDr28Fuy5JKEF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The repo shipped
docker/worker.Dockerfile(Celery) but nothing that ran the app itself — even though everysmpy newscaffold gets a Dockerfile. This adds the equivalent for the reference app.What's here
Dockerfile(root — sodocker build .and PaaS auto-detection just work). One uv+Node builder stage, because the Vite build importsmodules.generated.{ts,css}thatgen-pagesemits from the installed Python modules; runtime ispython:3.12-slim-bookworm, non-root uid 10001,HEALTHCHECKon/health.docker/entrypoint.sh—alembic upgrade heads, plus ephemeral values for the three secrets production refuses to boot without (SM_SECRET_KEY,SM_USERS_{RESET_PASSWORD,VERIFICATION}_TOKEN_SECRET), so a baredocker run -p 8000:8000 <image>works. Loud warning; set them to keep sessions across restarts.SM_USERS_BOOTSTRAP_*a fresh container served a login page nobody held credentials for. The entrypoint now seedsadmin@example.com/changeme— the same pair.env.exampleuses for local dev, so the container andmake devbehave alike — and prints a warning naming the two env vars that replace it. The users module applies the seed only while its table is empty, so the public default cannot overwrite an existing account on a reused volume.appservice +make docker-build/docker-app/docker-compose-app(all honourSM_APP_PORT), README +docs/reference/deployment.md..dockerignore: excludes.git/and.claude/— the latter holds full worktree checkouts of this repo, which multiplied the build context.Standalone means standalone: SQLite under
/app/data, no Postgres and no Redis required.No Celery in the image. The build passes
--no-install-package simple-module-background-tasks, so the module has no entry point to discover — no broker env, no admin page, none of its pages in the bundle. A queue is a second process plus a Redis, which is the opposite of a standalone image; background jobs stay with theworker/beatservices built fromdocker/worker.Dockerfile. Drop the flag and setSM_BG_TASKS_BROKER_URL/_RESULT_BACKENDto run tasks from the web process.Two fixes that were load-bearing
BackgroundTasksSettingsread neitherSM_BG_TASKS_BROKER_URLnorSM_BG_TASKS_RESULT_BACKEND, so the production validator only ever saw the localhost defaults it rejects — no container with the module installed could boot in production, and the existing composeworker/beatsilently usedlocalhostinstead of theredishostname they set. Both fields now resolve from env at construction (DB hydration still wins afterwards). Covered bymodules/background_tasks/tests/test_bg_settings_env.py. Still needed by worker/beat and by any app that keeps the module.worker/beatdeclared a requiredenv_file: .env, which is gitignored — a fresh clone couldn'tdocker composeanything at all. Nowrequired: false, matching the CLI templates.Verification (local)
docker build→ 768MB image;docker run(with zeroSM_BG_TASKS_*vars set) anddocker compose up appboth reachhealth=healthywith no other services running.admin@example.com/changemelogs in (204) on a fresh volume and a wrong password is rejected (400); explicitSM_USERS_BOOTSTRAP_*values are used instead with no warning banner (204 for them, 400 for the default); and rebooting an existing volume with no env vars re-seeds nothing — the default is still rejected (400) and the original account still works (204). Browser login works too;/dashboard/and/admin/settings/render styled, console clean apart from the pre-existing/favicon.ico404 (no favicon configured)./admin/background-tasks/404s, the settings module list and admin sidebar don't mention it, andmodules.generated.tshas nobackground_tasksentry./static/dist/..., precompressed), not a Vite dev server; anonymous/dashboard/still 302s to login.docker compose restarton the volume; migrations are not re-applied.make ci-python-lint,ci-python-typecheck,ci-check-file-sizeclean; fulluv run pytest= 2150 passed.Note: the
/healthpayload reportscurrent_revision: nullin this repo —resolve_head_revisionswallows alembic's multiple-heads error. Pre-existing and identical in local dev, so left alone here.https://claude.ai/code/session_01MKtkDrsfDCwZtXxbPGK3Tv