From 5d6e961a165de0146644d34d84ac5ffe21fc60cc Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 14 Sep 2026 12:19:46 -0700 Subject: [PATCH 1/8] Refresh docker-example deployments for current Reflex 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 --- docker-example/README.md | 26 +++-- .../production-app-platform/.dockerignore | 8 +- .../production-app-platform/Dockerfile | 49 ++++------ .../production-app-platform/README.md | 46 ++++----- .../production-compose/.dockerignore | 8 +- .../production-compose/Caddy.Dockerfile | 7 +- docker-example/production-compose/Caddyfile | 7 +- docker-example/production-compose/Dockerfile | 59 +++++------ docker-example/production-compose/README.md | 29 ++++-- .../production-compose/compose.prod.yaml | 3 + .../production-compose/compose.tools.yaml | 3 - .../production-compose/compose.yaml | 9 +- .../production-one-port/.dockerignore | 10 +- docker-example/production-one-port/Caddyfile | 7 +- docker-example/production-one-port/Dockerfile | 42 ++++---- docker-example/production-one-port/README.md | 28 ++++-- docker-example/simple-one-port/.dockerignore | 8 +- docker-example/simple-one-port/Caddyfile | 7 +- docker-example/simple-one-port/Dockerfile | 29 +++--- docker-example/simple-one-port/README.md | 25 +++-- .../simple-one-process/.dockerignore | 9 ++ docker-example/simple-one-process/Dockerfile | 31 ++++++ docker-example/simple-one-process/README.md | 54 ++++++++++ docker-example/simple-two-port/.dockerignore | 5 - docker-example/simple-two-port/Dockerfile | 26 ----- docker-example/simple-two-port/README.md | 45 --------- docs/hosting/self-hosting.md | 98 ++++++++++--------- news/+docker-examples-refresh.docs.md | 1 + 28 files changed, 385 insertions(+), 294 deletions(-) create mode 100644 docker-example/simple-one-process/.dockerignore create mode 100644 docker-example/simple-one-process/Dockerfile create mode 100644 docker-example/simple-one-process/README.md delete mode 100644 docker-example/simple-two-port/.dockerignore delete mode 100644 docker-example/simple-two-port/Dockerfile delete mode 100644 docker-example/simple-two-port/README.md create mode 100644 news/+docker-examples-refresh.docs.md diff --git a/docker-example/README.md b/docker-example/README.md index 877fc22bdc1..6fa797d7032 100644 --- a/docker-example/README.md +++ b/docker-example/README.md @@ -3,25 +3,35 @@ This directory contains several examples of how to deploy Reflex apps using docker. In all cases, ensure that your `requirements.txt` file is up to date and -includes the `reflex` package. +includes the `reflex` package. Commit the `reflex.lock/` directory that +`reflex init` creates so the frontend dependencies installed in the image match +the ones you developed against. -## `simple-two-port` +## `simple-one-process` -The most basic production deployment exposes two HTTP ports and relies on an -existing load balancer to forward the traffic appropriately. +The most basic deployment: a single Reflex process serves both the static +frontend and the backend on one port. No reverse proxy, no Redis. The frontend +is rebuilt each time the container starts. ## `simple-one-port` This deployment exports the frontend statically and serves it via a single HTTP -port using Caddy. This is useful for platforms that only support a single port -or where running a node server in the container is undesirable. +port using Caddy, with a local Redis for state. The backend starts instantly +because the frontend is built into the image, but the build tooling stays in +the image. + +## `production-one-port` + +Same layout as `simple-one-port`, built in multiple stages so the final image +contains no bun, `node_modules`, or build tooling, and Python dependencies are +cached in their own layer. ## `production-compose` This deployment is intended for use with a standalone VPS that is only hosting a 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. ## `production-app-platform` diff --git a/docker-example/production-app-platform/.dockerignore b/docker-example/production-app-platform/.dockerignore index ea66a51f527..b5668add4d9 100644 --- a/docker-example/production-app-platform/.dockerignore +++ b/docker-example/production-app-platform/.dockerignore @@ -1,5 +1,9 @@ .web .git -__pycache__/* -Dockerfile +.venv +__pycache__ +*.py[cod] +.states +*.db uploaded_files +Dockerfile diff --git a/docker-example/production-app-platform/Dockerfile b/docker-example/production-app-platform/Dockerfile index 0331861ce01..ef30b77fb57 100644 --- a/docker-example/production-app-platform/Dockerfile +++ b/docker-example/production-app-platform/Dockerfile @@ -1,8 +1,9 @@ +# check=skip=JSONArgsRecommended # This docker file is intended to be used with container hosting services # # After deploying this image, get the URL pointing to the backend service -# and run API_URL=https://path-to-my-container.example.com reflex export frontend -# then copy the contents of `frontend.zip` to your static file server (github pages, s3, etc). +# and run REFLEX_API_URL=https://path-to-my-container.example.com reflex export --frontend-only --no-zip +# then copy the contents of `.web/build/client` to your static file server (github pages, s3, etc). # # Azure Static Web App example: # npx @azure/static-web-apps-cli deploy --env production --app-location .web/build/client @@ -22,44 +23,36 @@ # Note: many container hosting platforms require amd64 images, so when building on an M1 Mac # for example, pass `docker build --platform=linux/amd64 ...` -# Stage 1: init -FROM python:3.13 as init +# Stage 1: build the python environment +FROM python:3.13-slim AS builder -ARG uv=/root/.local/bin/uv +# uv installs python packages much faster than pip. +COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /bin/uv +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy VIRTUAL_ENV=/app/.venv PATH="/app/.venv/bin:$PATH" -# Install `uv` for faster package bootstrapping -ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh -RUN /install.sh && rm /install.sh +WORKDIR /app + +# Install app requirements and reflex inside virtualenv. This layer is only +# rebuilt when requirements.txt changes. +COPY requirements.txt . +RUN --mount=type=cache,target=/root/.cache/uv \ + uv venv && uv pip install -r requirements.txt # Copy local context to `/app` inside container (see .dockerignore) -WORKDIR /app COPY . . RUN mkdir -p /app/data /app/uploaded_files -# Create virtualenv which will be copied into final container -ENV VIRTUAL_ENV=/app/.venv -ENV PATH="$VIRTUAL_ENV/bin:$PATH" -RUN $uv venv - -# Install app requirements and reflex inside virtualenv -RUN $uv pip install -r requirements.txt - -# Deploy templates and prepare app -RUN reflex init - -# Stage 2: copy artifacts into slim image +# Stage 2: copy artifacts into slim image +# The backend does not need bun, node_modules, or .web, so `reflex init` and +# the frontend build are skipped entirely; export the frontend separately. FROM python:3.13-slim WORKDIR /app -RUN adduser --disabled-password --home /app reflex -COPY --chown=reflex --from=init /app /app -# Install libpq-dev for psycopg (skip if not using postgres). -RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/* +# The app user needs to own /app itself so reflex can create .states, data, and uploads there. +RUN adduser --disabled-password --home /app reflex && chown reflex /app +COPY --chown=reflex --from=builder /app /app USER reflex ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 -# Needed until Reflex properly passes SIGTERM on backend. -STOPSIGNAL SIGKILL - # Always apply migrations before starting the backend. CMD [ -d alembic ] && reflex db migrate; \ exec reflex run --env prod --backend-only --backend-port ${PORT:-8000} diff --git a/docker-example/production-app-platform/README.md b/docker-example/production-app-platform/README.md index 6a2f5bed8b4..8c6ce9dc3ab 100644 --- a/docker-example/production-app-platform/README.md +++ b/docker-example/production-app-platform/README.md @@ -8,7 +8,9 @@ Azure, AWS, or Google Cloud Run. The production deployment consists of a few pieces: - Backend container - built by `Dockerfile` Runs the Reflex backend - service on port 8000 and is scalable to multiple instances. + service on port 8000 (or `$PORT`) and is scalable to multiple instances. + The image contains only the python environment and app source: no bun, + `node_modules`, or frontend build. - Redis container - A single instance the standard `redis` docker image should share private networking with the backend - Static frontend - HTML/CSS/JS files that are hosted via a CDN or static file @@ -36,6 +38,12 @@ The backend is built by the `Dockerfile` in this directory. When deploying the backend, be sure to set REFLEX_REDIS_URL=redis://internal-redis-hostname to connect to the redis service. +With redis available, each replica runs `2 * cpu_count + 1` worker processes. +Set `GRANIAN_WORKERS` to cap this on small instance sizes. + +If the app uses postgres, add `psycopg[binary]` to `requirements.txt`; the +binary wheel bundles libpq, so no extra system packages are needed in the image. + ### Ingress Configure the load balancer for the app to forward traffic to port 8000 on the @@ -43,12 +51,22 @@ backend service replicas. Most platforms will generate an ingress hostname automatically. Make sure when you access the ingress endpoint on `/ping` that it returns "pong", indicating that the backend is up an available. +The load balancer must support websockets (pass the `Upgrade` header) for the +event connection to work. + ### Frontend The frontend should be hosted on a static file server or CDN. -**Important**: when exporting the frontend, set the API_URL environment variable -to the ingress hostname of the backend service. +**Important**: when exporting the frontend, set the `REFLEX_API_URL` environment +variable to the ingress hostname of the backend service. + +```bash +REFLEX_API_URL=https://backend.example.com reflex export --frontend-only --no-zip +``` + +The exported files are in `.web/build/client`. Omit `--no-zip` to get a +`frontend.zip` instead. If you will host the frontend from a path other than the root, set the `REFLEX_FRONTEND_PATH` environment variable appropriately when exporting the frontend. @@ -67,28 +85,6 @@ The following sections are currently a work in progress and may be incomplete. ### Azure -In the Azure load balancer, per-message deflate is not supported. Add the following -to your `rxconfig.py` to workaround this issue. - -```python -import uvicorn.workers - -import reflex as rx - - -class NoWSPerMessageDeflate(uvicorn.workers.UvicornH11Worker): - CONFIG_KWARGS = { - **uvicorn.workers.UvicornH11Worker.CONFIG_KWARGS, - "ws_per_message_deflate": False, - } - - -config = rx.Config( - app_name="my_app", - gunicorn_worker_class="rxconfig.NoWSPerMessageDeflate", -) -``` - #### Persistent Storage If you need to use a database or upload files, you cannot save them to the diff --git a/docker-example/production-compose/.dockerignore b/docker-example/production-compose/.dockerignore index 2d2447b1941..4fa4361ff68 100644 --- a/docker-example/production-compose/.dockerignore +++ b/docker-example/production-compose/.dockerignore @@ -1,8 +1,12 @@ .web .git -__pycache__/* +.venv +__pycache__ +*.py[cod] +.states +*.db +uploaded_files Dockerfile Caddy.Dockerfile compose.yaml compose.*.yaml -uploaded_files diff --git a/docker-example/production-compose/Caddy.Dockerfile b/docker-example/production-compose/Caddy.Dockerfile index 76ae32079bc..fa132aae2ce 100644 --- a/docker-example/production-compose/Caddy.Dockerfile +++ b/docker-example/production-compose/Caddy.Dockerfile @@ -1,4 +1,5 @@ -FROM library/caddy +FROM caddy:2 -COPY --from=local/reflex-app /app/.web/build/client /srv -ADD Caddyfile /etc/caddy/Caddyfile \ No newline at end of file +# The `app` build context is the app service image (see compose.yaml). +COPY --from=app /app/.web/build/client /srv +COPY Caddyfile /etc/caddy/Caddyfile diff --git a/docker-example/production-compose/Caddyfile b/docker-example/production-compose/Caddyfile index 2d24868c64b..6f71f453175 100644 --- a/docker-example/production-compose/Caddyfile +++ b/docker-example/production-compose/Caddyfile @@ -2,7 +2,7 @@ encode gzip -@backend_routes path /_event/* /ping /_upload /_upload/* +@backend_routes path /_event/* /ping /_health /_upload /_upload/* handle @backend_routes { reverse_proxy app:8000 } @@ -10,5 +10,8 @@ handle @backend_routes { root * /srv route { try_files {path} {path}/ /404.html - file_server + # Reflex writes a pre-compressed copy of every asset at export time. + file_server { + precompressed br zstd gzip + } } diff --git a/docker-example/production-compose/Dockerfile b/docker-example/production-compose/Dockerfile index 59e73ed2304..9f95ae1099a 100644 --- a/docker-example/production-compose/Dockerfile +++ b/docker-example/production-compose/Dockerfile @@ -1,52 +1,45 @@ +# check=skip=JSONArgsRecommended # This docker file is intended to be used with docker compose to deploy a production # instance of a Reflex app. -# Stage 1: init -FROM python:3.13 as init +# Stage 1: build the python environment and export the static frontend +FROM python:3.13-slim AS builder -ARG uv=/root/.local/bin/uv +# uv installs python packages; reflex uses a bun found on PATH instead of downloading its own. +COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /bin/uv +COPY --from=oven/bun:1 /usr/local/bin/bun /usr/local/bin/bun +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy VIRTUAL_ENV=/app/.venv PATH="/app/.venv/bin:$PATH" -# Install `uv` for faster package bootstrapping -ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh -RUN /install.sh && rm /install.sh +WORKDIR /app + +# Install app requirements and reflex inside virtualenv. This layer is only +# rebuilt when requirements.txt changes. +COPY requirements.txt . +RUN --mount=type=cache,target=/root/.cache/uv \ + uv venv && uv pip install -r requirements.txt # Copy local context to `/app` inside container (see .dockerignore) -WORKDIR /app COPY . . RUN mkdir -p /app/data /app/uploaded_files -# Create virtualenv which will be copied into final container -ENV VIRTUAL_ENV=/app/.venv -ENV PATH="$VIRTUAL_ENV/bin:$PATH" -RUN $uv venv - -# Install app requirements and reflex inside virtualenv -RUN $uv pip install -r requirements.txt +# Export static copy of frontend to /app/.web/build/client, then drop the rest +# of .web (node_modules etc.) to save space in the backend image. The cache +# mount keeps bun's package cache between builds. Keep .web/backend so the backend only +# evaluates stateful pages at startup instead of every page. +RUN --mount=type=cache,target=/root/.bun/install/cache \ + reflex export --frontend-only --no-zip \ + && mv .web/build/client .web/backend /tmp/ && rm -rf .web \ + && mkdir -p .web/build && mv /tmp/client .web/build/client && mv /tmp/backend .web/backend -# Deploy templates and prepare app -RUN reflex init - -# Export static copy of frontend to /app/.web/build/client -RUN reflex export --frontend-only --no-zip - -# Copy static files out of /app to save space in backend image -RUN mv .web/build/client /tmp/client -RUN rm -rf .web && mkdir -p .web/build -RUN mv /tmp/client .web/build/client - -# Stage 2: copy artifacts into slim image +# Stage 2: copy artifacts into slim image FROM python:3.13-slim WORKDIR /app -RUN adduser --disabled-password --home /app reflex -COPY --chown=reflex --from=init /app /app -# Install libpq-dev for psycopg (skip if not using postgres). -RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/* +# The app user needs to own /app itself so reflex can create .states, data, and uploads there. +RUN adduser --disabled-password --home /app reflex && chown reflex /app +COPY --chown=reflex --from=builder /app /app USER reflex ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 -# Needed until Reflex properly passes SIGTERM on backend. -STOPSIGNAL SIGKILL - # Always apply migrations before starting the backend. CMD [ -d alembic ] && reflex db migrate; \ exec reflex run --env prod --backend-only diff --git a/docker-example/production-compose/README.md b/docker-example/production-compose/README.md index 19efa95fd3e..bcb88be4789 100644 --- a/docker-example/production-compose/README.md +++ b/docker-example/production-compose/README.md @@ -1,21 +1,22 @@ # production-compose This example production deployment uses automatic TLS with Caddy serving static -files for the frontend and proxying requests to both the frontend and backend. +files for the frontend and proxying requests to the backend. It is intended for use with a standalone VPS that is only hosting a single Reflex app. The production app container (`Dockerfile`), builds and exports the frontend statically (to be served by Caddy). The resulting image only runs the backend -service. +service and contains no bun, `node_modules`, or build tooling. The `webserver` service, based on `Caddy.Dockerfile`, copies the static frontend -and `Caddyfile` into the container to configure the reverse proxy routes that will -forward requests to the backend service. Caddy will automatically provision TLS -for localhost or the domain specified in the environment variable `DOMAIN`. +out of the app image and `Caddyfile` into the container to configure the +reverse proxy routes that will forward requests to the backend service. Caddy +will automatically provision TLS for localhost or the domain specified in the +environment variable `DOMAIN`. This type of deployment should use less memory and be more performant since -nodejs is not required at runtime. +neither bun nor nodejs is required at runtime. ## Customize `Caddyfile` (optional) @@ -33,8 +34,9 @@ be hosted! (Do not include http or https, it will always use https). DOMAIN=example.com docker compose build ``` -This will build both the `app` service from the `prod.Dockerfile` and the `webserver` -service via `Caddy.Dockerfile`. +This will build both the `app` service from the `Dockerfile` and the `webserver` +service via `Caddy.Dockerfile`. The `webserver` build copies the exported +frontend out of the `app` image, so compose always builds `app` first. ## Run Reflex Production Service @@ -49,7 +51,8 @@ provisioning will occur automatically and may take a few minutes. Named docker volumes are used to persist the app database (`db-data`), uploaded_files (`upload-data`), and caddy TLS keys and certificates -(`caddy-data`). +(`caddy-data`). Keep `caddy-data` across container recreations so certificates +are not re-issued each time, which can hit Let's Encrypt rate limits. ## More Robust Deployment @@ -61,6 +64,14 @@ the backend to run with multiple workers and service more requests. DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml up -d ``` +Add `psycopg[binary]` to `requirements.txt` so the backend can connect to +postgres; the binary wheel bundles libpq, so no extra system packages are +needed in the image. + +With redis available, the backend runs `2 * cpu_count + 1` worker processes. +Set `GRANIAN_WORKERS` in the `app` environment to cap this on memory +constrained hosts. + Postgres uses its own named docker volume for data persistence. ## Admin Tools diff --git a/docker-example/production-compose/compose.prod.yaml b/docker-example/production-compose/compose.prod.yaml index 408ed7ec351..eeb856a5fba 100644 --- a/docker-example/production-compose/compose.prod.yaml +++ b/docker-example/production-compose/compose.prod.yaml @@ -17,6 +17,9 @@ services: environment: REFLEX_DB_URL: postgresql+psycopg://postgres:secret@db/postgres REFLEX_REDIS_URL: redis://redis:6379 + # With redis available, the backend runs `2 * cpu_count + 1` workers. + # Uncomment to cap the worker count on memory constrained hosts. + # GRANIAN_WORKERS: 2 depends_on: - db - redis diff --git a/docker-example/production-compose/compose.tools.yaml b/docker-example/production-compose/compose.tools.yaml index 0bd1ab990ad..1184f6a0bde 100644 --- a/docker-example/production-compose/compose.tools.yaml +++ b/docker-example/production-compose/compose.tools.yaml @@ -13,6 +13,3 @@ services: - REDIS_HOSTS=local:redis:6379 ports: - "8081:8081" - -volumes: - redis-ui-settings: diff --git a/docker-example/production-compose/compose.yaml b/docker-example/production-compose/compose.yaml index e0b8711a75a..e08af8c1f38 100644 --- a/docker-example/production-compose/compose.yaml +++ b/docker-example/production-compose/compose.yaml @@ -26,8 +26,13 @@ services: build: context: . dockerfile: Caddy.Dockerfile + # Build the app image first and expose it to Caddy.Dockerfile as `app` + # so the static frontend can be copied out of it. + additional_contexts: + app: service:app volumes: - - caddy-data:/root/.caddy + - caddy-data:/data + - caddy-config:/config restart: always depends_on: - app @@ -39,3 +44,5 @@ volumes: upload-data: # TLS keys and certificates caddy-data: + # Caddy's copy of its own config + caddy-config: diff --git a/docker-example/production-one-port/.dockerignore b/docker-example/production-one-port/.dockerignore index 26ae41b8367..b5668add4d9 100644 --- a/docker-example/production-one-port/.dockerignore +++ b/docker-example/production-one-port/.dockerignore @@ -1,3 +1,9 @@ .web -!.web/bun.lockb -!.web/package.json +.git +.venv +__pycache__ +*.py[cod] +.states +*.db +uploaded_files +Dockerfile diff --git a/docker-example/production-one-port/Caddyfile b/docker-example/production-one-port/Caddyfile index 28fb0186165..5891d546eea 100644 --- a/docker-example/production-one-port/Caddyfile +++ b/docker-example/production-one-port/Caddyfile @@ -2,7 +2,7 @@ encode gzip -@backend_routes path /_event/* /ping /_upload /_upload/* +@backend_routes path /_event/* /ping /_health /_upload /_upload/* handle @backend_routes { reverse_proxy localhost:8000 } @@ -10,5 +10,8 @@ handle @backend_routes { root * /srv route { try_files {path} {path}/ /404.html - file_server + # Reflex writes a pre-compressed copy of every asset at export time. + file_server { + precompressed br zstd gzip + } } diff --git a/docker-example/production-one-port/Dockerfile b/docker-example/production-one-port/Dockerfile index f7d83b7ede0..0b2fa1b3838 100644 --- a/docker-example/production-one-port/Dockerfile +++ b/docker-example/production-one-port/Dockerfile @@ -1,3 +1,4 @@ +# check=skip=JSONArgsRecommended # This Dockerfile is used to deploy a single-container Reflex app instance # to services like Render, Railway, Heroku, GCP, and others. @@ -9,39 +10,41 @@ ARG API_URL # It uses a reverse proxy to serve the frontend statically and proxy to backend # from a single exposed port, expecting TLS termination to be handled at the # edge by the given platform. -FROM python:3.13 as builder +FROM python:3.13-slim AS builder -RUN mkdir -p /app/.web -RUN python -m venv /app/.venv -ENV PATH="/app/.venv/bin:$PATH" +# uv installs python packages; reflex uses a bun found on PATH instead of downloading its own. +COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /bin/uv +COPY --from=oven/bun:1 /usr/local/bin/bun /usr/local/bin/bun +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy VIRTUAL_ENV=/app/.venv PATH="/app/.venv/bin:$PATH" WORKDIR /app -# Install python app requirements and reflex in the container +# Install python app requirements and reflex in the container. This layer is +# only rebuilt when requirements.txt changes. COPY requirements.txt . -RUN pip install -r requirements.txt - -# Install reflex helper utilities like bun/node -COPY rxconfig.py ./ -RUN reflex init - -# Install pre-cached frontend dependencies (if exist) -COPY *.web/bun.lockb *.web/package.json .web/ -RUN if [ -f .web/bun.lockb ]; then cd .web && ~/.local/share/reflex/bun/bin/bun install --frozen-lockfile; fi +RUN --mount=type=cache,target=/root/.cache/uv \ + uv venv && uv pip install -r requirements.txt # Copy local context to `/app` inside container (see .dockerignore) COPY . . ARG PORT API_URL -# Download other npm dependencies and compile frontend -RUN REFLEX_API_URL=${API_URL:-http://localhost:$PORT} reflex export --loglevel debug --frontend-only --no-zip && mv .web/build/client/* /srv/ && rm -rf .web +# Download npm dependencies and compile frontend into /srv. The cache mount +# keeps bun's package cache between builds so unchanged dependencies are not +# downloaded again. Keep .web/backend so the backend only +# evaluates stateful pages at startup instead of every page. +RUN --mount=type=cache,target=/root/.bun/install/cache \ + REFLEX_API_URL=${API_URL:-http://localhost:$PORT} reflex export --frontend-only --no-zip \ + && cp -r .web/build/client/. /srv/ \ + && mv .web/backend /tmp/backend && rm -rf .web && mkdir .web && mv /tmp/backend .web/backend # Final image with only necessary files FROM python:3.13-slim -# Install Caddy and redis server inside image -RUN apt-get update -y && apt-get install -y caddy redis-server && rm -rf /var/lib/apt/lists/* +# Install redis server inside image; take caddy from its official image. +RUN apt-get update -y && apt-get install -y --no-install-recommends redis-server && rm -rf /var/lib/apt/lists/* +COPY --from=caddy:2 /usr/bin/caddy /usr/bin/caddy ARG PORT API_URL ENV PATH="/app/.venv/bin:$PATH" PORT=$PORT REFLEX_API_URL=${API_URL:-http://localhost:$PORT} REFLEX_REDIS_URL=redis://localhost PYTHONUNBUFFERED=1 @@ -50,9 +53,6 @@ WORKDIR /app COPY --from=builder /app /app COPY --from=builder /srv /srv -# Needed until Reflex properly passes SIGTERM on backend. -STOPSIGNAL SIGKILL - EXPOSE $PORT # Apply migrations before starting the backend. diff --git a/docker-example/production-one-port/README.md b/docker-example/production-one-port/README.md index 06d9ba079dd..8a1af4837fc 100644 --- a/docker-example/production-one-port/README.md +++ b/docker-example/production-one-port/README.md @@ -4,16 +4,20 @@ This docker deployment runs Reflex in prod mode, exposing a single HTTP port: - `8080` (`$PORT`) - Caddy server hosting the frontend statically and proxying requests to the backend. -The deployment also runs a local Redis server to store state for each user. +The deployment also runs a local Redis server to store state for each user, +which lets the backend run multiple worker processes. Conceptually it is similar to the `simple-one-port` example except it: -- has layer caching for python, reflex, and node dependencies -- uses multi-stage build to reduce the size of the final image +- installs python dependencies in their own layer, so app edits do not + reinstall them +- keeps bun's package cache between builds, so unchanged frontend + dependencies are not downloaded again +- uses a multi-stage build so the final image has no bun, `node_modules`, or + build tooling -Using this method may be preferable for deploying in memory constrained -environments, because it serves a static frontend export, rather than running -the Vite server via node. +The frontend is exported at build time and served as static files by Caddy, +so the backend starts in a couple of seconds. ## Build @@ -21,12 +25,21 @@ the Vite server via node. docker build -t reflex-production-one-port . ``` +To listen on a different port, pass `--build-arg PORT=10000`. + ## Run ```console docker run -p 8080:8080 reflex-production-one-port ``` +By default the backend runs `2 * cpu_count + 1` workers. Set `GRANIAN_WORKERS` +to a smaller number in memory constrained environments: + +```console +docker run -e GRANIAN_WORKERS=2 -p 8080:8080 reflex-production-one-port +``` + Note that this container has _no persistence_ and will lose all data when stopped. You can use bind mounts or named volumes to persist the database and uploaded_files directories as needed. @@ -37,3 +50,6 @@ This container should be used with an existing load balancer or reverse proxy to terminate TLS. It is also useful for deploying to simple app platforms, such as Render or Heroku. + +If the app defines additional backend API routes, add them to the +`@backend_routes` matcher in the `Caddyfile` so they are forwarded to the backend. diff --git a/docker-example/simple-one-port/.dockerignore b/docker-example/simple-one-port/.dockerignore index ea66a51f527..b5668add4d9 100644 --- a/docker-example/simple-one-port/.dockerignore +++ b/docker-example/simple-one-port/.dockerignore @@ -1,5 +1,9 @@ .web .git -__pycache__/* -Dockerfile +.venv +__pycache__ +*.py[cod] +.states +*.db uploaded_files +Dockerfile diff --git a/docker-example/simple-one-port/Caddyfile b/docker-example/simple-one-port/Caddyfile index 28fb0186165..5891d546eea 100644 --- a/docker-example/simple-one-port/Caddyfile +++ b/docker-example/simple-one-port/Caddyfile @@ -2,7 +2,7 @@ encode gzip -@backend_routes path /_event/* /ping /_upload /_upload/* +@backend_routes path /_event/* /ping /_health /_upload /_upload/* handle @backend_routes { reverse_proxy localhost:8000 } @@ -10,5 +10,8 @@ handle @backend_routes { root * /srv route { try_files {path} {path}/ /404.html - file_server + # Reflex writes a pre-compressed copy of every asset at export time. + file_server { + precompressed br zstd gzip + } } diff --git a/docker-example/simple-one-port/Dockerfile b/docker-example/simple-one-port/Dockerfile index 0244d5bdb00..29a2e34cd35 100644 --- a/docker-example/simple-one-port/Dockerfile +++ b/docker-example/simple-one-port/Dockerfile @@ -1,10 +1,11 @@ +# check=skip=JSONArgsRecommended # This Dockerfile is used to deploy a single-container Reflex app instance # to services like Render, Railway, Heroku, GCP, and others. # It uses a reverse proxy to serve the frontend statically and proxy to backend # from a single exposed port, expecting TLS termination to be handled at the # edge by the given platform. -FROM python:3.13 +FROM python:3.13-slim # If the service expects a different port, provide it here (f.e Render expects port 10000) ARG PORT=8080 @@ -12,25 +13,25 @@ ARG PORT=8080 ARG API_URL ENV PORT=$PORT REFLEX_API_URL=${API_URL:-http://localhost:$PORT} REFLEX_REDIS_URL=redis://localhost PYTHONUNBUFFERED=1 -# Install Caddy and redis server inside image -RUN apt-get update -y && apt-get install -y caddy redis-server && rm -rf /var/lib/apt/lists/* +# Install redis server inside image; take caddy and bun from their official images. +RUN apt-get update -y && apt-get install -y --no-install-recommends redis-server && rm -rf /var/lib/apt/lists/* +COPY --from=caddy:2 /usr/bin/caddy /usr/bin/caddy +COPY --from=oven/bun:1 /usr/local/bin/bun /usr/local/bin/bun WORKDIR /app +# Install python requirements first so app edits do not reinstall them. +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + # Copy local context to `/app` inside container (see .dockerignore) COPY . . -# Install app requirements and reflex in the container -RUN pip install -r requirements.txt - -# Deploy templates and prepare app -RUN reflex init - -# Download all npm dependencies and compile frontend -RUN reflex export --frontend-only --no-zip && mv .web/build/client/* /srv/ && rm -rf .web - -# Needed until Reflex properly passes SIGTERM on backend. -STOPSIGNAL SIGKILL +# Download all npm dependencies and compile frontend into /srv for caddy. Keep .web/backend so the backend only +# evaluates stateful pages at startup instead of every page. +RUN reflex export --frontend-only --no-zip \ + && cp -r .web/build/client/. /srv/ \ + && mv .web/backend /tmp/backend && rm -rf .web && mkdir .web && mv /tmp/backend .web/backend EXPOSE $PORT diff --git a/docker-example/simple-one-port/README.md b/docker-example/simple-one-port/README.md index b249249babc..5bfb65643b6 100644 --- a/docker-example/simple-one-port/README.md +++ b/docker-example/simple-one-port/README.md @@ -4,14 +4,13 @@ This docker deployment runs Reflex in prod mode, exposing a single HTTP port: - `8080` (`$PORT`) - Caddy server hosting the frontend statically and proxying requests to the backend. -The deployment also runs a local Redis server to store state for each user. +The deployment also runs a local Redis server to store state for each user, +which lets the backend run multiple worker processes. -Using this method may be preferable for deploying in memory constrained -environments, because it serves a static frontend export, rather than running -the Vite server via node. - -For platforms which only terminate TLS to a single port, this container can be -deployed instead of the `simple-two-port` example. +The frontend is exported at build time and served as static files by Caddy, +so the backend starts in a couple of seconds and bun is not needed at runtime. +The build tooling stays in the image, though; see `production-one-port` for a +multi-stage build that leaves it behind. ## Build @@ -19,12 +18,21 @@ deployed instead of the `simple-two-port` example. docker build -t reflex-simple-one-port . ``` +To listen on a different port, pass `--build-arg PORT=10000`. + ## Run ```console docker run -p 8080:8080 reflex-simple-one-port ``` +By default the backend runs `2 * cpu_count + 1` workers. Set `GRANIAN_WORKERS` +to a smaller number in memory constrained environments: + +```console +docker run -e GRANIAN_WORKERS=2 -p 8080:8080 reflex-simple-one-port +``` + Note that this container has _no persistence_ and will lose all data when stopped. You can use bind mounts or named volumes to persist the database and uploaded_files directories as needed. @@ -35,3 +43,6 @@ This container should be used with an existing load balancer or reverse proxy to terminate TLS. It is also useful for deploying to simple app platforms, such as Render or Heroku. + +If the app defines additional backend API routes, add them to the +`@backend_routes` matcher in the `Caddyfile` so they are forwarded to the backend. diff --git a/docker-example/simple-one-process/.dockerignore b/docker-example/simple-one-process/.dockerignore new file mode 100644 index 00000000000..b5668add4d9 --- /dev/null +++ b/docker-example/simple-one-process/.dockerignore @@ -0,0 +1,9 @@ +.web +.git +.venv +__pycache__ +*.py[cod] +.states +*.db +uploaded_files +Dockerfile diff --git a/docker-example/simple-one-process/Dockerfile b/docker-example/simple-one-process/Dockerfile new file mode 100644 index 00000000000..6d03ec26fa2 --- /dev/null +++ b/docker-example/simple-one-process/Dockerfile @@ -0,0 +1,31 @@ +# check=skip=JSONArgsRecommended +# The simplest single-container deployment: one Reflex process serves the +# static frontend and the backend on the same port. No reverse proxy, no Redis. +# +# The frontend is compiled and built each time the container starts, so startup +# takes longer than the other examples and bun stays in the image. For an image +# with a prebuilt frontend see `simple-one-port` or `production-one-port`. +FROM python:3.13-slim + +# Reflex uses a bun found on PATH instead of downloading its own. +COPY --from=oven/bun:1 /usr/local/bin/bun /usr/local/bin/bun + +ENV PYTHONUNBUFFERED=1 +WORKDIR /app + +# Install python requirements first so app edits do not reinstall them. +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy local context to `/app` inside container (see .dockerignore) +COPY . . + +# Install frontend dependencies at build time so the container does not need +# network access to start. The build output itself is regenerated on startup. +RUN reflex export --frontend-only --no-zip && rm -rf .web/build + +EXPOSE 3000 + +# Apply migrations before starting the backend. +CMD [ -d alembic ] && reflex db migrate; \ + exec reflex run --env prod --frontend-port ${PORT:-3000} diff --git a/docker-example/simple-one-process/README.md b/docker-example/simple-one-process/README.md new file mode 100644 index 00000000000..16edf068c7c --- /dev/null +++ b/docker-example/simple-one-process/README.md @@ -0,0 +1,54 @@ +# simple-one-process + +This docker deployment runs Reflex in prod mode with a single process exposing +a single HTTP port: + +- `3000` (`$PORT`) - Reflex serves the static frontend and the backend + (event websocket, `/ping`, `/_upload`). + +No reverse proxy or Redis is involved, so the backend runs a single worker +with in-memory state. The frontend is compiled and built when the container +starts, which adds some time before the app is reachable. + +## Build + +```console +docker build -t reflex-simple-one-process . +``` + +## Run + +```console +docker run -p 3000:3000 reflex-simple-one-process +``` + +Map the same port on both sides. The frontend connects to the backend using +the hostname it was loaded from and the port the app listens on, so +`-p 8080:3000` will not work over plain HTTP unless `REFLEX_API_URL` is set +to the externally visible address. Behind a TLS-terminating proxy the port is +dropped and the frontend connects to its own origin. + +To listen on a different port set `PORT`: + +```console +docker run -e PORT=8080 -p 8080:8080 reflex-simple-one-process +``` + +Note that this container has _no persistence_ and will lose all data when +stopped. You can use bind mounts or named volumes to persist the database and +uploaded_files directories as needed. + +## Usage + +This container should be used with an existing load balancer or reverse proxy to +terminate TLS. The proxy must pass the `Upgrade` header so the event websocket +can connect. + +For example, the following Caddyfile terminates TLS and forwards all traffic +to the container. + +``` +my-domain.com + +reverse_proxy localhost:3000 +``` diff --git a/docker-example/simple-two-port/.dockerignore b/docker-example/simple-two-port/.dockerignore deleted file mode 100644 index ea66a51f527..00000000000 --- a/docker-example/simple-two-port/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -.web -.git -__pycache__/* -Dockerfile -uploaded_files diff --git a/docker-example/simple-two-port/Dockerfile b/docker-example/simple-two-port/Dockerfile deleted file mode 100644 index 2e5a3b57fa6..00000000000 --- a/docker-example/simple-two-port/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# This Dockerfile is used to deploy a simple single-container Reflex app instance. -FROM python:3.13 - -RUN apt-get update && apt-get install -y redis-server && rm -rf /var/lib/apt/lists/* -ENV REFLEX_REDIS_URL=redis://localhost PYTHONUNBUFFERED=1 - -# Copy local context to `/app` inside container (see .dockerignore) -WORKDIR /app -COPY . . - -# Install app requirements and reflex in the container -RUN pip install -r requirements.txt - -# Deploy templates and prepare app -RUN reflex init - -# Download all npm dependencies and compile frontend -RUN reflex export --frontend-only --no-zip - -# Needed until Reflex properly passes SIGTERM on backend. -STOPSIGNAL SIGKILL - -# Always apply migrations before starting the backend. -CMD [ -d alembic ] && reflex db migrate; \ - redis-server --daemonize yes && \ - exec reflex run --env prod diff --git a/docker-example/simple-two-port/README.md b/docker-example/simple-two-port/README.md deleted file mode 100644 index f3a7dc37624..00000000000 --- a/docker-example/simple-two-port/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# simple-two-port - -This docker deployment runs Reflex in prod mode, exposing two HTTP ports: - -- `3000` - node server using optimized production build -- `8000` - python gunicorn server hosting the Reflex backend - -The deployment also runs a local Redis server to store state for each user. - -## Build - -```console -docker build -t reflex-simple-two-port . -``` - -## Run - -```console -docker run -p 3000:3000 -p 8000:8000 reflex-simple-two-port -``` - -Note that this container has _no persistence_ and will lose all data when -stopped. You can use bind mounts or named volumes to persist the database and -uploaded_files directories as needed. - -## Usage - -This container should be used with an existing load balancer or reverse proxy to -route traffic to the appropriate port inside the container. - -For example, the following Caddyfile can be used to terminate TLS and forward -traffic to the frontend and backend from outside the container. - -``` -my-domain.com - -encode gzip - -@backend_routes path /_event/* /ping /_upload /_upload/* -handle @backend_routes { - reverse_proxy localhost:8000 -} - -reverse_proxy localhost:3000 -``` diff --git a/docs/hosting/self-hosting.md b/docs/hosting/self-hosting.md index 4245bdf78d1..3f7f9e6b518 100644 --- a/docs/hosting/self-hosting.md +++ b/docs/hosting/self-hosting.md @@ -8,23 +8,53 @@ Use `reflex deploy` for the managed workflow. Follow this page when you need to Clone your code to a server and install the [requirements](/docs/getting-started/installation/). +## Production Mode + +Run your app in production mode: + +```bash +reflex run --env prod +``` + +Production mode compiles the app, builds an optimized static frontend, and +serves it together with the backend (event websocket, `/ping`, `/_upload`) +from a single process on port `3000`. Pass `--frontend-port` or +`--backend-port` to listen on a different port. + +The frontend and backend can also run as separate processes, for example to +serve the frontend from a CDN and scale the backend independently: + +```bash +reflex run --env prod --backend-only --backend-port 8000 +reflex run --env prod --frontend-only --frontend-port 3000 +``` + +```md alert warning +# Reverse Proxy and Websockets +Because the backend uses websockets, some reverse proxy servers, like [nginx](https://nginx.org/en/docs/http/websocket.html) or [apache](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#protoupgrade), must be configured to pass the `Upgrade` header to allow backend connectivity. +``` + ## API URL -Edit your `rxconfig.py` file and set `api_url` to the publicly accessible IP -address or hostname of your server, with the port `:8000` at the end. Setting -this correctly is essential for the frontend to interact with the backend state. +The frontend connects to the backend at `api_url`. When `api_url` points at +`localhost` (the default), the frontend substitutes the hostname it was loaded +from, and when the page is served over HTTPS it also drops the port and +connects to its own origin. So a single-port production deployment behind a +TLS-terminating proxy needs no `api_url` configuration at all. -For example, if your server is at `app.example.com`, use: +Set `api_url` explicitly when the backend is reachable at a different address +than the frontend, for example when the frontend is exported to a static host +and the backend runs elsewhere: ```python config = rx.Config( app_name="your_app_name", - api_url="http://app.example.com:8000", + api_url="https://api.example.com", ) ``` -It is also possible to set the environment variable `API_URL` at run time or -export time to retain the default for local development. +It is also possible to set the environment variable `REFLEX_API_URL` at run +time or export time to retain the default for local development. ## Proxying to a Subpath @@ -50,23 +80,6 @@ Note: changing `backend_path` (or `frontend_path`) requires a full restart of `reflex run` — routes and mount points are registered at startup, so hot reload alone will not move them. -## Production Mode - -Then run your app in production mode: - -```bash -reflex run --env prod -``` - -Production mode creates an optimized build of your app. By default, the static -frontend of the app (HTML, Javascript, CSS) will be exposed on port `3000` and -the backend (event handlers) will be listening on port `8000`. - -```md alert warning -# Reverse Proxy and Websockets -Because the backend uses websockets, some reverse proxy servers, like [nginx](https://nginx.org/en/docs/http/websocket.html) or [apache](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#protoupgrade), must be configured to pass the `Upgrade` header to allow backend connectivity. -``` - ## Exporting a Static Build Exporting a static build of the frontend allows the app to be served using a @@ -74,7 +87,7 @@ static hosting provider, such as Netlify or GitHub Pages. Make sure `api_url` is to an accessible backend URL when the frontend is exported. ```bash -API_URL=http://app.example.com:8000 reflex export +REFLEX_API_URL=https://api.example.com reflex export ``` This will create a `frontend.zip` file with your app's minified HTML, @@ -91,38 +104,31 @@ this, use the `--no-zip` parameter. This provides the frontend in the `.web/build/client/` directory and the backend can be found in the root directory of the project. -## Reflex Container Service - -Another option is to run your Reflex service in a container. For this -purpose, a `Dockerfile` and additional documentation is available in the Reflex -project in the directory `docker-example`. +The export also writes a pre-compressed `.gz` copy of every frontend asset, so +configure the static host to serve those directly where it supports it. -Before building the image, update `rxconfig.py` and add `requirements.txt` to -the project folder: - -```python -config = rx.Config( - app_name="app", - api_url="http://app.example.com:8000", -) -``` +## Reflex Container Service -Notice that the `api_url` should be set to the externally accessible hostname or -IP, as the client browser must be able to connect to it directly to establish -interactivity. +Another option is to run your Reflex service in a container. Several +`Dockerfile`s with additional documentation are available in the Reflex +project in the directory +[`docker-example`](https://github.com/reflex-dev/reflex/tree/main/docker-example), +ranging from a single process serving everything on one port to a full +compose stack with a TLS-terminating webserver, redis, and postgres. -You can find the `requirements.txt` in the `docker-example` folder of the -project too. +Before building the image, add a `requirements.txt` to the project folder +that includes `reflex` and commit the `reflex.lock/` directory so the frontend +dependencies installed in the image match the ones you developed against. The project structure should look like this: ```bash hello -├── .web ├── assets ├── hello │ ├── __init__.py │ └── hello.py +├── reflex.lock ├── rxconfig.py ├── Dockerfile └── requirements.txt @@ -137,5 +143,5 @@ docker build -t reflex-project:latest . Finally, you can start your Reflex container service as follows. ```bash -docker run -d -p 3000:3000 -p 8000:8000 --name app reflex-project:latest +docker run -d -p 3000:3000 --name app reflex-project:latest ``` diff --git a/news/+docker-examples-refresh.docs.md b/news/+docker-examples-refresh.docs.md new file mode 100644 index 00000000000..7a5f1301d7a --- /dev/null +++ b/news/+docker-examples-refresh.docs.md @@ -0,0 +1 @@ +Refresh the `docker-example` deployments for current Reflex: the two-port example becomes `simple-one-process` (prod mode now serves the frontend and backend on one port), the multi-stage images drop `reflex init`, bun downloads, and `libpq-dev` where the backend never needs them, uv and bun come from their official images, `STOPSIGNAL SIGKILL` is removed now that the backend shuts down cleanly on SIGTERM, Caddy serves the pre-compressed assets Reflex already writes, and the compose example persists Caddy's TLS state in the right directory. The self-hosting docs are updated to match. From 8c2502f4ee31d236fb7551f88ae1716b7fce1915 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 14 Sep 2026 12:20:39 -0700 Subject: [PATCH 2/8] Name the news fragment after the PR Co-Authored-By: Claude Fable 5.1 --- news/{+docker-examples-refresh.docs.md => 7140.docs.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{+docker-examples-refresh.docs.md => 7140.docs.md} (100%) diff --git a/news/+docker-examples-refresh.docs.md b/news/7140.docs.md similarity index 100% rename from news/+docker-examples-refresh.docs.md rename to news/7140.docs.md From e85a05f77e3b8663dd2e2cffb3d813d902b8e9f0 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 14 Sep 2026 13:35:20 -0700 Subject: [PATCH 3/8] Install requirements with uv in the single-stage examples too Co-Authored-By: Claude Fable 5.1 --- docker-example/simple-one-port/Dockerfile | 8 +++++--- docker-example/simple-one-process/Dockerfile | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docker-example/simple-one-port/Dockerfile b/docker-example/simple-one-port/Dockerfile index 29a2e34cd35..cbe5dedd9e1 100644 --- a/docker-example/simple-one-port/Dockerfile +++ b/docker-example/simple-one-port/Dockerfile @@ -11,10 +11,11 @@ FROM python:3.13-slim ARG PORT=8080 # Only set for local/direct access. When TLS is used, the API_URL is assumed to be the same as the frontend. ARG API_URL -ENV PORT=$PORT REFLEX_API_URL=${API_URL:-http://localhost:$PORT} REFLEX_REDIS_URL=redis://localhost PYTHONUNBUFFERED=1 +ENV PORT=$PORT REFLEX_API_URL=${API_URL:-http://localhost:$PORT} REFLEX_REDIS_URL=redis://localhost UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy PYTHONUNBUFFERED=1 -# Install redis server inside image; take caddy and bun from their official images. +# Install redis server inside image; take uv, caddy, and bun from their official images. RUN apt-get update -y && apt-get install -y --no-install-recommends redis-server && rm -rf /var/lib/apt/lists/* +COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /bin/uv COPY --from=caddy:2 /usr/bin/caddy /usr/bin/caddy COPY --from=oven/bun:1 /usr/local/bin/bun /usr/local/bin/bun @@ -22,7 +23,8 @@ WORKDIR /app # Install python requirements first so app edits do not reinstall them. COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --system -r requirements.txt # Copy local context to `/app` inside container (see .dockerignore) COPY . . diff --git a/docker-example/simple-one-process/Dockerfile b/docker-example/simple-one-process/Dockerfile index 6d03ec26fa2..e21824164dc 100644 --- a/docker-example/simple-one-process/Dockerfile +++ b/docker-example/simple-one-process/Dockerfile @@ -7,15 +7,17 @@ # with a prebuilt frontend see `simple-one-port` or `production-one-port`. FROM python:3.13-slim -# Reflex uses a bun found on PATH instead of downloading its own. +# uv installs python packages; reflex uses a bun found on PATH instead of downloading its own. +COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /bin/uv COPY --from=oven/bun:1 /usr/local/bin/bun /usr/local/bin/bun -ENV PYTHONUNBUFFERED=1 +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy PYTHONUNBUFFERED=1 WORKDIR /app # Install python requirements first so app edits do not reinstall them. COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --system -r requirements.txt # Copy local context to `/app` inside container (see .dockerignore) COPY . . From 1ec163ff8392f8e116ded90121751a7aed9d195d Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 14 Sep 2026 15:43:54 -0700 Subject: [PATCH 4/8] Fold simple-one-port into production-one-port 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 --- docker-example/README.md | 13 ++---- docker-example/production-one-port/README.md | 6 +-- docker-example/simple-one-port/.dockerignore | 9 ---- docker-example/simple-one-port/Caddyfile | 17 ------- docker-example/simple-one-port/Dockerfile | 44 ------------------ docker-example/simple-one-port/README.md | 48 -------------------- docker-example/simple-one-process/Dockerfile | 2 +- news/7140.docs.md | 2 +- 8 files changed, 8 insertions(+), 133 deletions(-) delete mode 100644 docker-example/simple-one-port/.dockerignore delete mode 100644 docker-example/simple-one-port/Caddyfile delete mode 100644 docker-example/simple-one-port/Dockerfile delete mode 100644 docker-example/simple-one-port/README.md diff --git a/docker-example/README.md b/docker-example/README.md index 6fa797d7032..956d8a91891 100644 --- a/docker-example/README.md +++ b/docker-example/README.md @@ -13,18 +13,13 @@ The most basic deployment: a single Reflex process serves both the static frontend and the backend on one port. No reverse proxy, no Redis. The frontend is rebuilt each time the container starts. -## `simple-one-port` +## `production-one-port` This deployment exports the frontend statically and serves it via a single HTTP port using Caddy, with a local Redis for state. The backend starts instantly -because the frontend is built into the image, but the build tooling stays in -the image. - -## `production-one-port` - -Same layout as `simple-one-port`, built in multiple stages so the final image -contains no bun, `node_modules`, or build tooling, and Python dependencies are -cached in their own layer. +because the frontend is built into the image, and a multi-stage build keeps +bun, `node_modules`, and other build tooling out of the final image. This is +useful for platforms that only support a single port, such as Render or Heroku. ## `production-compose` diff --git a/docker-example/production-one-port/README.md b/docker-example/production-one-port/README.md index 8a1af4837fc..fcd395ae78e 100644 --- a/docker-example/production-one-port/README.md +++ b/docker-example/production-one-port/README.md @@ -7,7 +7,8 @@ This docker deployment runs Reflex in prod mode, exposing a single HTTP port: The deployment also runs a local Redis server to store state for each user, which lets the backend run multiple worker processes. -Conceptually it is similar to the `simple-one-port` example except it: +The frontend is exported at build time and served as static files by Caddy, +so the backend starts in a couple of seconds. The build: - installs python dependencies in their own layer, so app edits do not reinstall them @@ -16,9 +17,6 @@ Conceptually it is similar to the `simple-one-port` example except it: - uses a multi-stage build so the final image has no bun, `node_modules`, or build tooling -The frontend is exported at build time and served as static files by Caddy, -so the backend starts in a couple of seconds. - ## Build ```console diff --git a/docker-example/simple-one-port/.dockerignore b/docker-example/simple-one-port/.dockerignore deleted file mode 100644 index b5668add4d9..00000000000 --- a/docker-example/simple-one-port/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -.web -.git -.venv -__pycache__ -*.py[cod] -.states -*.db -uploaded_files -Dockerfile diff --git a/docker-example/simple-one-port/Caddyfile b/docker-example/simple-one-port/Caddyfile deleted file mode 100644 index 5891d546eea..00000000000 --- a/docker-example/simple-one-port/Caddyfile +++ /dev/null @@ -1,17 +0,0 @@ -:{$PORT} - -encode gzip - -@backend_routes path /_event/* /ping /_health /_upload /_upload/* -handle @backend_routes { - reverse_proxy localhost:8000 -} - -root * /srv -route { - try_files {path} {path}/ /404.html - # Reflex writes a pre-compressed copy of every asset at export time. - file_server { - precompressed br zstd gzip - } -} diff --git a/docker-example/simple-one-port/Dockerfile b/docker-example/simple-one-port/Dockerfile deleted file mode 100644 index cbe5dedd9e1..00000000000 --- a/docker-example/simple-one-port/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -# check=skip=JSONArgsRecommended -# This Dockerfile is used to deploy a single-container Reflex app instance -# to services like Render, Railway, Heroku, GCP, and others. - -# It uses a reverse proxy to serve the frontend statically and proxy to backend -# from a single exposed port, expecting TLS termination to be handled at the -# edge by the given platform. -FROM python:3.13-slim - -# If the service expects a different port, provide it here (f.e Render expects port 10000) -ARG PORT=8080 -# Only set for local/direct access. When TLS is used, the API_URL is assumed to be the same as the frontend. -ARG API_URL -ENV PORT=$PORT REFLEX_API_URL=${API_URL:-http://localhost:$PORT} REFLEX_REDIS_URL=redis://localhost UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy PYTHONUNBUFFERED=1 - -# Install redis server inside image; take uv, caddy, and bun from their official images. -RUN apt-get update -y && apt-get install -y --no-install-recommends redis-server && rm -rf /var/lib/apt/lists/* -COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /bin/uv -COPY --from=caddy:2 /usr/bin/caddy /usr/bin/caddy -COPY --from=oven/bun:1 /usr/local/bin/bun /usr/local/bin/bun - -WORKDIR /app - -# Install python requirements first so app edits do not reinstall them. -COPY requirements.txt . -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system -r requirements.txt - -# Copy local context to `/app` inside container (see .dockerignore) -COPY . . - -# Download all npm dependencies and compile frontend into /srv for caddy. Keep .web/backend so the backend only -# evaluates stateful pages at startup instead of every page. -RUN reflex export --frontend-only --no-zip \ - && cp -r .web/build/client/. /srv/ \ - && mv .web/backend /tmp/backend && rm -rf .web && mkdir .web && mv /tmp/backend .web/backend - -EXPOSE $PORT - -# Apply migrations before starting the backend. -CMD [ -d alembic ] && reflex db migrate; \ - caddy start && \ - redis-server --daemonize yes && \ - exec reflex run --env prod --backend-only diff --git a/docker-example/simple-one-port/README.md b/docker-example/simple-one-port/README.md deleted file mode 100644 index 5bfb65643b6..00000000000 --- a/docker-example/simple-one-port/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# simple-one-port - -This docker deployment runs Reflex in prod mode, exposing a single HTTP port: - -- `8080` (`$PORT`) - Caddy server hosting the frontend statically and proxying requests to the backend. - -The deployment also runs a local Redis server to store state for each user, -which lets the backend run multiple worker processes. - -The frontend is exported at build time and served as static files by Caddy, -so the backend starts in a couple of seconds and bun is not needed at runtime. -The build tooling stays in the image, though; see `production-one-port` for a -multi-stage build that leaves it behind. - -## Build - -```console -docker build -t reflex-simple-one-port . -``` - -To listen on a different port, pass `--build-arg PORT=10000`. - -## Run - -```console -docker run -p 8080:8080 reflex-simple-one-port -``` - -By default the backend runs `2 * cpu_count + 1` workers. Set `GRANIAN_WORKERS` -to a smaller number in memory constrained environments: - -```console -docker run -e GRANIAN_WORKERS=2 -p 8080:8080 reflex-simple-one-port -``` - -Note that this container has _no persistence_ and will lose all data when -stopped. You can use bind mounts or named volumes to persist the database and -uploaded_files directories as needed. - -## Usage - -This container should be used with an existing load balancer or reverse proxy to -terminate TLS. - -It is also useful for deploying to simple app platforms, such as Render or Heroku. - -If the app defines additional backend API routes, add them to the -`@backend_routes` matcher in the `Caddyfile` so they are forwarded to the backend. diff --git a/docker-example/simple-one-process/Dockerfile b/docker-example/simple-one-process/Dockerfile index e21824164dc..fa1eb484354 100644 --- a/docker-example/simple-one-process/Dockerfile +++ b/docker-example/simple-one-process/Dockerfile @@ -4,7 +4,7 @@ # # The frontend is compiled and built each time the container starts, so startup # takes longer than the other examples and bun stays in the image. For an image -# with a prebuilt frontend see `simple-one-port` or `production-one-port`. +# with a prebuilt frontend see `production-one-port`. FROM python:3.13-slim # uv installs python packages; reflex uses a bun found on PATH instead of downloading its own. diff --git a/news/7140.docs.md b/news/7140.docs.md index 7a5f1301d7a..85cbd7e2d23 100644 --- a/news/7140.docs.md +++ b/news/7140.docs.md @@ -1 +1 @@ -Refresh the `docker-example` deployments for current Reflex: the two-port example becomes `simple-one-process` (prod mode now serves the frontend and backend on one port), the multi-stage images drop `reflex init`, bun downloads, and `libpq-dev` where the backend never needs them, uv and bun come from their official images, `STOPSIGNAL SIGKILL` is removed now that the backend shuts down cleanly on SIGTERM, Caddy serves the pre-compressed assets Reflex already writes, and the compose example persists Caddy's TLS state in the right directory. The self-hosting docs are updated to match. +Refresh the `docker-example` deployments for current Reflex: the two-port example becomes `simple-one-process` (prod mode now serves the frontend and backend on one port), `simple-one-port` is folded into `production-one-port`, the multi-stage images drop `reflex init`, bun downloads, and `libpq-dev` where the backend never needs them, uv and bun come from their official images, `STOPSIGNAL SIGKILL` is removed now that the backend shuts down cleanly on SIGTERM, Caddy serves the pre-compressed assets Reflex already writes, and the compose example persists Caddy's TLS state in the right directory. The self-hosting docs are updated to match. From 9431ec014a89e473e4d102088d3917699e2f5c16 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 14 Sep 2026 15:49:09 -0700 Subject: [PATCH 5/8] Consolidate docker examples into simple, production, production-compose, 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 --- docker-example/README.md | 11 ++++++----- .../.dockerignore | 0 .../Dockerfile | 0 .../README.md | 2 +- .../.dockerignore | 0 .../Caddyfile | 0 .../Dockerfile | 0 .../README.md | 8 ++++---- .../{simple-one-process => simple}/.dockerignore | 0 .../{simple-one-process => simple}/Dockerfile | 16 ++++++++++------ .../{simple-one-process => simple}/README.md | 14 ++++++++------ news/7140.docs.md | 2 +- 12 files changed, 30 insertions(+), 23 deletions(-) rename docker-example/{production-app-platform => app-platform-backend}/.dockerignore (100%) rename docker-example/{production-app-platform => app-platform-backend}/Dockerfile (100%) rename docker-example/{production-app-platform => app-platform-backend}/README.md (99%) rename docker-example/{production-one-port => production}/.dockerignore (100%) rename docker-example/{production-one-port => production}/Caddyfile (100%) rename docker-example/{production-one-port => production}/Dockerfile (100%) rename docker-example/{production-one-port => production}/README.md (88%) rename docker-example/{simple-one-process => simple}/.dockerignore (100%) rename docker-example/{simple-one-process => simple}/Dockerfile (61%) rename docker-example/{simple-one-process => simple}/README.md (77%) diff --git a/docker-example/README.md b/docker-example/README.md index 956d8a91891..26021a205a2 100644 --- a/docker-example/README.md +++ b/docker-example/README.md @@ -7,13 +7,14 @@ includes the `reflex` package. Commit the `reflex.lock/` directory that `reflex init` creates so the frontend dependencies installed in the image match the ones you developed against. -## `simple-one-process` +## `simple` The most basic deployment: a single Reflex process serves both the static -frontend and the backend on one port. No reverse proxy, no Redis. The frontend -is rebuilt each time the container starts. +frontend and the backend on one port. No reverse proxy, no Redis. The app is +compiled when the image is built; only the frontend bundle is rebuilt when the +container starts. -## `production-one-port` +## `production` This deployment exports the frontend statically and serves it via a single HTTP port using Caddy, with a local Redis for state. The backend starts instantly @@ -28,7 +29,7 @@ single Reflex app. It provides the entire stack in a single `compose.yaml` including a webserver with automatic TLS, one or more backend instances, redis, and a postgres database. -## `production-app-platform` +## `app-platform-backend` This example deployment is intended for use with App hosting platforms, like Azure, AWS, or Google Cloud Run. It is the backend of the deployment, which diff --git a/docker-example/production-app-platform/.dockerignore b/docker-example/app-platform-backend/.dockerignore similarity index 100% rename from docker-example/production-app-platform/.dockerignore rename to docker-example/app-platform-backend/.dockerignore diff --git a/docker-example/production-app-platform/Dockerfile b/docker-example/app-platform-backend/Dockerfile similarity index 100% rename from docker-example/production-app-platform/Dockerfile rename to docker-example/app-platform-backend/Dockerfile diff --git a/docker-example/production-app-platform/README.md b/docker-example/app-platform-backend/README.md similarity index 99% rename from docker-example/production-app-platform/README.md rename to docker-example/app-platform-backend/README.md index 8c6ce9dc3ab..3734caf6063 100644 --- a/docker-example/production-app-platform/README.md +++ b/docker-example/app-platform-backend/README.md @@ -1,4 +1,4 @@ -# production-app-platform +# app-platform-backend This example deployment is intended for use with App hosting platforms, like Azure, AWS, or Google Cloud Run. diff --git a/docker-example/production-one-port/.dockerignore b/docker-example/production/.dockerignore similarity index 100% rename from docker-example/production-one-port/.dockerignore rename to docker-example/production/.dockerignore diff --git a/docker-example/production-one-port/Caddyfile b/docker-example/production/Caddyfile similarity index 100% rename from docker-example/production-one-port/Caddyfile rename to docker-example/production/Caddyfile diff --git a/docker-example/production-one-port/Dockerfile b/docker-example/production/Dockerfile similarity index 100% rename from docker-example/production-one-port/Dockerfile rename to docker-example/production/Dockerfile diff --git a/docker-example/production-one-port/README.md b/docker-example/production/README.md similarity index 88% rename from docker-example/production-one-port/README.md rename to docker-example/production/README.md index fcd395ae78e..8515678e66a 100644 --- a/docker-example/production-one-port/README.md +++ b/docker-example/production/README.md @@ -1,4 +1,4 @@ -# production-one-port +# production This docker deployment runs Reflex in prod mode, exposing a single HTTP port: @@ -20,7 +20,7 @@ so the backend starts in a couple of seconds. The build: ## Build ```console -docker build -t reflex-production-one-port . +docker build -t reflex-production . ``` To listen on a different port, pass `--build-arg PORT=10000`. @@ -28,14 +28,14 @@ To listen on a different port, pass `--build-arg PORT=10000`. ## Run ```console -docker run -p 8080:8080 reflex-production-one-port +docker run -p 8080:8080 reflex-production ``` By default the backend runs `2 * cpu_count + 1` workers. Set `GRANIAN_WORKERS` to a smaller number in memory constrained environments: ```console -docker run -e GRANIAN_WORKERS=2 -p 8080:8080 reflex-production-one-port +docker run -e GRANIAN_WORKERS=2 -p 8080:8080 reflex-production ``` Note that this container has _no persistence_ and will lose all data when diff --git a/docker-example/simple-one-process/.dockerignore b/docker-example/simple/.dockerignore similarity index 100% rename from docker-example/simple-one-process/.dockerignore rename to docker-example/simple/.dockerignore diff --git a/docker-example/simple-one-process/Dockerfile b/docker-example/simple/Dockerfile similarity index 61% rename from docker-example/simple-one-process/Dockerfile rename to docker-example/simple/Dockerfile index fa1eb484354..eb094f89201 100644 --- a/docker-example/simple-one-process/Dockerfile +++ b/docker-example/simple/Dockerfile @@ -2,9 +2,10 @@ # The simplest single-container deployment: one Reflex process serves the # static frontend and the backend on the same port. No reverse proxy, no Redis. # -# The frontend is compiled and built each time the container starts, so startup +# The frontend is compiled at build time and the container start skips that +# step, but the production bundle is still rebuilt on each start, so startup # takes longer than the other examples and bun stays in the image. For an image -# with a prebuilt frontend see `production-one-port`. +# that serves a prebuilt frontend see `production`. FROM python:3.13-slim # uv installs python packages; reflex uses a bun found on PATH instead of downloading its own. @@ -22,12 +23,15 @@ RUN --mount=type=cache,target=/root/.cache/uv \ # Copy local context to `/app` inside container (see .dockerignore) COPY . . -# Install frontend dependencies at build time so the container does not need -# network access to start. The build output itself is regenerated on startup. -RUN reflex export --frontend-only --no-zip && rm -rf .web/build +# Compile the app and install frontend dependencies at build time so the +# container does not need network access to start. +RUN reflex export --frontend-only --no-zip EXPOSE 3000 -# Apply migrations before starting the backend. +# Apply migrations before starting the backend. The nocompile flag makes +# reflex reuse the compile from the image instead of redoing it; reflex deletes +# the flag once read, so it is recreated on every start. CMD [ -d alembic ] && reflex db migrate; \ + touch .web/nocompile && \ exec reflex run --env prod --frontend-port ${PORT:-3000} diff --git a/docker-example/simple-one-process/README.md b/docker-example/simple/README.md similarity index 77% rename from docker-example/simple-one-process/README.md rename to docker-example/simple/README.md index 16edf068c7c..85364d2b0ea 100644 --- a/docker-example/simple-one-process/README.md +++ b/docker-example/simple/README.md @@ -1,4 +1,4 @@ -# simple-one-process +# simple This docker deployment runs Reflex in prod mode with a single process exposing a single HTTP port: @@ -7,19 +7,21 @@ a single HTTP port: (event websocket, `/ping`, `/_upload`). No reverse proxy or Redis is involved, so the backend runs a single worker -with in-memory state. The frontend is compiled and built when the container -starts, which adds some time before the app is reachable. +with in-memory state. The app is compiled when the image is built and the +container reuses that compile, but the production frontend bundle is still +rebuilt when the container starts, which adds some time before the app is +reachable. ## Build ```console -docker build -t reflex-simple-one-process . +docker build -t reflex-simple . ``` ## Run ```console -docker run -p 3000:3000 reflex-simple-one-process +docker run -p 3000:3000 reflex-simple ``` Map the same port on both sides. The frontend connects to the backend using @@ -31,7 +33,7 @@ dropped and the frontend connects to its own origin. To listen on a different port set `PORT`: ```console -docker run -e PORT=8080 -p 8080:8080 reflex-simple-one-process +docker run -e PORT=8080 -p 8080:8080 reflex-simple ``` Note that this container has _no persistence_ and will lose all data when diff --git a/news/7140.docs.md b/news/7140.docs.md index 85cbd7e2d23..517d96cf59a 100644 --- a/news/7140.docs.md +++ b/news/7140.docs.md @@ -1 +1 @@ -Refresh the `docker-example` deployments for current Reflex: the two-port example becomes `simple-one-process` (prod mode now serves the frontend and backend on one port), `simple-one-port` is folded into `production-one-port`, the multi-stage images drop `reflex init`, bun downloads, and `libpq-dev` where the backend never needs them, uv and bun come from their official images, `STOPSIGNAL SIGKILL` is removed now that the backend shuts down cleanly on SIGTERM, Caddy serves the pre-compressed assets Reflex already writes, and the compose example persists Caddy's TLS state in the right directory. The self-hosting docs are updated to match. +Refresh the `docker-example` deployments for current Reflex and consolidate them into `simple`, `production`, `production-compose`, and `app-platform-backend`. Prod mode now serves the frontend and backend on one port, so the two-port example is gone; the images drop `reflex init`, bun downloads, and `libpq-dev` where the backend never needs them, take uv and bun from their official images, drop `STOPSIGNAL SIGKILL` now that the backend shuts down cleanly on SIGTERM, serve the pre-compressed assets Reflex already writes from Caddy, and persist Caddy's TLS state in the right directory in the compose example. The self-hosting docs are updated to match. From 6028a84f3bd3eba04912427e8f91b41b47d07cbd Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 14 Sep 2026 15:53:22 -0700 Subject: [PATCH 6/8] Drop the simple docker example 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 --- docker-example/README.md | 18 ++++------ docker-example/simple/.dockerignore | 9 ----- docker-example/simple/Dockerfile | 37 ------------------- docker-example/simple/README.md | 56 ----------------------------- docs/hosting/self-hosting.md | 8 +++-- news/7140.docs.md | 2 +- 6 files changed, 12 insertions(+), 118 deletions(-) delete mode 100644 docker-example/simple/.dockerignore delete mode 100644 docker-example/simple/Dockerfile delete mode 100644 docker-example/simple/README.md diff --git a/docker-example/README.md b/docker-example/README.md index 26021a205a2..cdcdbff4453 100644 --- a/docker-example/README.md +++ b/docker-example/README.md @@ -7,20 +7,14 @@ includes the `reflex` package. Commit the `reflex.lock/` directory that `reflex init` creates so the frontend dependencies installed in the image match the ones you developed against. -## `simple` - -The most basic deployment: a single Reflex process serves both the static -frontend and the backend on one port. No reverse proxy, no Redis. The app is -compiled when the image is built; only the frontend bundle is rebuilt when the -container starts. - ## `production` -This deployment exports the frontend statically and serves it via a single HTTP -port using Caddy, with a local Redis for state. The backend starts instantly -because the frontend is built into the image, and a multi-stage build keeps -bun, `node_modules`, and other build tooling out of the final image. This is -useful for platforms that only support a single port, such as Render or Heroku. +Start here. This single-container deployment exports the frontend statically +and serves it via a single HTTP port using Caddy, with a local Redis for state. +The backend starts instantly because the frontend is built into the image, and +a multi-stage build keeps bun, `node_modules`, and other build tooling out of +the final image. It works anywhere a container with one exposed port can run, +including platforms such as Render or Heroku. ## `production-compose` diff --git a/docker-example/simple/.dockerignore b/docker-example/simple/.dockerignore deleted file mode 100644 index b5668add4d9..00000000000 --- a/docker-example/simple/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -.web -.git -.venv -__pycache__ -*.py[cod] -.states -*.db -uploaded_files -Dockerfile diff --git a/docker-example/simple/Dockerfile b/docker-example/simple/Dockerfile deleted file mode 100644 index eb094f89201..00000000000 --- a/docker-example/simple/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -# check=skip=JSONArgsRecommended -# The simplest single-container deployment: one Reflex process serves the -# static frontend and the backend on the same port. No reverse proxy, no Redis. -# -# The frontend is compiled at build time and the container start skips that -# step, but the production bundle is still rebuilt on each start, so startup -# takes longer than the other examples and bun stays in the image. For an image -# that serves a prebuilt frontend see `production`. -FROM python:3.13-slim - -# uv installs python packages; reflex uses a bun found on PATH instead of downloading its own. -COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /bin/uv -COPY --from=oven/bun:1 /usr/local/bin/bun /usr/local/bin/bun - -ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy PYTHONUNBUFFERED=1 -WORKDIR /app - -# Install python requirements first so app edits do not reinstall them. -COPY requirements.txt . -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system -r requirements.txt - -# Copy local context to `/app` inside container (see .dockerignore) -COPY . . - -# Compile the app and install frontend dependencies at build time so the -# container does not need network access to start. -RUN reflex export --frontend-only --no-zip - -EXPOSE 3000 - -# Apply migrations before starting the backend. The nocompile flag makes -# reflex reuse the compile from the image instead of redoing it; reflex deletes -# the flag once read, so it is recreated on every start. -CMD [ -d alembic ] && reflex db migrate; \ - touch .web/nocompile && \ - exec reflex run --env prod --frontend-port ${PORT:-3000} diff --git a/docker-example/simple/README.md b/docker-example/simple/README.md deleted file mode 100644 index 85364d2b0ea..00000000000 --- a/docker-example/simple/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# simple - -This docker deployment runs Reflex in prod mode with a single process exposing -a single HTTP port: - -- `3000` (`$PORT`) - Reflex serves the static frontend and the backend - (event websocket, `/ping`, `/_upload`). - -No reverse proxy or Redis is involved, so the backend runs a single worker -with in-memory state. The app is compiled when the image is built and the -container reuses that compile, but the production frontend bundle is still -rebuilt when the container starts, which adds some time before the app is -reachable. - -## Build - -```console -docker build -t reflex-simple . -``` - -## Run - -```console -docker run -p 3000:3000 reflex-simple -``` - -Map the same port on both sides. The frontend connects to the backend using -the hostname it was loaded from and the port the app listens on, so -`-p 8080:3000` will not work over plain HTTP unless `REFLEX_API_URL` is set -to the externally visible address. Behind a TLS-terminating proxy the port is -dropped and the frontend connects to its own origin. - -To listen on a different port set `PORT`: - -```console -docker run -e PORT=8080 -p 8080:8080 reflex-simple -``` - -Note that this container has _no persistence_ and will lose all data when -stopped. You can use bind mounts or named volumes to persist the database and -uploaded_files directories as needed. - -## Usage - -This container should be used with an existing load balancer or reverse proxy to -terminate TLS. The proxy must pass the `Upgrade` header so the event websocket -can connect. - -For example, the following Caddyfile terminates TLS and forwards all traffic -to the container. - -``` -my-domain.com - -reverse_proxy localhost:3000 -``` diff --git a/docs/hosting/self-hosting.md b/docs/hosting/self-hosting.md index 3f7f9e6b518..822067363a3 100644 --- a/docs/hosting/self-hosting.md +++ b/docs/hosting/self-hosting.md @@ -113,8 +113,9 @@ Another option is to run your Reflex service in a container. Several `Dockerfile`s with additional documentation are available in the Reflex project in the directory [`docker-example`](https://github.com/reflex-dev/reflex/tree/main/docker-example), -ranging from a single process serving everything on one port to a full -compose stack with a TLS-terminating webserver, redis, and postgres. +ranging from a single container serving everything on one port to a full +compose stack with a TLS-terminating webserver, redis, and postgres. The +`production` example is the place to start. Before building the image, add a `requirements.txt` to the project folder that includes `reflex` and commit the `reflex.lock/` directory so the frontend @@ -130,6 +131,7 @@ hello │ └── hello.py ├── reflex.lock ├── rxconfig.py +├── Caddyfile ├── Dockerfile └── requirements.txt ``` @@ -143,5 +145,5 @@ docker build -t reflex-project:latest . Finally, you can start your Reflex container service as follows. ```bash -docker run -d -p 3000:3000 --name app reflex-project:latest +docker run -d -p 8080:8080 --name app reflex-project:latest ``` diff --git a/news/7140.docs.md b/news/7140.docs.md index 517d96cf59a..03af1d269e9 100644 --- a/news/7140.docs.md +++ b/news/7140.docs.md @@ -1 +1 @@ -Refresh the `docker-example` deployments for current Reflex and consolidate them into `simple`, `production`, `production-compose`, and `app-platform-backend`. Prod mode now serves the frontend and backend on one port, so the two-port example is gone; the images drop `reflex init`, bun downloads, and `libpq-dev` where the backend never needs them, take uv and bun from their official images, drop `STOPSIGNAL SIGKILL` now that the backend shuts down cleanly on SIGTERM, serve the pre-compressed assets Reflex already writes from Caddy, and persist Caddy's TLS state in the right directory in the compose example. The self-hosting docs are updated to match. +Refresh the `docker-example` deployments for current Reflex and consolidate them into `production`, `production-compose`, and `app-platform-backend`. Prod mode now serves the frontend and backend on one port, so the two-port example is gone; the images drop `reflex init`, bun downloads, and `libpq-dev` where the backend never needs them, take uv and bun from their official images, drop `STOPSIGNAL SIGKILL` now that the backend shuts down cleanly on SIGTERM, serve the pre-compressed assets Reflex already writes from Caddy, and persist Caddy's TLS state in the right directory in the compose example. The self-hosting docs are updated to match. From e36ac1d21f8d22a92b1ed04a27e9d0d3063693df Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 14 Sep 2026 16:00:30 -0700 Subject: [PATCH 7/8] Simplify the docker examples 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 --- .../app-platform-backend/Dockerfile | 54 +++++-------------- docker-example/app-platform-backend/README.md | 21 ++++++++ docker-example/production-compose/Dockerfile | 42 +++++++-------- .../production-compose/compose.yaml | 1 - docker-example/production/Dockerfile | 40 ++++++-------- 5 files changed, 71 insertions(+), 87 deletions(-) diff --git a/docker-example/app-platform-backend/Dockerfile b/docker-example/app-platform-backend/Dockerfile index ef30b77fb57..824c4ebfaa2 100644 --- a/docker-example/app-platform-backend/Dockerfile +++ b/docker-example/app-platform-backend/Dockerfile @@ -1,58 +1,28 @@ # check=skip=JSONArgsRecommended -# This docker file is intended to be used with container hosting services -# -# After deploying this image, get the URL pointing to the backend service -# and run REFLEX_API_URL=https://path-to-my-container.example.com reflex export --frontend-only --no-zip -# then copy the contents of `.web/build/client` to your static file server (github pages, s3, etc). -# -# Azure Static Web App example: -# npx @azure/static-web-apps-cli deploy --env production --app-location .web/build/client -# -# For dynamic routes to function properly, ensure that 404s are redirected to /404 on the -# static file host (for github pages, this works out of the box; remember to create .nojekyll). -# -# For azure static web apps, add `staticwebapp.config.json` to to `.web/build/client` with the following: -# { -# "responseOverrides": { -# "404": { -# "rewrite": "/404.html" -# } -# } -# } +# Backend-only image for container hosting services. The frontend is exported +# separately and served from a static host; see README.md. # # Note: many container hosting platforms require amd64 images, so when building on an M1 Mac # for example, pass `docker build --platform=linux/amd64 ...` +FROM python:3.13-slim -# Stage 1: build the python environment -FROM python:3.13-slim AS builder - -# uv installs python packages much faster than pip. -COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /bin/uv -ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy VIRTUAL_ENV=/app/.venv PATH="/app/.venv/bin:$PATH" +ENV UV_COMPILE_BYTECODE=1 UV_NO_CACHE=1 PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 WORKDIR /app +# The app user needs to own /app itself so reflex can create .states there. +RUN adduser --disabled-password --home /app reflex && chown reflex /app -# Install app requirements and reflex inside virtualenv. This layer is only -# rebuilt when requirements.txt changes. +# Install python requirements first so app edits do not reinstall them. uv is +# mounted only for this step so it does not end up in the image. COPY requirements.txt . -RUN --mount=type=cache,target=/root/.cache/uv \ +RUN --mount=from=ghcr.io/astral-sh/uv:0.12,source=/uv,target=/bin/uv \ uv venv && uv pip install -r requirements.txt # Copy local context to `/app` inside container (see .dockerignore) -COPY . . -RUN mkdir -p /app/data /app/uploaded_files - -# Stage 2: copy artifacts into slim image -# The backend does not need bun, node_modules, or .web, so `reflex init` and -# the frontend build are skipped entirely; export the frontend separately. -FROM python:3.13-slim -WORKDIR /app -# The app user needs to own /app itself so reflex can create .states, data, and uploads there. -RUN adduser --disabled-password --home /app reflex && chown reflex /app -COPY --chown=reflex --from=builder /app /app +COPY --chown=reflex . . USER reflex -ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 +RUN mkdir -p data uploaded_files -# Always apply migrations before starting the backend. +# Apply migrations before starting the backend. CMD [ -d alembic ] && reflex db migrate; \ exec reflex run --env prod --backend-only --backend-port ${PORT:-8000} diff --git a/docker-example/app-platform-backend/README.md b/docker-example/app-platform-backend/README.md index 3734caf6063..75365b8a2dd 100644 --- a/docker-example/app-platform-backend/README.md +++ b/docker-example/app-platform-backend/README.md @@ -85,6 +85,27 @@ The following sections are currently a work in progress and may be incomplete. ### Azure +#### Static Web App + +Deploy the exported frontend with the Static Web Apps CLI: + +```bash +npx @azure/static-web-apps-cli deploy --env production --app-location .web/build/client +``` + +For dynamic routes to work, add `staticwebapp.config.json` to `.web/build/client` +so 404s are served from `/404.html`: + +```json +{ + "responseOverrides": { + "404": { + "rewrite": "/404.html" + } + } +} +``` + #### Persistent Storage If you need to use a database or upload files, you cannot save them to the diff --git a/docker-example/production-compose/Dockerfile b/docker-example/production-compose/Dockerfile index 9f95ae1099a..47e964caef7 100644 --- a/docker-example/production-compose/Dockerfile +++ b/docker-example/production-compose/Dockerfile @@ -1,45 +1,45 @@ # check=skip=JSONArgsRecommended -# This docker file is intended to be used with docker compose to deploy a production -# instance of a Reflex app. +# Backend image for the docker compose deployment. The frontend is exported +# here too, so the Caddy image can copy it out (see Caddy.Dockerfile). -# Stage 1: build the python environment and export the static frontend FROM python:3.13-slim AS builder # uv installs python packages; reflex uses a bun found on PATH instead of downloading its own. COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /bin/uv COPY --from=oven/bun:1 /usr/local/bin/bun /usr/local/bin/bun -ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy VIRTUAL_ENV=/app/.venv PATH="/app/.venv/bin:$PATH" +ENV UV_COMPILE_BYTECODE=1 UV_NO_CACHE=1 PATH="/app/.venv/bin:$PATH" WORKDIR /app -# Install app requirements and reflex inside virtualenv. This layer is only -# rebuilt when requirements.txt changes. +# Install python requirements first so app edits do not reinstall them. COPY requirements.txt . -RUN --mount=type=cache,target=/root/.cache/uv \ - uv venv && uv pip install -r requirements.txt +RUN uv venv && uv pip install -r requirements.txt # Copy local context to `/app` inside container (see .dockerignore) COPY . . -RUN mkdir -p /app/data /app/uploaded_files -# Export static copy of frontend to /app/.web/build/client, then drop the rest -# of .web (node_modules etc.) to save space in the backend image. The cache -# mount keeps bun's package cache between builds. Keep .web/backend so the backend only -# evaluates stateful pages at startup instead of every page. +# Compile the app and build the static frontend. The cache mount keeps bun's +# package cache between builds so unchanged dependencies are not downloaded again. RUN --mount=type=cache,target=/root/.bun/install/cache \ - reflex export --frontend-only --no-zip \ - && mv .web/build/client .web/backend /tmp/ && rm -rf .web \ - && mkdir -p .web/build && mv /tmp/client .web/build/client && mv /tmp/backend .web/backend + reflex export --frontend-only --no-zip -# Stage 2: copy artifacts into slim image + +# Final image: the python environment, the app source, the static frontend, and +# .web/backend so the backend only evaluates stateful pages at startup. FROM python:3.13-slim + +ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 + WORKDIR /app -# The app user needs to own /app itself so reflex can create .states, data, and uploads there. +# The app user needs to own /app itself so reflex can create .states there. RUN adduser --disabled-password --home /app reflex && chown reflex /app -COPY --chown=reflex --from=builder /app /app +COPY --chown=reflex --from=builder /app/.venv .venv +COPY --chown=reflex --from=builder /app/.web/backend .web/backend +COPY --chown=reflex --from=builder /app/.web/build/client .web/build/client +COPY --chown=reflex . . USER reflex -ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 +RUN mkdir -p data uploaded_files -# Always apply migrations before starting the backend. +# Apply migrations before starting the backend. CMD [ -d alembic ] && reflex db migrate; \ exec reflex run --env prod --backend-only diff --git a/docker-example/production-compose/compose.yaml b/docker-example/production-compose/compose.yaml index e08af8c1f38..94b1197e30d 100644 --- a/docker-example/production-compose/compose.yaml +++ b/docker-example/production-compose/compose.yaml @@ -7,7 +7,6 @@ # to publicly accessible domain where app will be hosted services: app: - image: local/reflex-app environment: REFLEX_DB_URL: sqlite:///data/reflex.db build: diff --git a/docker-example/production/Dockerfile b/docker-example/production/Dockerfile index 0b2fa1b3838..1a855ea6d44 100644 --- a/docker-example/production/Dockerfile +++ b/docker-example/production/Dockerfile @@ -1,57 +1,51 @@ # check=skip=JSONArgsRecommended -# This Dockerfile is used to deploy a single-container Reflex app instance -# to services like Render, Railway, Heroku, GCP, and others. +# Single-container Reflex deployment for services like Render, Railway, Heroku, +# GCP, and others. Caddy serves the exported frontend and proxies backend routes +# from a single exposed port; TLS termination is expected at the platform edge. # If the service expects a different port, provide it here (f.e Render expects port 10000) ARG PORT=8080 # Only set for local/direct access. When TLS is used, the API_URL is assumed to be the same as the frontend. ARG API_URL -# It uses a reverse proxy to serve the frontend statically and proxy to backend -# from a single exposed port, expecting TLS termination to be handled at the -# edge by the given platform. FROM python:3.13-slim AS builder # uv installs python packages; reflex uses a bun found on PATH instead of downloading its own. COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /bin/uv COPY --from=oven/bun:1 /usr/local/bin/bun /usr/local/bin/bun -ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy VIRTUAL_ENV=/app/.venv PATH="/app/.venv/bin:$PATH" +ENV UV_COMPILE_BYTECODE=1 UV_NO_CACHE=1 PATH="/app/.venv/bin:$PATH" WORKDIR /app -# Install python app requirements and reflex in the container. This layer is -# only rebuilt when requirements.txt changes. +# Install python requirements first so app edits do not reinstall them. COPY requirements.txt . -RUN --mount=type=cache,target=/root/.cache/uv \ - uv venv && uv pip install -r requirements.txt +RUN uv venv && uv pip install -r requirements.txt # Copy local context to `/app` inside container (see .dockerignore) COPY . . ARG PORT API_URL -# Download npm dependencies and compile frontend into /srv. The cache mount -# keeps bun's package cache between builds so unchanged dependencies are not -# downloaded again. Keep .web/backend so the backend only -# evaluates stateful pages at startup instead of every page. +# Compile the app and build the static frontend. The cache mount keeps bun's +# package cache between builds so unchanged dependencies are not downloaded again. RUN --mount=type=cache,target=/root/.bun/install/cache \ - REFLEX_API_URL=${API_URL:-http://localhost:$PORT} reflex export --frontend-only --no-zip \ - && cp -r .web/build/client/. /srv/ \ - && mv .web/backend /tmp/backend && rm -rf .web && mkdir .web && mv /tmp/backend .web/backend + REFLEX_API_URL=${API_URL:-http://localhost:$PORT} reflex export --frontend-only --no-zip -# Final image with only necessary files +# Final image: the python environment, the app source, the static frontend for +# caddy, and .web/backend so the backend only evaluates stateful pages at startup. FROM python:3.13-slim -# Install redis server inside image; take caddy from its official image. RUN apt-get update -y && apt-get install -y --no-install-recommends redis-server && rm -rf /var/lib/apt/lists/* COPY --from=caddy:2 /usr/bin/caddy /usr/bin/caddy -ARG PORT API_URL -ENV PATH="/app/.venv/bin:$PATH" PORT=$PORT REFLEX_API_URL=${API_URL:-http://localhost:$PORT} REFLEX_REDIS_URL=redis://localhost PYTHONUNBUFFERED=1 +ARG PORT +ENV PATH="/app/.venv/bin:$PATH" PORT=$PORT REFLEX_REDIS_URL=redis://localhost PYTHONUNBUFFERED=1 WORKDIR /app -COPY --from=builder /app /app -COPY --from=builder /srv /srv +COPY --from=builder /app/.venv .venv +COPY --from=builder /app/.web/backend .web/backend +COPY --from=builder /app/.web/build/client /srv +COPY . . EXPOSE $PORT From a7a4909545dce8bc4d34c75711a5cabb1db3ab37 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 14 Sep 2026 23:07:00 -0700 Subject: [PATCH 8/8] Stop the container when a migration fails The semicolon let startup continue past a failed alembic migration, so the app would serve against an incompatible schema. An if/fi chained with && keeps the no-alembic case working and propagates a migration failure. Also correct the README: the base compose.yaml is sqlite-only, and redis and postgres come from the compose.prod.yaml override. Co-Authored-By: Claude Opus 5 --- docker-example/README.md | 7 ++++--- docker-example/app-platform-backend/Dockerfile | 4 ++-- docker-example/production-compose/Dockerfile | 4 ++-- docker-example/production/Dockerfile | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docker-example/README.md b/docker-example/README.md index cdcdbff4453..3f8b811d0dd 100644 --- a/docker-example/README.md +++ b/docker-example/README.md @@ -19,9 +19,10 @@ including platforms such as Render or Heroku. ## `production-compose` This deployment is intended for use with a standalone VPS that is only hosting a -single Reflex app. It provides the entire stack in a single `compose.yaml` -including a webserver with automatic TLS, one or more backend instances, redis, -and a postgres database. +single Reflex app. `compose.yaml` provides a webserver with automatic TLS in +front of the app, which stores its data in SQLite. Adding the +`compose.prod.yaml` override swaps in postgres and redis, which also lets the +backend run multiple workers. ## `app-platform-backend` diff --git a/docker-example/app-platform-backend/Dockerfile b/docker-example/app-platform-backend/Dockerfile index 824c4ebfaa2..ef812759a60 100644 --- a/docker-example/app-platform-backend/Dockerfile +++ b/docker-example/app-platform-backend/Dockerfile @@ -23,6 +23,6 @@ COPY --chown=reflex . . USER reflex RUN mkdir -p data uploaded_files -# Apply migrations before starting the backend. -CMD [ -d alembic ] && reflex db migrate; \ +# Apply migrations before starting the backend; a failed migration stops the container. +CMD if [ -d alembic ]; then reflex db migrate; fi && \ exec reflex run --env prod --backend-only --backend-port ${PORT:-8000} diff --git a/docker-example/production-compose/Dockerfile b/docker-example/production-compose/Dockerfile index 47e964caef7..32d4f1c4f08 100644 --- a/docker-example/production-compose/Dockerfile +++ b/docker-example/production-compose/Dockerfile @@ -40,6 +40,6 @@ COPY --chown=reflex . . USER reflex RUN mkdir -p data uploaded_files -# Apply migrations before starting the backend. -CMD [ -d alembic ] && reflex db migrate; \ +# Apply migrations before starting the backend; a failed migration stops the container. +CMD if [ -d alembic ]; then reflex db migrate; fi && \ exec reflex run --env prod --backend-only diff --git a/docker-example/production/Dockerfile b/docker-example/production/Dockerfile index 1a855ea6d44..dbbc66433de 100644 --- a/docker-example/production/Dockerfile +++ b/docker-example/production/Dockerfile @@ -49,8 +49,8 @@ COPY . . EXPOSE $PORT -# Apply migrations before starting the backend. -CMD [ -d alembic ] && reflex db migrate; \ +# Apply migrations before starting the backend; a failed migration stops the container. +CMD if [ -d alembic ]; then reflex db migrate; fi && \ caddy start && \ redis-server --daemonize yes && \ exec reflex run --env prod --backend-only