From 0a34949019f3aaab9b624b13dc401435c2caf802 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 9 Dec 2024 15:26:48 -0800 Subject: [PATCH] Add production-one-port example (#4489) * Add production-one-port example A more complex version of simple-one-port that facilitates better layer caching to shorten build times and multi-stage build to reduce final image size. Harder to understand, but ultimately nicer to use. * fix Caddyfile format to avoid complaints * docker-examples: bump all base images to python:3.13 --- .../production-app-platform/Dockerfile | 4 +- docker-example/production-compose/Dockerfile | 4 +- .../production-one-port/.dockerignore | 3 + docker-example/production-one-port/Caddyfile | 14 +++++ docker-example/production-one-port/Dockerfile | 62 +++++++++++++++++++ docker-example/production-one-port/README.md | 37 +++++++++++ docker-example/simple-one-port/Caddyfile | 2 +- docker-example/simple-one-port/Dockerfile | 4 +- docker-example/simple-two-port/Dockerfile | 2 +- 9 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 docker-example/production-one-port/.dockerignore create mode 100644 docker-example/production-one-port/Caddyfile create mode 100644 docker-example/production-one-port/Dockerfile create mode 100644 docker-example/production-one-port/README.md diff --git a/docker-example/production-app-platform/Dockerfile b/docker-example/production-app-platform/Dockerfile index 3dd9f1fed..fec3b13f1 100644 --- a/docker-example/production-app-platform/Dockerfile +++ b/docker-example/production-app-platform/Dockerfile @@ -23,7 +23,7 @@ # for example, pass `docker build --platform=linux/amd64 ...` # Stage 1: init -FROM python:3.11 as init +FROM python:3.13 as init ARG uv=/root/.local/bin/uv @@ -48,7 +48,7 @@ RUN $uv pip install -r requirements.txt RUN reflex init # Stage 2: copy artifacts into slim image -FROM python:3.11-slim +FROM python:3.13-slim WORKDIR /app RUN adduser --disabled-password --home /app reflex COPY --chown=reflex --from=init /app /app diff --git a/docker-example/production-compose/Dockerfile b/docker-example/production-compose/Dockerfile index 42345af40..757c03b8e 100644 --- a/docker-example/production-compose/Dockerfile +++ b/docker-example/production-compose/Dockerfile @@ -2,7 +2,7 @@ # instance of a Reflex app. # Stage 1: init -FROM python:3.11 as init +FROM python:3.13 as init ARG uv=/root/.local/bin/uv @@ -35,7 +35,7 @@ RUN rm -rf .web && mkdir .web RUN mv /tmp/_static .web/_static # Stage 2: copy artifacts into slim image -FROM python:3.11-slim +FROM python:3.13-slim WORKDIR /app RUN adduser --disabled-password --home /app reflex COPY --chown=reflex --from=init /app /app diff --git a/docker-example/production-one-port/.dockerignore b/docker-example/production-one-port/.dockerignore new file mode 100644 index 000000000..26ae41b83 --- /dev/null +++ b/docker-example/production-one-port/.dockerignore @@ -0,0 +1,3 @@ +.web +!.web/bun.lockb +!.web/package.json diff --git a/docker-example/production-one-port/Caddyfile b/docker-example/production-one-port/Caddyfile new file mode 100644 index 000000000..28fb01861 --- /dev/null +++ b/docker-example/production-one-port/Caddyfile @@ -0,0 +1,14 @@ +:{$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 new file mode 100644 index 000000000..f5e157bae --- /dev/null +++ b/docker-example/production-one-port/Dockerfile @@ -0,0 +1,62 @@ +# 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/fnm/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 API_URL=${API_URL:-http://localhost:$PORT} reflex export --loglevel debug --frontend-only --no-zip && mv .web/_static/* /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 API_URL=${API_URL:-http://localhost:$PORT} 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 new file mode 100644 index 000000000..f6f76620a --- /dev/null +++ b/docker-example/production-one-port/README.md @@ -0,0 +1,37 @@ +# 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 NextJS 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. \ No newline at end of file diff --git a/docker-example/simple-one-port/Caddyfile b/docker-example/simple-one-port/Caddyfile index 13d94ce8e..28fb01861 100644 --- a/docker-example/simple-one-port/Caddyfile +++ b/docker-example/simple-one-port/Caddyfile @@ -11,4 +11,4 @@ root * /srv route { try_files {path} {path}/ /404.html file_server -} \ No newline at end of file +} diff --git a/docker-example/simple-one-port/Dockerfile b/docker-example/simple-one-port/Dockerfile index 4cd7e9a18..0b6dba0c4 100644 --- a/docker-example/simple-one-port/Dockerfile +++ b/docker-example/simple-one-port/Dockerfile @@ -4,7 +4,7 @@ # 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.11 +FROM python:3.13 # If the service expects a different port, provide it here (f.e Render expects port 10000) ARG PORT=8080 @@ -38,4 +38,4 @@ EXPOSE $PORT CMD [ -d alembic ] && reflex db migrate; \ caddy start && \ redis-server --daemonize yes && \ - exec reflex run --env prod --backend-only \ No newline at end of file + exec reflex run --env prod --backend-only diff --git a/docker-example/simple-two-port/Dockerfile b/docker-example/simple-two-port/Dockerfile index 288f605a8..6d69fa390 100644 --- a/docker-example/simple-two-port/Dockerfile +++ b/docker-example/simple-two-port/Dockerfile @@ -1,5 +1,5 @@ # This Dockerfile is used to deploy a simple single-container Reflex app instance. -FROM python:3.12 +FROM python:3.13 RUN apt-get update && apt-get install -y redis-server && rm -rf /var/lib/apt/lists/* ENV REDIS_URL=redis://localhost PYTHONUNBUFFERED=1