mirror of
https://github.com/evroon/bracket.git
synced 2026-05-19 05:59:34 -04:00
Allow serving frontend via backend (#1492)
This should make it easier to run Bracket in development/simple production environments because you only need to run 1 Docker container. Also, it avoids CORS problems because the frontend and backend run on the same domain.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,5 +15,6 @@ coverage.xml
|
||||
coverage.json
|
||||
|
||||
backend/static
|
||||
backend/frontend-dist
|
||||
|
||||
/process-compose.yml
|
||||
|
||||
50
Dockerfile
Normal file
50
Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
# Build static frontend files
|
||||
FROM node:24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY frontend .
|
||||
|
||||
RUN corepack enable && CI=true pnpm install && pnpm build
|
||||
|
||||
# Build backend image that also serves frontend (stored in `/app/frontend-dist`)
|
||||
FROM python:3.14-alpine3.22
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
RUN rm -rf /var/cache/apk/*
|
||||
|
||||
COPY backend /app
|
||||
WORKDIR /app
|
||||
|
||||
# -- Install dependencies:
|
||||
RUN addgroup --system bracket && \
|
||||
adduser --system bracket --ingroup bracket && \
|
||||
chown -R bracket:bracket /app
|
||||
USER bracket
|
||||
|
||||
RUN uv sync --no-dev --locked
|
||||
|
||||
COPY --from=builder /app/dist /app/frontend-dist
|
||||
|
||||
EXPOSE 8400
|
||||
|
||||
HEALTHCHECK --interval=3s --timeout=5s --retries=10 \
|
||||
CMD ["wget", "-O", "/dev/null", "http://0.0.0.0:8400/ping"]
|
||||
|
||||
CMD [ \
|
||||
"uv", \
|
||||
"run", \
|
||||
"--no-dev", \
|
||||
"--locked", \
|
||||
"--", \
|
||||
"gunicorn", \
|
||||
"-k", \
|
||||
"uvicorn.workers.UvicornWorker", \
|
||||
"bracket.app:app", \
|
||||
"--bind", \
|
||||
"0.0.0.0:8400", \
|
||||
"--workers", \
|
||||
"1" \
|
||||
]
|
||||
@@ -90,7 +90,7 @@ be able to view bracket at http://localhost:3000. You can log in with the follow
|
||||
|
||||
To insert dummy rows into the database, run:
|
||||
```bash
|
||||
sudo docker exec bracket-backend uv run ./cli.py create-dev-db
|
||||
docker exec bracket-backend uv run --no-dev ./cli.py create-dev-db
|
||||
```
|
||||
|
||||
See also the [quickstart docs](https://docs.bracketapp.nl/docs/running-bracket/quickstart).
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
FROM python:3.12-alpine3.17
|
||||
ARG packages
|
||||
RUN apk --update add ${packages} \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& pip3 install --upgrade pip uv wheel virtualenv
|
||||
FROM python:3.14-alpine3.22
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
RUN rm -rf /var/cache/apk/*
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
# -- Install dependencies:
|
||||
RUN addgroup --system bracket && adduser --system bracket --ingroup bracket \
|
||||
&& chown -R bracket:bracket /app
|
||||
RUN addgroup --system bracket && \
|
||||
adduser --system bracket --ingroup bracket && \
|
||||
chown -R bracket:bracket /app
|
||||
USER bracket
|
||||
|
||||
RUN set -ex \
|
||||
&& pip3 install --upgrade pip uv wheel virtualenv \
|
||||
&& uv sync --no-dev
|
||||
RUN uv sync --no-dev --locked
|
||||
|
||||
EXPOSE 8400
|
||||
|
||||
@@ -24,9 +22,15 @@ HEALTHCHECK --interval=3s --timeout=5s --retries=10 \
|
||||
CMD [ \
|
||||
"uv", \
|
||||
"run", \
|
||||
"--no-dev", \
|
||||
"--locked", \
|
||||
"--", \
|
||||
"gunicorn", \
|
||||
"-k", "uvicorn.workers.UvicornWorker", \
|
||||
"-k", \
|
||||
"uvicorn.workers.UvicornWorker", \
|
||||
"bracket.app:app", \
|
||||
"--bind", "0.0.0.0:8400", \
|
||||
"--workers", "1" \
|
||||
"--bind", \
|
||||
"0.0.0.0:8400", \
|
||||
"--workers", \
|
||||
"1" \
|
||||
]
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import glob
|
||||
import time
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import FileResponse
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.middleware.base import RequestResponseEndpoint
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
@@ -153,3 +156,23 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
for tag, router in routers.items():
|
||||
app.include_router(router, tags=[tag])
|
||||
|
||||
if config.serve_frontend:
|
||||
frontend_root = Path("frontend-dist").resolve()
|
||||
allowed_paths = list(glob.iglob("frontend-dist/**/*", recursive=True))
|
||||
|
||||
@app.get("/{full_path:path}")
|
||||
async def frontend(full_path: str) -> FileResponse:
|
||||
path = (frontend_root / Path(full_path)).resolve()
|
||||
|
||||
# Checking `str(path) in allowed_paths` should be enough here but we check for more cases
|
||||
# to be sure and avoid AI tools raising false positives.
|
||||
if (
|
||||
path.exists()
|
||||
and path.is_file()
|
||||
and str(path) in allowed_paths
|
||||
and frontend_root in path.parents
|
||||
):
|
||||
return FileResponse(path)
|
||||
|
||||
return FileResponse(frontend_root / Path("index.html"))
|
||||
|
||||
@@ -40,6 +40,7 @@ class Config(BaseSettings):
|
||||
auto_run_migrations: bool = True
|
||||
pg_dsn: PostgresDsn = PostgresDsn("postgresql://user:pass@localhost:5432/db")
|
||||
sentry_dsn: str | None = None
|
||||
serve_frontend: bool = False
|
||||
|
||||
def is_cors_enabled(self) -> bool:
|
||||
return self.cors_origins != "*"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build static files
|
||||
FROM node:22-alpine AS builder
|
||||
FROM node:24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Bracket</title>
|
||||
<meta charSet="UTF-8" />
|
||||
|
||||
Reference in New Issue
Block a user