Skip to content

Commit 7331abc

Browse files
authored
single port prod (#5839)
* single port prod * set that * few fixes for frontend path
1 parent e5d6c4d commit 7331abc

File tree

4 files changed

+99
-27
lines changed

4 files changed

+99
-27
lines changed

reflex/app.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,18 @@ 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+
"/" + config.frontend_path.strip("/"),
634+
StaticFiles(
635+
directory=prerequisites.get_web_dir()
636+
/ constants.Dirs.STATIC
637+
/ config.frontend_path.strip("/"),
638+
html=True,
639+
),
640+
name="frontend",
641+
)
642+
631643
if self.api_transformer is not None:
632644
api_transformers: Sequence[Starlette | Callable[[ASGIApp], ASGIApp]] = (
633645
[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 SystemExit(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: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,18 @@ 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+
151+
Args:
152+
url: The URL where the frontend is running.
153+
backend_present: Whether the backend is present.
154+
"""
155+
console.print(
156+
f"App running at: [bold green]{url.rstrip('/')}/[/bold green]{' (Frontend-only mode)' if not backend_present else ''}"
157+
)
158+
159+
148160
def notify_backend():
149161
"""Output a string notifying where the backend is running."""
150162
console.print(
@@ -210,9 +222,7 @@ def run_process_and_launch_url(
210222
if get_config().frontend_path != "":
211223
url = urljoin(url, get_config().frontend_path)
212224

213-
console.print(
214-
f"App running at: [bold green]{url}[/bold green]{' (Frontend-only mode)' if not backend_present else ''}"
215-
)
225+
notify_frontend(url, backend_present)
216226
if backend_present:
217227
notify_backend()
218228
first_run = False
@@ -249,6 +259,11 @@ def run_frontend(root: Path, port: str, backend_present: bool = True):
249259
)
250260

251261

262+
def notify_app_running():
263+
"""Notify that the app is running."""
264+
console.rule("[bold green]App Running")
265+
266+
252267
def run_frontend_prod(root: Path, port: str, backend_present: bool = True):
253268
"""Run the frontend.
254269
@@ -264,7 +279,7 @@ def run_frontend_prod(root: Path, port: str, backend_present: bool = True):
264279
# validate dependencies before run
265280
js_runtimes.validate_frontend_dependencies(init=False)
266281
# Run the frontend in production mode.
267-
console.rule("[bold green]App Running")
282+
notify_app_running()
268283
run_process_and_launch_url(
269284
[*js_runtimes.get_js_package_executor(raise_on_none=True)[0], "run", "prod"],
270285
backend_present,
@@ -552,6 +567,7 @@ def run_backend_prod(
552567
port: int,
553568
loglevel: constants.LogLevel = constants.LogLevel.ERROR,
554569
frontend_present: bool = False,
570+
mount_frontend_compiled_app: bool = False,
555571
):
556572
"""Run the backend.
557573
@@ -560,10 +576,13 @@ def run_backend_prod(
560576
port: The app port
561577
loglevel: The log level.
562578
frontend_present: Whether the frontend is present.
579+
mount_frontend_compiled_app: Whether to mount the compiled frontend app with the backend.
563580
"""
564581
if not frontend_present:
565582
notify_backend()
566583

584+
environment.REFLEX_MOUNT_FRONTEND_COMPILED_APP.set(mount_frontend_compiled_app)
585+
567586
if should_use_granian():
568587
run_granian_backend_prod(host, port, loglevel)
569588
else:

0 commit comments

Comments
 (0)