Reflex is a versatile framework that allows you to build web applications with both front-end and back-end components. However, there are instances where you may only want a front-end application without the Reflex backend state management. Perhaps your backend is hosted separately, or your application doesn't require server-side logic at all. In this blog post, we'll walk through how to use Reflex to generate front-end code and serve it statically using Caddy, a powerful and easy-to-configure web server, all within a Docker container.
For a deeper dive into running Reflex on cloud platforms, you might also be interested in my guide on optimizing Reflex performance on Google Cloud Run.
To deploy just the front-end of a Reflex application, we need to generate static files that a web server can serve. Reflex makes this straightforward with the reflex export
command. By using the --frontend-only
flag, Reflex compiles your front-end code into static assets like HTML, CSS, and JavaScript, excluding any backend dependencies.
In this setup, we'll leverage a Docker container to automate the process of building these static files. The provided Dockerfile includes a multi-stage build that handles this efficiently. The principles of multi-stage builds are also covered in my article on creating slimmer Docker images.
Caddy is an excellent choice for serving static files due to its simplicity and robust features. In this deployment, Caddy will act as our web server, delivering the static front-end files generated by Reflex. The configuration for Caddy is defined in a Caddyfile.
Here's the one provided:
:{$PORT} encode gzip root * /srv route { try_files {path} {path}/ /404.html file_server }
Let's break down what this does:
encode gzip
: Enables gzip compression to reduce the size of responses, improving load times.root * /srv
: Sets the root directory for serving files to /srv, where our static files will reside.route
: Defines how Caddy handles requests:try_files {path} {path}/ /404.html
: Attempts to serve the requested path directly, then as a directory (e.g., appending /), and falls back to /404.html if neither exists.file_server
: Activates Caddy's static file serving capability.This configuration ensures that your front-end files are served efficiently, with a fallback to a 404 page for unmatched routes.
The provided Dockerfile is designed to build and deploy a Reflex front-end application in a single container. It uses a multi-stage build process to keep the final image lightweight. Here's how it works:
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 COPY requirements.txt . RUN pip install -r requirements.txt COPY rxconfig.py ./ RUN reflex init 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 . . ARG PORT REFLEX_API_URL RUN REFLEX_API_URL=${REFLEX_API_URL:-http://localhost:$PORT} reflex export --loglevel debug --frontend-only --no-zip && mv .web/build/client/* /srv/ && rm -rf .web
Base Image: Starts with python:3.13
to provide a full Python environment for building.
Environment Setup: Creates a virtual environment at /app/.venv
and adds it to the PATH.
Dependencies: Installs Python dependencies from requirements.txt
and initializes Reflex with reflex init
.
Front-End Dependencies: If a bun.lockb
file exists, it installs front-end dependencies using Bun, Reflex's preferred package manager.
Project Files: Copies your entire project into /app
.
Static Export: Runs reflex export --frontend-only --no-zip
to generate static files, placing them in .web/build/client
. The REFLEX_API_URL
is set to a default of http://localhost:$PORT
unless overridden. The static files are then moved to /srv
.
FROM python:3.13-slim RUN apt-get update -y && apt-get install -y caddy && rm -rf /var/lib/apt/lists/* ARG PORT REFLEX_API_URL ENV PATH="/app/.venv/bin:$PATH" PORT=$PORT REFLEX_API_URL=${REFLEX_API_URL:-http://localhost:$PORT} PYTHONUNBUFFERED=1 WORKDIR /app COPY --from=builder /app /app COPY --from=builder /srv /srv STOPSIGNAL SIGKILL EXPOSE $PORT CMD caddy run
Base Image: Uses python:3.13-slim
for a smaller image size.
Caddy Installation: Installs Caddy using apt-get
.
Environment Variables: Sets PORT
and REFLEX_API_URL
, with REFLEX_API_URL
defaulting to http://localhost:$PORT
.
File Copy: Copies /app
and /srv
from the builder stage. (Note: /app
may not be strictly necessary since Caddy only serves from /srv
, but it's included in the provided setup.)
Port Exposure: Exposes the specified $PORT
for external access.
Run Command: Starts Caddy with caddy run
, serving the static files from /srv
.
HTTPS: This setup assumes TLS termination is handled by the deployment platform (e.g., Google Cloud Platform, Render, Heroku). If you need Caddy to handle HTTPS locally, you'll need to configure it accordingly, though that's beyond this basic guide.
Deploying a Reflex front-end with Caddy is a streamlined process thanks to Reflex's static export capabilities and Caddy's efficient file serving. Using the provided Dockerfile, you can build your front-end into static files and serve them in a lightweight Docker container, perfect for platforms like GCP, Render, Railway, or Heroku. Once you have this manual process down, you can automate it with a CI/CD pipeline.
Just remember to set the API_URL
correctly if your app relies on a backend, and tweak the Caddyfile as needed for your application's routing requirements.
With this approach, you get a fast, scalable front-end deployment without the overhead of Reflex's backend state management - ideal for static sites or apps with separate backends.
It's important to note that as of Reflex version 0.7.13 and later, all environment variables used by Reflex must be prefixed with REFLEX_
. For example, the API_URL
environment variable mentioned in this article should now be set as REFLEX_API_URL
.
If you see a deprecation warning like:
DeprecationWarning: Usage of deprecated API_URL env var detected. has been deprecated in version 0.7.13. Prefer `REFLEX_` prefix when setting env vars. It will be completely removed in 0.8.0. (rxconfig.py:11)
Ensure you update your Dockerfile and any deployment configurations to use the REFLEX_
prefix for all relevant environment variables to maintain compatibility with future versions of Reflex.
Important: As of Reflex version v0.8.0, the location of static export files has changed.
From the release notes:
Static exports are now stored in
.web/build/client
(instead of.web/_static
)
If your Dockerfile or deployment scripts reference .web/_static/*
, you need to update them to use .web/build/client/*
instead. This change is part of a major rewrite in Reflex, replacing NextJS with React Router for improved performance and flexibility.
What to do:
mv .web/_static/* /srv/
commands in your Dockerfile to mv .web/build/client/* /srv/
.This update is required for compatibility with Reflex v0.8.0
Starting with Reflex version v0.8.3, if your application only serves static files and does not use backend state management, you must explicitly set your app to be stateless. Reflex no longer automatically detects whether state is being used. Instead, you need to pass enable_state=False
to your rx.App
instance:
import reflex as rx app = rx.App(enable_state=False)
This change prevents issues where automatic detection could conflict with explicit state configuration.
Key changes:
enable_state
boolean flag to the App
class (default: True
)If your app is frontend-only and does not require Reflex backend state, set enable_state=False
to ensure proper stateless behavior and avoid runtime errors.
For more details on this update, see the Reflex v0.8.3 pull request on GitHub.
David Muraya is a Solutions Architect specializing in Python, FastAPI, and Cloud Infrastructure. He is passionate about building scalable, production-ready applications and sharing his knowledge with the developer community. You can connect with him on LinkedIn.
Enjoyed this blog post? Check out these related posts!
Full-Text Search: Using the Trigram Tokenizer Algorithm to Match Peoples Names
Leveraging Full Text Search and Trigram Tokenization for Efficient Name Matching
Read More..
FastAPI Tutorial: A Complete Guide for Beginners
A step-by-step guide to building your first API with Python and FastAPI, from installation to production-ready concepts.
Read More..
Managing Background Tasks in FastAPI: BackgroundTasks vs ARQ + Redis
A practical guide to background processing in FastAPI, comparing built-in BackgroundTasks with ARQ and Redis for scalable async job queues.
Read More..
Connecting FastAPI to a Database with SQLModel
A practical guide to building CRUD APIs with FastAPI and SQLModel.
Read More..
Have a project in mind? Send me an email at hello@davidmuraya.com and let's bring your ideas to life. I am always available for exciting discussions.