Reflex is a relatively new (and exciting!) entrant to the world of Python web frameworks. It introduces a new paradigm of writing web apps, which also has a few implications on how such an application can be deployed externally.

In this blog post we'll have a broad look at how you can run Reflex applications in production.

Introduction

Before looking at the deployment story, let's first understand what a Reflex app looks like.

The main idea with Reflex is that you the developer can write your entire application in Python, making use of the data structures and primitives that Reflex offers. And when the time comes when you want to deploy your code publicly, Reflex will split your codebase into two parts - a backend and a frontend.

The backend will be a FastAPI server with a few core endpoints (and any other endpoints you may have added as part of your application). And the frontend will be a Next.js application.

As the developer you don't have to care about how this split is performed and how the compilation is done. The only thing you need to keep in mind is what is available on the frontend and on the backend. The Reflex frontend and backend communicate through an event-based system with updates sent over a websocket connection, so some primitives might only be available in the Python world and some might not.

Deployment Artifacts

As a consequence of the steps we mentioned above (compilation to FastAPI and Next.js), it's not possible to deploy Reflex applications like any other Python framework. At its core, you have two deployment artifacts - the FastAPI server and the Next.js application.

The FastAPI server can indeed be deployed like most other web applications, usually running behind gunicorn where the worker type is set to uvicorn.workers.UvicornWorker. The Next.js application has two modes of deployment - you can either run it as a Node application or you can export the entire application into a bunch of static files and deploy those on a static site hosting service.

Essentially those two components are what we need to deploy, which is also reflected in what the Reflex CLI gives you.

How to run the application

Broadly speaking, at the time of this writing there are two ways how you can run Reflex applications in production.

1. reflex run

The first way is to use reflex run --env prod.

~/W/p/g/code main ❯ reflex run --env prod
────────────────────────────────── Starting Reflex App ───────────────────────────────────
[Admin Dashboard] Admin enabled, building admin dashboard. Time: 2023-07-19 10:42:37.710005
Admin dashboard running at: http://localhost:8000/admin
────────────────────────────── Installing frontend packages ──────────────────────────────
bun install v0.6.1 (78229da7)
bun add v0.6.1 (78229da7)
Export process completed.
Building app...  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00
────────────────────────────────────── App Running ───────────────────────────────────────
App running at: http://localhost:3000

Executing this command starts two subprocesses, one each for the backend and the frontend. The backend is run using uvicorn and the frontend runs as a node process. After reflex run has successfully started, your machine will have two processes listening on port 8000 and 3000 for backend and frontend (HTTP) requests, respectively.

It's also possible to selectively run the backend and the frontend, so that you can take the "process management" responsibility of Reflex into your own hands.

Running reflex run --env prod --backend-only will instruct Reflex to only start the backend Uvicorn process.

~/W/p/g/code main ❯ reflex run --env prod --backend-only
────────────────────────────────── Starting Reflex App ───────────────────────────────────
[Admin Dashboard] Admin enabled, building admin dashboard. Time: 2023-07-19 10:42:37.710005
Admin dashboard running at: http://localhost:8000/admin

Similarly, running reflex run --env prod --frontend-only will instruct Reflex to only start the frontend node process.

~/W/p/g/code main ❯ reflex run --env prod --frontend-only
────────────────────────────────── Starting Reflex App ───────────────────────────────────
[Admin Dashboard] Admin enabled, building admin dashboard. Time: 2023-07-19 10:42:37.710005
Admin dashboard running at: http://localhost:8000/admin
────────────────────────────── Installing frontend packages ──────────────────────────────
bun install v0.6.1 (78229da7)
bun add v0.6.1 (78229da7)
Export process completed.
Building app...  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00
────────────────────────────────────── App Running ───────────────────────────────────────
App running at: http://localhost:3000

What's different in this case is that you have to manage these two processes yourself. We'll take a look at that in a bit.

2. reflex export

The second way that Reflex provides is the ability to export your application into a zip file, which is particularly interesting for the frontend.

If you run reflex export you will see that Reflex will create two files for you - frontend.zip and backend.zip. frontend.zip contains a static build of your Next.js application, which you can upload to any static site hosting service. And backend.zip contains the source code of your application from where you can run the Uvicorn process. Similar to reflex run, you can also add the --backend-only and --frontend-only flags to selectively export the backend or the frontend.

If you use this option, there is one less process to run. Since the frontend is exported as a set of static files, it's possible to put it behind a static file server and call it a day. The only dynamic process we need to run in this case is the Uvicorn backend.

This mode makes it a lot simpler to run Reflex apps in production. One example mode of deployment is to start the backend process (eg. using reflex run --env prod --backend-only), copy the statically exported frontend on to your server, and configure your reverse-proxy software to selectively route requests based on the URL. The frontend requests can be served with static files and the backend requests can be sent to the FastAPI server.

How to manage processes

In case you decide to use reflex run and run the frontend and backend selectively yourself, it's your responsibility to ensure that the two processes are kept running at all times.

There are plenty of process managers on the market that can do this for you. Systemd is quite popular in the Linux world. Supervisor is another process manager which is popular in the Python ecosystem. At their core they're responsible for making sure that the given processes are started properly, and restarted in case they're killed for any reason.

In case you decide to go with Supervisor, here's a quick configuration snippet you can use to keep the two processes running.

[program:backend]
directory=/app
command=reflex run --env prod --backend-only --backend-port 8000
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stdout
stderr_logfile_maxbytes=0

[program:frontend]
directory=/app
command=reflex run --env prod --frontend-only --frontend-port 3000
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stdout
stderr_logfile_maxbytes=0

And of course, if you decide instead to use reflex export the program:frontend section in the above snippet is not relevant and can be removed.

How to route user requests

The common thread between both the modes is how to route user requests. Depending on the URL of the incoming request, it would need to be served either by the frontend or the backend.

The usual solution in such cases is to use a reverse-proxy. Reverse-proxies are very robust at handling HTTP requests, serving static files, and also acting as a proxy between the end-user and downstream applications. Nginx is an extremely stable and popular reverse-proxy software. Caddy is another option which is more recent and has become quite popular in the last few years. The decision of which one to use depends on various factors which are outside the scope of this blog post.

In case you decide to go with Nginx, you will need to configure separate location blocks for the different kinds of requests depending on their URL paths. Here's a quick configuration snippet:

server {
    # ...

    # backend
    location ~ ^/(admin|ping|upload) {
        proxy_pass http://0.0.0.0:8000;
        # ...
    }

    # websocket
    location ~ /_event/ {
        proxy_pass http://0.0.0.0:8000;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Upgrade $http_upgrade;
        # ...
    }

    # frontend
    location / {
        proxy_pass http://0.0.0.0:3000;
        # ...
    }
}

In this snippet we're routing requests to /admin, /ping, and /upload to the Uvicorn process listening on port 8000. The /_event URL is accepting websocket connections which is why we need a separate location block to be able to upgrade the regular HTTP connection to a websocket connection. And last but not least, all the other requests are routed to the Node process running Next.js.


That's it for now! In this post we had a look at the two modes that Reflex (at the time of this writing) provides to run applications in production.

In the next post we'll dive deeper into setting such a scenario up. Specifically, we'll look at how to configure a process manager to run the backend and frontend and use a reverse-proxy process to route requests between the two so our setup can serve end-user traffic. Stay tuned!


Photo by Taylor Vick on Unsplash

Launch your next SaaS quickly using Python 🐍

User authentication, Stripe payments, SEO-optimized blog, and many more features work out of the box on day one. Simply download the codebase and start building.