HomeBlog

Deploying Reflex Front-End with Caddy

3 min read
David Muraya Reflex Caddy Blog Header Image
By David Muraya • March 28, 2025

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.

Building the Front-End

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.

Setting Up Caddy

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:

  • PORT: Caddy listens on the port specified by the PORT environment variable, making it flexible for different deployment environments.

  • 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.

Dockerfile Explanation

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:

Builder Stage

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.

Final Stage


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., GCP, Render, Heroku). If you need Caddy to handle HTTPS locally, you'll need to configure it accordingly, though that's beyond this basic guide.

Conclusion

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. 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.


Update - May 30, 2025

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.


Update - July 10, 2025

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:

  • Update any mv .web/_static/* /srv/ commands in your Dockerfile to mv .web/build/client/* /srv/.
  • Check your build and deployment steps for references to the old static directory.

This update is required for compatibility with Reflex v0.8.0


Update - August 2025

Reflex v0.8.3: Explicit Stateless App Configuration

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:

  • Added enable_state boolean flag to the App class (default: True)
  • Removed automatic state detection during page compilation

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.

About the Author

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.

Related Blog Posts

Enjoyed this blog post? Check out these related posts!

Adding Google Authentication to Your FastAPI Application
Connecting FastAPI to a Serverless Database with pool_pre_ping

Connecting FastAPI to a Serverless Database with pool_pre_ping

How to Keep FastAPI Connected to Serverless Databases Like Neon

Read More..

6 Essential FastAPI Middlewares for Production-Ready Apps

6 Essential FastAPI Middlewares for Production-Ready Apps

A guide to the 6 key middlewares for building secure, performant, and resilient FastAPI applications.

Read More..

How to Get a Structured JSON Response from a Web Page Using AI

How to Get a Structured JSON Response from a Web Page Using AI

Using AI to Extract Structured Data from Web Pages: A Kenyan Invoice Verification Case Study

Read More..

Contact Me

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.

© 2025 David Muraya. All rights reserved.