From 1d4603c60ac21bd068758c34799fcf7b14aa156d Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 5 Dec 2024 11:37:41 -0800 Subject: [PATCH] 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. --- .../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 +++++++++++ 4 files changed, 116 insertions(+) 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-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..13d94ce8e --- /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 +} \ No newline at end of file diff --git a/docker-example/production-one-port/Dockerfile b/docker-example/production-one-port/Dockerfile new file mode 100644 index 000000000..bec265a9a --- /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 \ No newline at end of file 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