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!