Forráskód Böngészése

docker-example overhaul (#2690)

* docker-example overhaul

Update docker-example with a more realistic multi-compose deployment, and
also a more simplistic single-image deploy.

* Use `uv` for faster bootstrapping
* Separate simple and production-ready docker files
* Split compose.yaml into 3 parts
  * Persist sqlite db and tls keys
  * Include postgres and redis
  * Include Adminer and redis-commander for adminstration
  * Suppose upload persistence
* Update documentation
* Update Caddyfile for compatibility with new Upload API

* Simple Dockerfile: keep `reflex export --frontend-only`

Pre-pack the resulting image with npm dependencies to reduce startup time

* Simplify simple docker file to just use pip
Masen Furer 1 éve
szülő
commit
e8faec708e

+ 7 - 1
docker-example/.dockerignore

@@ -1,2 +1,8 @@
 .web
-__pycache__/*
+.git
+__pycache__/*
+Dockerfile
+Caddy.Dockerfile
+compose.yaml
+compose.*.yaml
+uploaded_files

+ 1 - 1
docker-example/Caddyfile

@@ -2,7 +2,7 @@
 
 encode gzip
 
-@backend_routes path /_event/* /_upload /ping
+@backend_routes path /_event/* /ping /_upload /_upload/*
 handle @backend_routes {
 	reverse_proxy app:8000
 }

+ 8 - 26
docker-example/Dockerfile

@@ -1,39 +1,21 @@
-# Stage 1: init
-FROM python:3.11 as init
-
-# Pass `--build-arg API_URL=http://app.example.com:8000` during build
-ARG API_URL
+# This Dockerfile is used to deploy a simple single-container Reflex app instance.
+FROM python:3.11
 
 # Copy local context to `/app` inside container (see .dockerignore)
 WORKDIR /app
 COPY . .
 
-# Create virtualenv which will be copied into final container
-ENV VIRTUAL_ENV=/app/.venv
-ENV PATH="$VIRTUAL_ENV/bin:$PATH"
-RUN python3.11 -m venv $VIRTUAL_ENV
-
-# Install app requirements and reflex inside virtualenv
+# Install app requirements and reflex in the container
 RUN pip install -r requirements.txt
 
 # Deploy templates and prepare app
 RUN reflex init
 
-# Export static copy of frontend to /app/.web/_static
+# Download all npm dependencies and compile and frontend
 RUN reflex export --frontend-only --no-zip
 
-# Copy static files out of /app to save space in backend image
-RUN mv .web/_static /tmp/_static
-RUN rm -rf .web && mkdir .web
-RUN mv /tmp/_static .web/_static
-
-# Stage 2: copy artifacts into slim image 
-FROM python:3.11-slim
-ARG API_URL
-WORKDIR /app
-RUN adduser --disabled-password --home /app reflex
-COPY --chown=reflex --from=init /app /app
-USER reflex
-ENV PATH="/app/.venv/bin:$PATH" API_URL=$API_URL
+# Needed until Reflex properly passes SIGTERM on backend.
+STOPSIGNAL SIGKILL
 
-CMD reflex db migrate && reflex run --env prod --backend-only
+# Always apply migrations before starting the backend.
+CMD [ -d alembic ] && reflex db migrate; reflex run --env prod

+ 66 - 13
docker-example/README.md

@@ -9,36 +9,59 @@ Reflex framework. If you use additional packages in your project you have to add
 this in the `requirements.txt` first. Copy the `Dockerfile`, `.dockerignore` and
 the `requirements.txt` file in your project folder.
 
-## Build Reflex Container Image
+## Build Simple Reflex Container Image
+
+The main `Dockerfile` is intended to build a very simple, single container deployment that runs
+the Reflex frontend and backend together, exposing ports 3000 and 8000.
 
 To build your container image run the following command:
 
 ```bash
-docker build -t reflex-app:latest . --build-arg API_URL=http://app.example.com:8000
+docker build -t reflex-app:latest .
 ```
 
-Ensure that `API_URL` is set to the publicly accessible hostname or IP where the app
-will be hosted.
-
 ## Start Container Service
 
 Finally, you can start your Reflex container service as follows:
 
 ```bash
-docker run -p 3000:3000 -p 8000:8000 --name app reflex-app:latest
+docker run -it --rm -p 3000:3000 -p 8000:8000 --name app reflex-app:latest
 ```
 
 It may take a few seconds for the service to become available.
 
+Access your app at http://localhost:3000.
+
+Note that this container has _no persistence_ and will lose all data when
+stopped. You can use bind mounts or named volumes to persist the database and
+uploaded_files directories as needed.
+
 # Production Service with Docker Compose and Caddy
 
 An example production deployment uses automatic TLS with Caddy serving static files
 for the frontend and proxying requests to both the frontend and backend.
 
-Copy `compose.yaml`, `Caddy.Dockerfile` and `Caddyfile` to your project directory. The production
-build leverages the same `Dockerfile` described above.
+Copy the following files to your project directory:
+  * `compose.yaml`
+  * `compose.prod.yaml`
+  * `compose.tools.yaml`
+  * `prod.Dockerfile`
+  * `Caddy.Dockerfile`
+  * `Caddyfile`
+
+The production app container, based on `prod.Dockerfile`, builds and exports the
+frontend statically (to be served by Caddy). The resulting image only runs the
+backend service.
 
-## Customize `Caddyfile`
+The `webserver` service, based on `Caddy.Dockerfile`, copies the static frontend
+and `Caddyfile` into the container to configure the reverse proxy routes that will
+forward requests to the backend service. Caddy will automatically provision TLS
+for localhost or the domain specified in the environment variable `DOMAIN`.
+
+This type of deployment should use less memory and be more performant since
+nodejs is not required at runtime.
+
+## Customize `Caddyfile` (optional)
 
 If the app uses additional backend API routes, those should be added to the
 `@backend_routes` path matcher to ensure they are forwarded to the backend.
@@ -46,15 +69,16 @@ If the app uses additional backend API routes, those should be added to the
 ## Build Reflex Production Service
 
 During build, set `DOMAIN` environment variable to the domain where the app will
-be hosted!  (Do not include http or https, it will always use https)
+be hosted!  (Do not include http or https, it will always use https).
+
+**If `DOMAIN` is not provided, the service will default to `localhost`.**
 
 ```bash
 DOMAIN=example.com docker compose build
 ```
 
-This will build both the `app` service from the existing `Dockerfile` and the `webserver`
-service via `Caddy.Dockerfile` that copies the `Caddyfile` and static frontend export
-from the `app` service into the container.
+This will build both the `app` service from the `prod.Dockerfile` and the `webserver`
+service via `Caddy.Dockerfile`.
 
 ## Run Reflex Production Service
 
@@ -64,3 +88,32 @@ DOMAIN=example.com docker compose up
 
 The app should be available at the specified domain via HTTPS. Certificate
 provisioning will occur automatically and may take a few minutes.
+
+### Data Persistence
+
+Named docker volumes are used to persist the app database (`db-data`),
+uploaded_files (`upload-data`), and caddy TLS keys and certificates
+(`caddy-data`).
+
+## More Robust Deployment
+
+For a more robust deployment, consider bringing the service up with
+`compose.prod.yaml` which includes postgres database and redis cache, allowing
+the backend to run with multiple workers and service more requests.
+
+```bash
+DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml up -d
+```
+
+Postgres uses its own named docker volume for data persistence.
+
+## Admin Tools
+
+When needed, the services in `compose.tools.yaml` can be brought up, providing
+graphical database administration (Adminer on http://localhost:8080) and a
+redis cache browser (redis-commander on http://localhost:8081). It is not recommended
+to deploy these services if they are not in active use.
+
+```bash
+DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml -f compose.tools.yaml up -d
+```

+ 25 - 0
docker-example/compose.prod.yaml

@@ -0,0 +1,25 @@
+# Use this override file to run the app in prod mode with postgres and redis
+#     docker compose -f compose.yaml -f compose.prod.yaml up -d
+services:
+  db:
+    image: postgres
+    restart: always
+    environment:
+      POSTGRES_PASSWORD: secret
+    volumes:
+       - postgres-data:/var/lib/postgresql/data
+
+  redis:
+    image: redis
+    restart: always
+
+  app:
+    environment:
+      DB_URL: postgresql+psycopg2://postgres:secret@db/postgres
+      REDIS_URL: redis://redis:6379
+    depends_on:
+      - db
+      - redis
+
+volumes:
+  postgres-data:

+ 18 - 0
docker-example/compose.tools.yaml

@@ -0,0 +1,18 @@
+# Use this override file with `compose.prod.yaml` to run admin tools
+# for production services.
+#     docker compose -f compose.yaml -f compose.prod.yaml -f compose.tools.yaml up -d
+services:
+  adminer:
+    image: adminer
+    ports:
+      - 8080:8080
+
+  redis-commander:
+    image: ghcr.io/joeferner/redis-commander:latest
+    environment:
+    - REDIS_HOSTS=local:redis:6379
+    ports:
+    - "8081:8081"
+
+volumes:
+  redis-ui-settings:

+ 25 - 4
docker-example/compose.yaml

@@ -1,21 +1,42 @@
+# Base compose file production deployment of reflex app with Caddy webserver
+# providing TLS termination and reverse proxying.
+#
+# See `compose.prod.yaml` for more robust and performant deployment option.
+#
 # During build and run, set environment DOMAIN pointing
 # to publicly accessible domain where app will be hosted
 services:
   app:
     image: local/reflex-app
+    environment:
+      DB_URL: sqlite:///data/reflex.db
     build:
       context: .
-      args:
-        API_URL: https://${DOMAIN:-localhost}
+      dockerfile: prod.Dockerfile
+    volumes:
+       - db-data:/app/data
+       - upload-data:/app/uploaded_files
+    restart: always
 
   webserver:
     environment:
       DOMAIN: ${DOMAIN:-localhost}
     ports:
       - 443:443
-      - 80:80  # for acme-challenge via HTTP
+      - 80:80  # For acme-challenge via HTTP.
     build:
       context: .
       dockerfile: Caddy.Dockerfile
+    volumes:
+       - caddy-data:/root/.caddy
+    restart: always
     depends_on:
-      - app
+      - app
+
+volumes:
+  # SQLite data
+  db-data:
+  # Uploaded files
+  upload-data:
+  # TLS keys and certificates
+  caddy-data:

+ 51 - 0
docker-example/prod.Dockerfile

@@ -0,0 +1,51 @@
+# This docker file is intended to be used with docker compose to deploy a production
+# instance of a Reflex app.
+
+# Stage 1: init
+FROM python:3.11 as init
+
+ARG uv=/root/.cargo/bin/uv
+
+# Install `uv` for faster package boostrapping
+ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh
+RUN /install.sh && rm /install.sh
+
+# Copy local context to `/app` inside container (see .dockerignore)
+WORKDIR /app
+COPY . .
+RUN mkdir -p /app/data /app/uploaded_files
+
+# Create virtualenv which will be copied into final container
+ENV VIRTUAL_ENV=/app/.venv
+ENV PATH="$VIRTUAL_ENV/bin:$PATH"
+RUN $uv venv
+
+# Install app requirements and reflex inside virtualenv
+RUN $uv pip install -r requirements.txt
+
+# Deploy templates and prepare app
+RUN reflex init
+
+# Export static copy of frontend to /app/.web/_static
+RUN reflex export --frontend-only --no-zip
+
+# Copy static files out of /app to save space in backend image
+RUN mv .web/_static /tmp/_static
+RUN rm -rf .web && mkdir .web
+RUN mv /tmp/_static .web/_static
+
+# Stage 2: copy artifacts into slim image 
+FROM python:3.11-slim
+WORKDIR /app
+RUN adduser --disabled-password --home /app reflex
+COPY --chown=reflex --from=init /app /app
+# Install libpq-dev for psycopg2 (skip if not using postgres).
+RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
+USER reflex
+ENV PATH="/app/.venv/bin:$PATH"
+
+# Needed until Reflex properly passes SIGTERM on backend.
+STOPSIGNAL SIGKILL
+
+# Always apply migrations before starting the backend.
+CMD reflex db migrate && reflex run --env prod --backend-only