Skip to content

Commit 09980c7

Browse files
committed
single port prod
1 parent 5feefe3 commit 09980c7

File tree

4 files changed

+92
-27
lines changed

4 files changed

+92
-27
lines changed

reflex/app.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,16 @@ def __call__(self) -> ASGIApp:
628628

629629
asgi_app = self._api
630630

631+
if environment.REFLEX_MOUNT_FRONTEND_COMPILED_APP.get():
632+
asgi_app.mount(
633+
"/",
634+
StaticFiles(
635+
directory=prerequisites.get_web_dir() / constants.Dirs.STATIC,
636+
html=True,
637+
),
638+
name="frontend",
639+
)
640+
631641
if self.api_transformer is not None:
632642
api_transformers: Sequence[Starlette | Callable[[ASGIApp], ASGIApp]] = (
633643
[self.api_transformer]

reflex/environment.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,9 @@ class EnvironmentVariables:
660660
# Whether to enable SSR for the frontend.
661661
REFLEX_SSR: EnvVar[bool] = env_var(True)
662662

663+
# Whether to mount the compiled frontend app in the backend server in production.
664+
REFLEX_MOUNT_FRONTEND_COMPILED_APP: EnvVar[bool] = env_var(False, internal=True)
665+
663666

664667
environment = EnvironmentVariables()
665668

reflex/reflex.py

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from __future__ import annotations
44

5-
import atexit
65
from importlib.util import find_spec
76
from pathlib import Path
87
from typing import TYPE_CHECKING
@@ -132,8 +131,11 @@ def _run(
132131
frontend_port: int | None = None,
133132
backend_port: int | None = None,
134133
backend_host: str | None = None,
134+
single_port: bool = False,
135135
):
136136
"""Run the app in the given directory."""
137+
import atexit
138+
137139
from reflex.utils import build, exec, prerequisites, processes
138140

139141
config = get_config()
@@ -173,7 +175,9 @@ def _run(
173175
auto_increment=auto_increment_frontend,
174176
)
175177

176-
if backend:
178+
if single_port:
179+
backend_port = frontend_port
180+
elif backend:
177181
auto_increment_backend = not bool(backend_port or config.backend_port)
178182

179183
backend_port = processes.handle_port(
@@ -223,23 +227,23 @@ def _run(
223227
if not return_result:
224228
raise SystemExit(1)
225229

230+
if env != constants.Env.PROD and env != constants.Env.DEV:
231+
msg = f"Invalid env: {env}. Must be DEV or PROD."
232+
raise ValueError(msg)
233+
226234
# Get the frontend and backend commands, based on the environment.
227-
setup_frontend = frontend_cmd = backend_cmd = None
228235
if env == constants.Env.DEV:
229236
setup_frontend, frontend_cmd, backend_cmd = (
230237
build.setup_frontend,
231238
exec.run_frontend,
232239
exec.run_backend,
233240
)
234-
if env == constants.Env.PROD:
241+
elif env == constants.Env.PROD:
235242
setup_frontend, frontend_cmd, backend_cmd = (
236243
build.setup_frontend_prod,
237244
exec.run_frontend_prod,
238245
exec.run_backend_prod,
239246
)
240-
if not setup_frontend or not frontend_cmd or not backend_cmd:
241-
msg = f"Invalid env: {env}. Must be DEV or PROD."
242-
raise ValueError(msg)
243247

244248
# Post a telemetry event.
245249
telemetry.send(f"run-{env.value}")
@@ -251,7 +255,7 @@ def _run(
251255
commands = []
252256

253257
# Run the frontend on a separate thread.
254-
if frontend:
258+
if frontend and not single_port:
255259
setup_frontend(Path.cwd())
256260
commands.append((frontend_cmd, Path.cwd(), frontend_port, backend))
257261

@@ -267,21 +271,30 @@ def _run(
267271
)
268272
)
269273

270-
# Start the frontend and backend.
271-
with processes.run_concurrently_context(*commands):
272-
# In dev mode, run the backend on the main thread.
273-
if backend and backend_port and env == constants.Env.DEV:
274-
backend_cmd(
275-
backend_host,
276-
int(backend_port),
277-
config.loglevel.subprocess_level(),
278-
frontend,
279-
)
280-
# The windows uvicorn bug workaround
281-
# https://github.com/reflex-dev/reflex/issues/2335
282-
if constants.IS_WINDOWS and exec.frontend_process:
283-
# Sends SIGTERM in windows
284-
exec.kill(exec.frontend_process.pid)
274+
if single_port:
275+
setup_frontend(Path.cwd())
276+
backend_function, *args = commands[0]
277+
exec.notify_app_running()
278+
exec.notify_frontend(
279+
f"http://0.0.0.0:{get_config().frontend_port}", backend_present=True
280+
)
281+
backend_function(*args, mount_frontend_compiled_app=True)
282+
else:
283+
# Start the frontend and backend.
284+
with processes.run_concurrently_context(*commands):
285+
# In dev mode, run the backend on the main thread.
286+
if backend and backend_port and env == constants.Env.DEV:
287+
backend_cmd(
288+
backend_host,
289+
int(backend_port),
290+
config.loglevel.subprocess_level(),
291+
frontend,
292+
)
293+
# The windows uvicorn bug workaround
294+
# https://github.com/reflex-dev/reflex/issues/2335
295+
if constants.IS_WINDOWS and exec.frontend_process:
296+
# Sends SIGTERM in windows
297+
exec.kill(exec.frontend_process.pid)
285298

286299

287300
@cli.command()
@@ -322,19 +335,43 @@ def _run(
322335
"--backend-host",
323336
help="Specify the backend host.",
324337
)
338+
@click.option(
339+
"--single-port",
340+
is_flag=True,
341+
help="Run both frontend and backend on the same port.",
342+
default=False,
343+
)
325344
def run(
326345
env: LITERAL_ENV,
327346
frontend_only: bool,
328347
backend_only: bool,
329348
frontend_port: int | None,
330349
backend_port: int | None,
331350
backend_host: str | None,
351+
single_port: bool,
332352
):
333353
"""Run the app in the current directory."""
334354
if frontend_only and backend_only:
335355
console.error("Cannot use both --frontend-only and --backend-only options.")
336356
raise click.exceptions.Exit(1)
337357

358+
if single_port:
359+
if env != constants.Env.PROD.value:
360+
console.error("--single-port can only be used with --env=PROD.")
361+
raise click.exceptions.Exit(1)
362+
if frontend_only or backend_only:
363+
console.error(
364+
"Cannot use --single-port with --frontend-only or --backend-only options."
365+
)
366+
raise click.exceptions.Exit(1)
367+
if backend_port and frontend_port and backend_port != frontend_port:
368+
console.error(
369+
"When using --single-port, --backend-port and --frontend-port must be the same."
370+
)
371+
raise click.exceptions.Exit(1)
372+
elif frontend_port and backend_port and frontend_port == backend_port:
373+
single_port = True
374+
338375
config = get_config()
339376

340377
frontend_port = frontend_port or config.frontend_port
@@ -352,6 +389,7 @@ def run(
352389
frontend_port,
353390
backend_port,
354391
backend_host,
392+
single_port,
355393
)
356394

357395

reflex/utils/exec.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ def kill(proc_pid: int):
145145
process.kill()
146146

147147

148+
def notify_frontend(url: str, backend_present: bool):
149+
"""Output a string notifying where the frontend is running."""
150+
console.print(
151+
f"App running at: [bold green]{url}[/bold green]{' (Frontend-only mode)' if not backend_present else ''}"
152+
)
153+
154+
148155
def notify_backend():
149156
"""Output a string notifying where the backend is running."""
150157
console.print(
@@ -210,9 +217,7 @@ def run_process_and_launch_url(
210217
if get_config().frontend_path != "":
211218
url = urljoin(url, get_config().frontend_path)
212219

213-
console.print(
214-
f"App running at: [bold green]{url}[/bold green]{' (Frontend-only mode)' if not backend_present else ''}"
215-
)
220+
notify_frontend(url, backend_present)
216221
if backend_present:
217222
notify_backend()
218223
first_run = False
@@ -249,6 +254,11 @@ def run_frontend(root: Path, port: str, backend_present: bool = True):
249254
)
250255

251256

257+
def notify_app_running():
258+
"""Notify that the app is running."""
259+
console.rule("[bold green]App Running")
260+
261+
252262
def run_frontend_prod(root: Path, port: str, backend_present: bool = True):
253263
"""Run the frontend.
254264
@@ -264,7 +274,7 @@ def run_frontend_prod(root: Path, port: str, backend_present: bool = True):
264274
# validate dependencies before run
265275
js_runtimes.validate_frontend_dependencies(init=False)
266276
# Run the frontend in production mode.
267-
console.rule("[bold green]App Running")
277+
notify_app_running()
268278
run_process_and_launch_url(
269279
[*js_runtimes.get_js_package_executor(raise_on_none=True)[0], "run", "prod"],
270280
backend_present,
@@ -552,6 +562,7 @@ def run_backend_prod(
552562
port: int,
553563
loglevel: constants.LogLevel = constants.LogLevel.ERROR,
554564
frontend_present: bool = False,
565+
mount_frontend_compiled_app: bool = False,
555566
):
556567
"""Run the backend.
557568
@@ -560,10 +571,13 @@ def run_backend_prod(
560571
port: The app port
561572
loglevel: The log level.
562573
frontend_present: Whether the frontend is present.
574+
mount_frontend_compiled_app: Whether to mount the compiled frontend app with the backend.
563575
"""
564576
if not frontend_present:
565577
notify_backend()
566578

579+
environment.REFLEX_MOUNT_FRONTEND_COMPILED_APP.set(mount_frontend_compiled_app)
580+
567581
if should_use_granian():
568582
run_granian_backend_prod(host, port, loglevel)
569583
else:

0 commit comments

Comments
 (0)