Conversation
Prod mode has served frontend and backend from one process on a single port for a while, so simple-two-port becomes simple-one-process. The multi-stage images stop running reflex init where the backend never needs it, take uv, bun, and caddy from their official images instead of install scripts and apt, install requirements.txt before the source tree, drop libpq-dev, and keep bun's package cache in a BuildKit cache mount. STOPSIGNAL SIGKILL goes away now that Granian exits cleanly on SIGTERM, Caddy serves the pre-compressed assets Reflex already writes, and the compose stack persists Caddy state under /data and /config. Two runtime problems found while building and running every image: the non-root images could not write to /app because WORKDIR created it as root, and deleting .web let 2*cpu+1 workers race on .web/backend/stateful_pages.json. The images now chown /app and keep .web/backend from the export so workers only read the marker. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Merging this PR will not alter performance
Comparing Footnotes
|
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
After the refresh the two differed only by the multi-stage build, and the multi-stage variant is strictly better. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…se, app-platform-backend The simple image now compiles at build time and touches .web/nocompile on start so reflex reuses that compile; the frontend bundle is still rebuilt on start. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
It rebuilt the frontend bundle on every start and carried node_modules, so it only looked simpler than production while being worse to run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copy only the venv, .web/backend, and the static client out of the builder instead of copying all of /app and pruning .web in place. Drop the uv cache mount and its link-mode companion, the runtime REFLEX_API_URL that only the frontend build uses, the redundant compose image name, and the second stage in app-platform-backend, which now mounts uv for the install step. Azure notes move from the Dockerfile header to the README. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
3 issues found across 31 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docker-example/README.md">
<violation number="1" location="docker-example/README.md:23">
P3: The README states the stack in `compose.yaml` includes redis and a postgres database, but the base `compose.yaml` uses SQLite and defines no redis or postgres services. Those come only from the `compose.prod.yaml` override (`docker compose -f compose.yaml -f compose.prod.yaml up -d`). Update the sentence so it does not attribute redis/postgres to `compose.yaml` alone.</violation>
</file>
<file name="docker-example/production/Dockerfile">
<violation number="1" location="docker-example/production/Dockerfile:53">
P1: When an Alembic migration fails, the semicolon still starts Caddy, Redis, and the backend. Chain startup to the conditional migration so the container stops instead of serving an incompatible schema.</violation>
</file>
<file name="docker-example/app-platform-backend/Dockerfile">
<violation number="1" location="docker-example/app-platform-backend/Dockerfile:27">
P1: On a fresh image with `REFLEX_REDIS_URL`, this command starts the documented multiple backend workers without a `.web/backend` marker. Backend-only compilation can then have workers concurrently create or rewrite `stateful_pages.json`, causing an incomplete marker and startup failures. Provide a prebuilt read-only backend marker before enabling multiple workers, or use the framework's atomic marker-write fix.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| EXPOSE $PORT | ||
|
|
||
| # Apply migrations before starting the backend. | ||
| CMD [ -d alembic ] && reflex db migrate; \ |
There was a problem hiding this comment.
P1: When an Alembic migration fails, the semicolon still starts Caddy, Redis, and the backend. Chain startup to the conditional migration so the container stops instead of serving an incompatible schema.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-example/production/Dockerfile, line 53:
<comment>When an Alembic migration fails, the semicolon still starts Caddy, Redis, and the backend. Chain startup to the conditional migration so the container stops instead of serving an incompatible schema.</comment>
<file context>
@@ -0,0 +1,56 @@
+EXPOSE $PORT
+
+# Apply migrations before starting the backend.
+CMD [ -d alembic ] && reflex db migrate; \
+ caddy start && \
+ redis-server --daemonize yes && \
</file context>
| RUN mkdir -p data uploaded_files | ||
|
|
||
| # Apply migrations before starting the backend. | ||
| CMD [ -d alembic ] && reflex db migrate; \ |
There was a problem hiding this comment.
P1: On a fresh image with REFLEX_REDIS_URL, this command starts the documented multiple backend workers without a .web/backend marker. Backend-only compilation can then have workers concurrently create or rewrite stateful_pages.json, causing an incomplete marker and startup failures. Provide a prebuilt read-only backend marker before enabling multiple workers, or use the framework's atomic marker-write fix.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-example/app-platform-backend/Dockerfile, line 27:
<comment>On a fresh image with `REFLEX_REDIS_URL`, this command starts the documented multiple backend workers without a `.web/backend` marker. Backend-only compilation can then have workers concurrently create or rewrite `stateful_pages.json`, causing an incomplete marker and startup failures. Provide a prebuilt read-only backend marker before enabling multiple workers, or use the framework's atomic marker-write fix.</comment>
<file context>
@@ -0,0 +1,28 @@
+RUN mkdir -p data uploaded_files
+
+# Apply migrations before starting the backend.
+CMD [ -d alembic ] && reflex db migrate; \
+ exec reflex run --env prod --backend-only --backend-port ${PORT:-8000}
</file context>
| including a webserver with automatic TLS, one or more backend instances, redis, | ||
| and a postgres database. |
There was a problem hiding this comment.
P3: The README states the stack in compose.yaml includes redis and a postgres database, but the base compose.yaml uses SQLite and defines no redis or postgres services. Those come only from the compose.prod.yaml override (docker compose -f compose.yaml -f compose.prod.yaml up -d). Update the sentence so it does not attribute redis/postgres to compose.yaml alone.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-example/README.md, line 23:
<comment>The README states the stack in `compose.yaml` includes redis and a postgres database, but the base `compose.yaml` uses SQLite and defines no redis or postgres services. Those come only from the `compose.prod.yaml` override (`docker compose -f compose.yaml -f compose.prod.yaml up -d`). Update the sentence so it does not attribute redis/postgres to `compose.yaml` alone.</comment>
<file context>
@@ -3,27 +3,27 @@
single Reflex app. It provides the entire stack in a single `compose.yaml`
-including a webserver, one or more backend instances, redis, and a postgres
-database.
+including a webserver with automatic TLS, one or more backend instances, redis,
+and a postgres database.
</file context>
| including a webserver with automatic TLS, one or more backend instances, redis, | |
| and a postgres database. | |
| including a webserver with automatic TLS and one or more backend instances. Add | |
| redis and a postgres database by deploying with the `compose.prod.yaml` | |
| override: `docker compose -f compose.yaml -f compose.prod.yaml up -d`. |
Summary
Refresh every example under
docker-example/for current Reflex, consolidate them intoproduction,production-compose, andapp-platform-backend, and update the self-hosting docs page to match.simple-one-portdiffered fromproductiononly by the multi-stage build, andsimple-two-port(a single process that rebuilds the frontend bundle on every start and shipsnode_modules) has no use caseproductiondoes not serve better, so both are gone and the docs point atproductionas the starting point. Follow-up to the earlier analysis of these examples; every image was built with Docker and run against a blankreflex init --template blankapp on Reflex 0.9.11.Stale behavior fixed
production(wasproduction-one-port) lockfile pre-cache was dead code. It copied.web/bun.lockb, which no longer exists (reflex.lock/bun.lockin the project root now). Replaced with a BuildKit cache mount on bun's package cache.app-platform-backend(wasproduction-app-platform) README dropped thegunicorn_worker_class/uvicorn.workersAzure snippet (neither field nor dependency exists; prod runs on Granian) and now documentsREFLEX_API_URL=… reflex export --frontend-only --no-zipinstead of the ignored bareAPI_URLand the removedexport frontendsyntax.production-composeis persisted at/dataand/config, the Caddy v2 locations, instead of the v1/root/.caddy, so certificates survive container recreation.STOPSIGNAL SIGKILLremoved from all images. Granian shuts down cleanly on SIGTERM; measureddocker stoptimes of 0.8–1.1 s with exit code 0.prod.Dockerfile, the unusedredis-ui-settingsvolume, and the docs page's 3000+8000 layout andrequirements.txt-in-docker-exampleclaim.Size, layers, and build behavior
app-platform-backend) no longer runreflex init, so no bun download, nounzip, no.web, nonode_modulescopied into the final image (~200 MB less; final image is 207 MB on the blank app).ghcr.io/astral-sh/uv:0.12(copied into the builder stages, mounted only for the install step in the single-stageapp-platform-backend) and bun fromoven/bun:1viaCOPY --from, replacing theastral.shinstall script and the GitHub bun download. Reflex uses a bun found on PATH as long as it satisfies the minimum version, soreflex init/exportnever download anything but npm packages. Caddy likewise comes fromcaddy:2instead of Debian's 2.6 apt package.requirements.txtis copied and installed before the source tree in every image, so app edits no longer reinstall Python dependencies.UV_COMPILE_BYTECODE=1for faster cold starts on scale-to-zero platforms..web/backend, and the static client out of the builder and take the app source from the build context, so nothing has to prune.webin place and no__pycache__or.statesfrom the build leaks into the image.libpq-devdropped from the slim images; READMEs say to usepsycopg[binary].file_serverusesprecompressed br zstd gzip, serving the.gz(or.br/.zst, perfrontend_compression_formats) files Reflex already writes at export instead of recompressing per request. VerifiedContent-Encoding: gzipwith the pre-compressed size.additional_contexts: app: service:appso the Caddy image build explicitly depends on the app image instead of relying on build order.RUNchains replace the per-command layers;FROM … AScasing fixed;# check=skip=JSONArgsRecommendedsince the shell-formCMDwithexecis intentional..dockerignorefiles now also exclude.git,.venv,__pycache__,.states,*.db, anduploaded_fileseverywhere.GRANIAN_WORKERSis documented for memory-constrained hosts (default is2 * cpu_count + 1when Redis is configured).Two runtime bugs found while testing
/app.WORKDIR /appcreates the directory as root andCOPY --chownonly chowns the contents, so without Redis the disk state manager failed withPermissionError: /app/.states, and with Redis every worker failed creating.web. Both non-root images nowchown reflex /app. This was present in the previous versions too.stateful_pages.json. With.webdeleted from the image and2 * cpu + 1workers, workers race on.web/backend/stateful_pages.json: each writes it with a truncating open while others read it, which intermittently kills a worker withJSONDecodeError. Reproduced on 1 of 2 starts ofproduction. The images that export the frontend now keep.web/backendso all workers take the read-only path (and only evaluate stateful pages at startup). This does not coverapp-platform-backend, which has no build-time compile; the proper fix is an atomic write inApp._write_stateful_pages_marker, which belongs in a separate framework PR.Not changed
productionandproduction-composestill need Caddy because prod--backend-onlyforces the compiled-frontend mount off, so there is no way to serve a pre-built frontend without rebuilding it. A framework change, not a Dockerfile change.try_files … /404.htmlin the Caddyfiles still returns 200 for unknown routes, as before.Test plan
Built all images with Docker Desktop 4.84 against a blank 0.9.11 app (the uv image pulled from the Docker Hub mirror locally because this machine's ghcr login is stale; the committed files reference ghcr). For each:
/pingreturnspong,/serves the app, unknown routes serve404.html,Accept-Encoding: gzipon an asset returnsContent-Encoding: gzipat the pre-compressed size, anddocker stopexits 0 in about 1 s. Compose stack brought up withDOMAIN=localhostand probed over HTTPS. Verified the finalproductionandapp-platform-backendimages contain no bun, uv, ornode_modules, and that the non-root image runs asreflexand can start with and without Redis. Repeated starts of the multi-worker images to confirm the race no longer reproduces.🤖 Generated with Claude Code