Skip to content

feat(docker): default app image, standalone by docker run - #281

Merged
antosubash merged 4 commits into
mainfrom
worktree-docker-default-app
Aug 24, 2026
Merged

feat(docker): default app image, standalone by docker run#281
antosubash merged 4 commits into
mainfrom
worktree-docker-default-app

Conversation

@antosubash

@antosubash antosubash commented Aug 21, 2026

Copy link
Copy Markdown
Owner

The repo shipped docker/worker.Dockerfile (Celery) but nothing that ran the app itself — even though every smpy new scaffold gets a Dockerfile. This adds the equivalent for the reference app.

What's here

  • Dockerfile (root — so docker build . and PaaS auto-detection just work). 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-bookworm, non-root uid 10001, HEALTHCHECK on /health.
  • docker/entrypoint.shalembic 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 bare docker run -p 8000:8000 <image> works. Loud warning; set them to keep sessions across restarts.
  • A default admin. Without SM_USERS_BOOTSTRAP_* a fresh container served a login page nobody held credentials for. The entrypoint now seeds admin@example.com / changeme — the same pair .env.example uses for local dev, so the container and make dev behave 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.
  • compose app service + make docker-build / docker-app / docker-compose-app (all honour SM_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 the worker/beat services built from docker/worker.Dockerfile. Drop the flag and set SM_BG_TASKS_BROKER_URL / _RESULT_BACKEND to run tasks from the web process.

Two fixes that were load-bearing

  1. 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 in production, and the existing compose worker/beat silently used localhost instead of the redis hostname they set. Both fields now resolve from env at construction (DB hydration still wins afterwards). Covered by modules/background_tasks/tests/test_bg_settings_env.py. Still needed by worker/beat and by any app that keeps the module.
  2. worker/beat declared a required env_file: .env, which is gitignored — a fresh clone couldn't docker compose anything at all. Now required: false, matching the CLI templates.

Verification (local)

  • docker build → 768MB image; docker run (with zero SM_BG_TASKS_* vars set) and docker compose up app both reach health=healthy with no other services running.
  • Admin seeding: admin@example.com / changeme logs in (204) on a fresh volume and a wrong password is rejected (400); explicit SM_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.ico 404 (no favicon configured).
  • Celery really is gone: /admin/background-tasks/ 404s, the settings module list and admin sidebar don't mention it, and modules.generated.ts has no background_tasks entry.
  • Assets come from the built bundle (/static/dist/..., precompressed), not a Vite dev server; anonymous /dashboard/ still 302s to login.
  • Data survives docker compose restart on the volume; migrations are not re-applied.
  • make ci-python-lint, ci-python-typecheck, ci-check-file-size clean; full uv run pytest = 2150 passed.

Note: the /health payload reports current_revision: null in this repo — resolve_head_revision swallows alembic's multiple-heads error. Pre-existing and identical in local dev, so left alone here.

https://claude.ai/code/session_01MKtkDrsfDCwZtXxbPGK3Tv

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
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 21, 2026

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

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

View logs

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
antosubash marked this pull request as ready for review August 24, 2026 09:20
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@antosubash
antosubash merged commit c00a5b4 into main Aug 24, 2026
13 checks passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant