Skip to content

Refresh docker-example deployments for current Reflex - #7140

Open
masenf wants to merge 7 commits into
mainfrom
claude/docker-examples-analysis-357cae
Open

masenf wants to merge 7 commits into
mainfrom
claude/docker-examples-analysis-357cae

Conversation

@masenf

@masenf masenf commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Refresh every example under docker-example/ for current Reflex, consolidate them into production, production-compose, and app-platform-backend, and update the self-hosting docs page to match. simple-one-port differed from production only by the multi-stage build, and simple-two-port (a single process that rebuilds the frontend bundle on every start and ships node_modules) has no use case production does not serve better, so both are gone and the docs point at production as the starting point. Follow-up to the earlier analysis of these examples; every image was built with Docker and run against a blank reflex init --template blank app on Reflex 0.9.11.

Stale behavior fixed

  • production (was production-one-port) lockfile pre-cache was dead code. It copied .web/bun.lockb, which no longer exists (reflex.lock/bun.lock in the project root now). Replaced with a BuildKit cache mount on bun's package cache.
  • app-platform-backend (was production-app-platform) README dropped the gunicorn_worker_class / uvicorn.workers Azure snippet (neither field nor dependency exists; prod runs on Granian) and now documents REFLEX_API_URL=… reflex export --frontend-only --no-zip instead of the ignored bare API_URL and the removed export frontend syntax.
  • Caddy TLS state in production-compose is persisted at /data and /config, the Caddy v2 locations, instead of the v1 /root/.caddy, so certificates survive container recreation.
  • STOPSIGNAL SIGKILL removed from all images. Granian shuts down cleanly on SIGTERM; measured docker stop times of 0.8–1.1 s with exit code 0.
  • README rot: prod.Dockerfile, the unused redis-ui-settings volume, and the docs page's 3000+8000 layout and requirements.txt-in-docker-example claim.

Size, layers, and build behavior

  • The backend-only image (app-platform-backend) no longer run reflex init, so no bun download, no unzip, no .web, no node_modules copied into the final image (~200 MB less; final image is 207 MB on the blank app).
  • uv comes from ghcr.io/astral-sh/uv:0.12 (copied into the builder stages, mounted only for the install step in the single-stage app-platform-backend) and bun from oven/bun:1 via COPY --from, replacing the astral.sh install script and the GitHub bun download. Reflex uses a bun found on PATH as long as it satisfies the minimum version, so reflex init/export never download anything but npm packages. Caddy likewise comes from caddy:2 instead of Debian's 2.6 apt package.
  • requirements.txt is copied and installed before the source tree in every image, so app edits no longer reinstall Python dependencies. UV_COMPILE_BYTECODE=1 for faster cold starts on scale-to-zero platforms.
  • The final stages copy only the venv, .web/backend, and the static client out of the builder and take the app source from the build context, so nothing has to prune .web in place and no __pycache__ or .states from the build leaks into the image.
  • libpq-dev dropped from the slim images; READMEs say to use psycopg[binary].
  • Caddy file_server uses precompressed br zstd gzip, serving the .gz (or .br/.zst, per frontend_compression_formats) files Reflex already writes at export instead of recompressing per request. Verified Content-Encoding: gzip with the pre-compressed size.
  • Compose uses additional_contexts: app: service:app so the Caddy image build explicitly depends on the app image instead of relying on build order.
  • Multi-line RUN chains replace the per-command layers; FROM … AS casing fixed; # check=skip=JSONArgsRecommended since the shell-form CMD with exec is intentional.
  • .dockerignore files now also exclude .git, .venv, __pycache__, .states, *.db, and uploaded_files everywhere.
  • GRANIAN_WORKERS is documented for memory-constrained hosts (default is 2 * cpu_count + 1 when Redis is configured).

Two runtime bugs found while testing

  1. Non-root images could not write to /app. WORKDIR /app creates the directory as root and COPY --chown only chowns the contents, so without Redis the disk state manager failed with PermissionError: /app/.states, and with Redis every worker failed creating .web. Both non-root images now chown reflex /app. This was present in the previous versions too.
  2. Multi-worker startup race on stateful_pages.json. With .web deleted from the image and 2 * cpu + 1 workers, workers race on .web/backend/stateful_pages.json: each writes it with a truncating open while others read it, which intermittently kills a worker with JSONDecodeError. Reproduced on 1 of 2 starts of production. The images that export the frontend now keep .web/backend so all workers take the read-only path (and only evaluate stateful pages at startup). This does not cover app-platform-backend, which has no build-time compile; the proper fix is an atomic write in App._write_stateful_pages_marker, which belongs in a separate framework PR.

Not changed

  • production and production-compose still need Caddy because prod --backend-only forces 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.html in 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: /ping returns pong, / serves the app, unknown routes serve 404.html, Accept-Encoding: gzip on an asset returns Content-Encoding: gzip at the pre-compressed size, and docker stop exits 0 in about 1 s. Compose stack brought up with DOMAIN=localhost and probed over HTTPS. Verified the final production and app-platform-backend images contain no bun, uv, or node_modules, and that the non-root image runs as reflex and can start with and without Redis. Repeated starts of the multi-worker images to confirm the race no longer reproduces.

🤖 Generated with Claude Code

masenf and others added 2 commits September 14, 2026 12:19
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>
@greptile-apps

greptile-apps Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with no outstanding blocking or non-blocking code findings.

Summary

Refreshes and consolidates the Docker deployment examples for current Reflex behavior.

  • Replaces obsolete examples with production, production-compose, and app-platform-backend.
  • Modernizes dependency installation, image layering, non-root filesystem ownership, frontend export, and Caddy asset serving.
  • Persists Caddy v2 state correctly and updates self-hosting documentation for current production-mode ports, URLs, and export commands.
  • No new actionable issues were identified in the changes since the previous review.

Reviews (6) · Last reviewed commit: "Simplify the docker examples"

Comment thread docker-example/simple/Dockerfile Outdated
@codspeed-hq

codspeed-hq Bot commented Sep 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 40 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing claude/docker-examples-analysis-357cae (e36ac1d) with main (de63f5d)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread docker-example/production-compose/compose.prod.yaml
masenf and others added 4 commits September 14, 2026 15:43
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>
@masenf
masenf marked this pull request as ready for review September 15, 2026 01:47
@masenf
masenf requested review from a team and Alek99 as code owners September 15, 2026 01:47

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread docker-example/README.md
Comment on lines +23 to +24
including a webserver with automatic TLS, one or more backend instances, redis,
and a postgres database.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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`.

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