diff --git a/docker-example/README.md b/docker-example/README.md index 877fc22bdc1..3f8b811d0dd 100644 --- a/docker-example/README.md +++ b/docker-example/README.md @@ -3,27 +3,28 @@ 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` +## `production` -The most basic production deployment exposes two HTTP ports and relies on an -existing load balancer to forward the traffic appropriately. - -## `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. +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` 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. +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. -## `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/app-platform-backend/.dockerignore b/docker-example/app-platform-backend/.dockerignore new file mode 100644 index 00000000000..b5668add4d9 --- /dev/null +++ b/docker-example/app-platform-backend/.dockerignore @@ -0,0 +1,9 @@ +.web +.git +.venv +__pycache__ +*.py[cod] +.states +*.db +uploaded_files +Dockerfile diff --git a/docker-example/app-platform-backend/Dockerfile b/docker-example/app-platform-backend/Dockerfile new file mode 100644 index 00000000000..ef812759a60 --- /dev/null +++ b/docker-example/app-platform-backend/Dockerfile @@ -0,0 +1,28 @@ +# check=skip=JSONArgsRecommended +# 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 + +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 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=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 --chown=reflex . . +USER reflex +RUN mkdir -p data uploaded_files + +# 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-app-platform/README.md b/docker-example/app-platform-backend/README.md similarity index 71% rename from docker-example/production-app-platform/README.md rename to docker-example/app-platform-backend/README.md index 6a2f5bed8b4..75365b8a2dd 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. @@ -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,26 +85,25 @@ 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. +#### Static Web App -```python -import uvicorn.workers +Deploy the exported frontend with the Static Web Apps CLI: -import reflex as rx +```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`: -class NoWSPerMessageDeflate(uvicorn.workers.UvicornH11Worker): - CONFIG_KWARGS = { - **uvicorn.workers.UvicornH11Worker.CONFIG_KWARGS, - "ws_per_message_deflate": False, +```json +{ + "responseOverrides": { + "404": { + "rewrite": "/404.html" } - - -config = rx.Config( - app_name="my_app", - gunicorn_worker_class="rxconfig.NoWSPerMessageDeflate", -) + } +} ``` #### Persistent Storage diff --git a/docker-example/production-app-platform/.dockerignore b/docker-example/production-app-platform/.dockerignore deleted file mode 100644 index ea66a51f527..00000000000 --- a/docker-example/production-app-platform/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -.web -.git -__pycache__/* -Dockerfile -uploaded_files diff --git a/docker-example/production-app-platform/Dockerfile b/docker-example/production-app-platform/Dockerfile deleted file mode 100644 index 0331861ce01..00000000000 --- a/docker-example/production-app-platform/Dockerfile +++ /dev/null @@ -1,65 +0,0 @@ -# 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). -# -# 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" -# } -# } -# } -# -# 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 - -ARG uv=/root/.local/bin/uv - -# Install `uv` for faster package bootstrapping -ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh -RUN /install.sh && rm /install.sh - -# 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 -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/* -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-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..32d4f1c4f08 100644 --- a/docker-example/production-compose/Dockerfile +++ b/docker-example/production-compose/Dockerfile @@ -1,52 +1,45 @@ -# This docker file is intended to be used with docker compose to deploy a production -# instance of a Reflex app. +# check=skip=JSONArgsRecommended +# 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: init -FROM python:3.13 as init +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_NO_CACHE=1 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 python requirements first so app edits do not reinstall them. +COPY requirements.txt . +RUN 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 +# 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 -# Install app requirements and reflex inside virtualenv -RUN $uv pip install -r requirements.txt -# 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 +# 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 -# 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 +ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 -# 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 there. +RUN adduser --disabled-password --home /app reflex && chown reflex /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 - -# Needed until Reflex properly passes SIGTERM on backend. -STOPSIGNAL SIGKILL +RUN mkdir -p data uploaded_files -# Always 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-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..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: @@ -26,8 +25,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 +43,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 deleted file mode 100644 index 26ae41b8367..00000000000 --- a/docker-example/production-one-port/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -.web -!.web/bun.lockb -!.web/package.json diff --git a/docker-example/production-one-port/Caddyfile b/docker-example/production-one-port/Caddyfile deleted file mode 100644 index 28fb0186165..00000000000 --- a/docker-example/production-one-port/Caddyfile +++ /dev/null @@ -1,14 +0,0 @@ -:{$PORT} - -encode gzip - -@backend_routes path /_event/* /ping /_upload /_upload/* -handle @backend_routes { - reverse_proxy localhost:8000 -} - -root * /srv -route { - try_files {path} {path}/ /404.html - file_server -} diff --git a/docker-example/production-one-port/Dockerfile b/docker-example/production-one-port/Dockerfile deleted file mode 100644 index f7d83b7ede0..00000000000 --- a/docker-example/production-one-port/Dockerfile +++ /dev/null @@ -1,62 +0,0 @@ -# This Dockerfile is used to deploy a single-container Reflex app instance -# to services like Render, Railway, Heroku, GCP, and others. - -# 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 as builder - -RUN mkdir -p /app/.web -RUN python -m venv /app/.venv -ENV PATH="/app/.venv/bin:$PATH" - -WORKDIR /app - -# Install python app requirements and reflex in the container -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 - -# 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 - - -# 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/* - -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 - -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. -CMD [ -d alembic ] && reflex db migrate; \ - caddy start && \ - redis-server --daemonize yes && \ - exec reflex run --env prod --backend-only diff --git a/docker-example/production-one-port/README.md b/docker-example/production-one-port/README.md deleted file mode 100644 index 06d9ba079dd..00000000000 --- a/docker-example/production-one-port/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# production-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. - -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 - -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. - -## Build - -```console -docker build -t reflex-production-one-port . -``` - -## Run - -```console -docker run -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. - -## 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. diff --git a/docker-example/production/.dockerignore b/docker-example/production/.dockerignore new file mode 100644 index 00000000000..b5668add4d9 --- /dev/null +++ b/docker-example/production/.dockerignore @@ -0,0 +1,9 @@ +.web +.git +.venv +__pycache__ +*.py[cod] +.states +*.db +uploaded_files +Dockerfile diff --git a/docker-example/production/Caddyfile b/docker-example/production/Caddyfile new file mode 100644 index 00000000000..5891d546eea --- /dev/null +++ b/docker-example/production/Caddyfile @@ -0,0 +1,17 @@ +:{$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/production/Dockerfile b/docker-example/production/Dockerfile new file mode 100644 index 00000000000..dbbc66433de --- /dev/null +++ b/docker-example/production/Dockerfile @@ -0,0 +1,56 @@ +# check=skip=JSONArgsRecommended +# 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 + +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_NO_CACHE=1 PATH="/app/.venv/bin:$PATH" + +WORKDIR /app + +# Install python requirements first so app edits do not reinstall them. +COPY 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 +# 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 + + +# 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 + +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 +ENV PATH="/app/.venv/bin:$PATH" PORT=$PORT REFLEX_REDIS_URL=redis://localhost PYTHONUNBUFFERED=1 + +WORKDIR /app +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 + +# 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 diff --git a/docker-example/production/README.md b/docker-example/production/README.md new file mode 100644 index 00000000000..8515678e66a --- /dev/null +++ b/docker-example/production/README.md @@ -0,0 +1,53 @@ +# production + +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. The build: + +- 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 + +## Build + +```console +docker build -t reflex-production . +``` + +To listen on a different port, pass `--build-arg PORT=10000`. + +## Run + +```console +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 +``` + +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-port/.dockerignore b/docker-example/simple-one-port/.dockerignore deleted file mode 100644 index ea66a51f527..00000000000 --- a/docker-example/simple-one-port/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -.web -.git -__pycache__/* -Dockerfile -uploaded_files diff --git a/docker-example/simple-one-port/Caddyfile b/docker-example/simple-one-port/Caddyfile deleted file mode 100644 index 28fb0186165..00000000000 --- a/docker-example/simple-one-port/Caddyfile +++ /dev/null @@ -1,14 +0,0 @@ -:{$PORT} - -encode gzip - -@backend_routes path /_event/* /ping /_upload /_upload/* -handle @backend_routes { - reverse_proxy localhost:8000 -} - -root * /srv -route { - try_files {path} {path}/ /404.html - file_server -} diff --git a/docker-example/simple-one-port/Dockerfile b/docker-example/simple-one-port/Dockerfile deleted file mode 100644 index 0244d5bdb00..00000000000 --- a/docker-example/simple-one-port/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -# 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 - -# 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 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/* - -WORKDIR /app - -# 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 - -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 b249249babc..00000000000 --- a/docker-example/simple-one-port/README.md +++ /dev/null @@ -1,37 +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. - -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. - -## Build - -```console -docker build -t reflex-simple-one-port . -``` - -## Run - -```console -docker run -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. 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..822067363a3 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,39 +104,34 @@ 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 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. -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 +├── Caddyfile ├── Dockerfile └── requirements.txt ``` @@ -137,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 -p 8000:8000 --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 new file mode 100644 index 00000000000..03af1d269e9 --- /dev/null +++ b/news/7140.docs.md @@ -0,0 +1 @@ +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.