diff --git a/.gitignore b/.gitignore index a825459902..452db68f49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Dependencies +/.venv /venv # Generated files diff --git a/docs/conf.py b/docs/conf.py index ce9fd132d1..042c8e4da4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,6 +4,7 @@ # -- Path setup -------------------------------------------------------------- +import re from datetime import datetime # If extensions (or modules to document with autodoc) are in another directory, @@ -316,6 +317,17 @@ "substitution", # Use Jinja2 for substitutions. https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#substitutions-with-jinja2 ] +# Versions of the container images used in the documentation. +# Use them as MyST substitutions in text, such as {{PLONE_BACKEND_MINOR_VERSION}}, +# and as source replacements in code blocks, such as {PLONE_BACKEND_MINOR_VERSION}. +container_image_versions = { + "PLONE_BACKEND_MINOR_VERSION": "6.2", + "PLONE_FRONTEND_VERSION": "19", + "PLONE_ZEO_VERSION": "6", + "TRAEFIK_VERSION": "v3.7", + "POSTGRES_VERSION": "18", +} + myst_substitutions = { "postman_basic_auth": "![](../_static/img/postman_basic_auth.png)", "postman_headers": "![](../_static/img/postman_headers.png)", @@ -326,6 +338,7 @@ "SUPPORTED_PYTHON_VERSIONS_PLONE60": "3.9, 3.10, 3.11, 3.12, or 3.13", "SUPPORTED_PYTHON_VERSIONS_PLONE61": "3.10, 3.11, 3.12, or 3.13", "SUPPORTED_PYTHON_VERSIONS_PLONE62": "3.10, 3.11, 3.12, 3.13, or 3.14", + **container_image_versions, } @@ -467,14 +480,16 @@ # https://stackoverflow.com/a/56328457/2214933 def source_replace(app, docname, source): result = source[0] - for key in app.config.source_replacements: - result = result.replace(key, app.config.source_replacements[key]) + for key, value in app.config.source_replacements.items(): + # Skip MyST substitutions, such as {{KEY}}, which contain the key {KEY}. + pattern = rf"(?` | Stack with Traefik, Frontend, and Backend | +| {doc}`traefik-volto-plone-zeo ` | Stack with Traefik, Frontend, Backend, and ZEO server | +| {doc}`traefik-volto-plone-postgresql ` | Stack with Traefik, Frontend, Backend, and PostgreSQL DB | +| {doc}`traefik-plone ` | Stack with Traefik and Backend (Plone Classic) | +| {doc}`traefik-volto-plone-varnish ` | Stack with Traefik, Frontend, Backend, ZEO server, and Varnish | + +## nginx + +| Project example | Description | +| --- | --- | +| {doc}`nginx-volto-plone ` | Stack with nginx, Frontend, and Backend | +| {doc}`nginx-volto-plone-zeo ` | Stack with nginx, Frontend, Backend, and ZEO server | +| {doc}`nginx-volto-plone-postgresql ` | Stack with nginx, Frontend, Backend, and PostgreSQL DB | +| {doc}`nginx-plone ` | Stack with nginx and Backend (Plone Classic) | + +## HAProxy + +| Project example | Description | +| --- | --- | +| {doc}`haproxy-plone-zeo ` | Stack with HAProxy, Backend, and ZEO server | diff --git a/docs/install/containers/examples/nginx-plone.md b/docs/install/containers/examples/compose/nginx-plone.md similarity index 62% rename from docs/install/containers/examples/nginx-plone.md rename to docs/install/containers/examples/compose/nginx-plone.md index 9cac06c9a8..4259e70f1b 100644 --- a/docs/install/containers/examples/nginx-plone.md +++ b/docs/install/containers/examples/compose/nginx-plone.md @@ -1,8 +1,8 @@ --- myst: html_meta: - "description": "Simple Plone 6 setup with one backend and data being persisted in a Docker volume." - "property=og:description": "Simple Plone 6 setup with one backend and data being persisted in a Docker volume." + "description": "nginx and a Classic UI backend in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:description": "nginx and a Classic UI backend in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." "property=og:title": "nginx, Plone Classic container example" "keywords": "Plone 6, Container, Docker, nginx, Plone Classic" --- @@ -16,7 +16,7 @@ This example is a simple setup with one backend and data being persisted in a Do ## Setup -Create an empty project directory named `nginx-plone`. +Create an empty project directory named {file}`nginx-plone`. ```shell mkdir nginx-plone @@ -31,7 +31,7 @@ cd nginx-plone ### nginx configuration -Add a `default.conf` that will be used by the nginx image: +Add a {file}`default.conf` that will be used by the nginx image: ```nginx upstream backend { @@ -61,12 +61,12 @@ server { ```{note} `http://plone.localhost/` is the URL you will be using to access the website. -You can either use `plone.localhost`, or add it in your `/etc/hosts` file or DNS, to point to the Docker host IP. +You can either use `plone.localhost`, or add it in your {file}`/etc/hosts` file or DNS, to point to the Docker host IP. ``` ### Service configuration with Docker Compose -Now let's create a `docker-compose.yml` file: +Now let's create a {file}`docker-compose.yml` file: ```yaml services: @@ -81,17 +81,34 @@ services: - "80:80" backend: - image: plone/plone-backend:{PLONE_BACKEND_MINOR_VERSION} + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} environment: SITE: Plone TYPE: classic volumes: - - data:/data + - vol-site-data:/data ports: - "8080:8080" volumes: - data: {} + vol-site-data: {} +``` + + +### Environment variables + +The {file}`docker-compose.yml` file reads the tags of its images from the following environment variables. +All of them are required, and `docker compose` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | + +Create a {file}`.env` file in your project directory with your values. +Docker Compose reads it automatically when you run `docker compose` from that directory. + +```shell +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} ``` diff --git a/docs/install/containers/examples/nginx-volto-plone-postgresql.md b/docs/install/containers/examples/compose/nginx-volto-plone-postgresql.md similarity index 61% rename from docs/install/containers/examples/nginx-volto-plone-postgresql.md rename to docs/install/containers/examples/compose/nginx-volto-plone-postgresql.md index c568ed4917..ef739140bf 100644 --- a/docs/install/containers/examples/nginx-volto-plone-postgresql.md +++ b/docs/install/containers/examples/compose/nginx-volto-plone-postgresql.md @@ -1,22 +1,22 @@ --- myst: html_meta: - "description": "Very simple Plone 6 setup with only one or more backend instances accessing a PostgreSQL server and data being persisted in a Docker volume." - "property=og:description": "Very simple Plone 6 setup with only one or more backend instances accessing a PostgreSQL server and data being persisted in a Docker volume." + "description": "PostgreSQL server, nginx, a frontend, and one or more backend instances in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:description": "PostgreSQL server, nginx, a frontend, and one or more backend instances in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." "property=og:title": "nginx, Frontend, Backend, PostgreSQL container example" - "keywords": "Plone 6, Container, Docker, nginx, Frontend, Backend, PostgreSQL, " + "keywords": "Plone 6, Container, Docker, nginx, Frontend, Backend, PostgreSQL" --- # nginx, Frontend, Backend, PostgreSQL container example -This example is a very simple setup with one or more backend instances accessing a Postgres server and data being persisted in a Docker volume. +This example is a very simple setup with one or more backend instances accessing a PostgreSQL server and data being persisted in a Docker volume. {term}`nginx` in this example is used as a [reverse proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/). ## Setup -Create an empty project directory named `nginx-volto-plone-postgresql`. +Create an empty project directory named {file}`nginx-volto-plone-postgresql`. ```shell mkdir nginx-volto-plone-postgresql @@ -31,7 +31,7 @@ cd nginx-volto-plone-postgresql ### nginx configuration -Add a `default.conf` that will be used by the nginx image: +Add a {file}`default.conf` that will be used by the nginx image: ```nginx upstream backend { @@ -74,13 +74,13 @@ server { ```{note} `http://plone.localhost/` is the URL you will be using to access the website. -You can either use `localhost`, or add it in your `/etc/hosts` file or DNS to point to the Docker host IP. +You can either use `localhost`, or add it in your {file}`/etc/hosts` file or DNS to point to the Docker host IP. ``` ### Service configuration with Docker Compose -Now let's create a `docker-compose.yml` file: +Now let's create a {file}`docker-compose.yml` file: ```yaml services: @@ -96,7 +96,7 @@ services: - "80:80" frontend: - image: plone/plone-frontend:latest + image: plone/plone-frontend:${STACK_FRONTEND_TAG:?Set STACK_FRONTEND_TAG} environment: RAZZLE_INTERNAL_API_PATH: http://backend:8080/Plone ports: @@ -105,7 +105,7 @@ services: - backend backend: - image: plone/plone-backend:{PLONE_BACKEND_MINOR_VERSION} + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} environment: SITE: Plone RELSTORAGE_DSN: "dbname='plone' user='plone' host='db' password='plone'" @@ -115,18 +115,37 @@ services: - db db: - image: postgres + image: postgres:${STACK_POSTGRES_TAG:?Set STACK_POSTGRES_TAG} environment: POSTGRES_USER: plone POSTGRES_PASSWORD: plone POSTGRES_DB: plone volumes: - - data:/var/lib/postgresql/data - ports: - - "5432:5432" + - vol-site-data:/var/lib/postgresql volumes: - data: {} + vol-site-data: {} +``` + + +### Environment variables + +The {file}`docker-compose.yml` file reads the tags of its images from the following environment variables. +All of them are required, and `docker compose` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_FRONTEND_TAG` | Tag (version) of the image for the frontend | | {{PLONE_FRONTEND_VERSION}} | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | +| `STACK_POSTGRES_TAG` | Tag (version) of the image for PostgreSQL | | {{POSTGRES_VERSION}} | + +Create a {file}`.env` file in your project directory with your values. +Docker Compose reads it automatically when you run `docker compose` from that directory. + +```shell +STACK_FRONTEND_TAG={PLONE_FRONTEND_VERSION} +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} +STACK_POSTGRES_TAG={POSTGRES_VERSION} ``` diff --git a/docs/install/containers/examples/nginx-volto-plone-zeo.md b/docs/install/containers/examples/compose/nginx-volto-plone-zeo.md similarity index 62% rename from docs/install/containers/examples/nginx-volto-plone-zeo.md rename to docs/install/containers/examples/compose/nginx-volto-plone-zeo.md index 06102d25a6..fdb58eac76 100644 --- a/docs/install/containers/examples/nginx-volto-plone-zeo.md +++ b/docs/install/containers/examples/compose/nginx-volto-plone-zeo.md @@ -1,8 +1,8 @@ --- myst: html_meta: - "description": "Very simple Plone 6 setup with only one or more backend instances accessing a ZEO server and data being persisted in a Docker volume." - "property=og:description": "Very simple Plone 6 setup with only one or more backend instances accessing a ZEO server and data being persisted in a Docker volume." + "description": "ZEO server, nginx, a frontend, and one or more backend instances in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:description": "ZEO server, nginx, a frontend, and one or more backend instances in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." "property=og:title": "nginx, Frontend, Backend, ZEO container example" "keywords": "Plone 6, Container, Docker, nginx, Frontend, Backend, ZEO" --- @@ -16,7 +16,7 @@ This example is a very simple setup with one or more backend instances accessing ## Setup -Create an empty project directory named `nginx-volto-plone-zeo`. +Create an empty project directory named {file}`nginx-volto-plone-zeo`. ```shell mkdir nginx-volto-plone-zeo @@ -31,7 +31,7 @@ cd nginx-volto-plone-zeo ### nginx configuration -Add a `default.conf` that will be used by the nginx image: +Add a {file}`default.conf` that will be used by the nginx image: ```nginx upstream backend { @@ -74,13 +74,13 @@ server { ```{note} `http://plone.localhost/` is the URL you will be using to access the website. -You can either use `localhost`, or add it in your `/etc/hosts` file or DNS to point to the Docker host IP. +You can either use `localhost`, or add it in your {file}`/etc/hosts` file or DNS to point to the Docker host IP. ``` ### Service configuration with Docker Compose -Now let's create a `docker-compose.yml` file: +Now let's create a {file}`docker-compose.yml` file: ```yaml services: @@ -96,7 +96,7 @@ services: - "80:80" frontend: - image: plone/plone-frontend:latest + image: plone/plone-frontend:${STACK_FRONTEND_TAG:?Set STACK_FRONTEND_TAG} environment: RAZZLE_INTERNAL_API_PATH: http://backend:8080/Plone ports: @@ -105,28 +105,47 @@ services: - backend backend: - image: plone/plone-backend:{PLONE_BACKEND_MINOR_VERSION} + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} environment: SITE: Plone ZEO_ADDRESS: db:8100 ZEO_SHARED_BLOB_DIR: on # otherwise the backend will create its own blob storage volumes: - - data:/data # the backend and database need access to the same volume + - vol-site-data:/data # the backend and database need access to the same volume ports: - "8080:8080" depends_on: - db db: - image: plone/plone-zeo:latest + image: plone/plone-zeo:${STACK_ZEO_TAG:?Set STACK_ZEO_TAG} restart: always volumes: - - data:/data - ports: - - "8100:8100" + - vol-site-data:/data volumes: - data: {} + vol-site-data: {} +``` + + +### Environment variables + +The {file}`docker-compose.yml` file reads the tags of its images from the following environment variables. +All of them are required, and `docker compose` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_FRONTEND_TAG` | Tag (version) of the image for the frontend | | {{PLONE_FRONTEND_VERSION}} | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | +| `STACK_ZEO_TAG` | Tag (version) of the image for the ZEO server | | {{PLONE_ZEO_VERSION}} | + +Create a {file}`.env` file in your project directory with your values. +Docker Compose reads it automatically when you run `docker compose` from that directory. + +```shell +STACK_FRONTEND_TAG={PLONE_FRONTEND_VERSION} +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} +STACK_ZEO_TAG={PLONE_ZEO_VERSION} ``` diff --git a/docs/install/containers/examples/nginx-volto-plone.md b/docs/install/containers/examples/compose/nginx-volto-plone.md similarity index 63% rename from docs/install/containers/examples/nginx-volto-plone.md rename to docs/install/containers/examples/compose/nginx-volto-plone.md index 4955d6119b..cec2cd7861 100644 --- a/docs/install/containers/examples/nginx-volto-plone.md +++ b/docs/install/containers/examples/compose/nginx-volto-plone.md @@ -1,8 +1,8 @@ --- myst: html_meta: - "description": "Very simple Plone 6 setup with only one backend and data being persisted in a Docker volume." - "property=og:description": "Very simple Plone 6 setup with only one backend and data being persisted in a Docker volume." + "description": "nginx, a frontend, and a single backend in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:description": "nginx, a frontend, and a single backend in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." "property=og:title": "nginx, Frontend, Backend container example" "keywords": "Plone 6, Container, Docker, nginx, Frontend, Backend" --- @@ -15,7 +15,7 @@ This example is a very simple setup with one backend and data being persisted in ## Setup -Create an empty project directory named `nginx-volto-plone` +Create an empty project directory named {file}`nginx-volto-plone` ```shell mkdir nginx-volto-plone @@ -30,7 +30,7 @@ cd nginx-volto-plone ### nginx configuration -Add a `default.conf` that will be used by the nginx image: +Add a {file}`default.conf` that will be used by the nginx image: ```nginx upstream backend { @@ -73,12 +73,12 @@ server { ```{note} `http://plone.localhost/` is the URL you will be using to access the website. -You can either use `localhost`, or add it in your `/etc/hosts` file or DNS to point to the Docker host IP. +You can either use `localhost`, or add it in your {file}`/etc/hosts` file or DNS to point to the Docker host IP. ``` ### Service configuration with Docker Compose -Now let's create a `docker-compose.yml` file: +Now let's create a {file}`docker-compose.yml` file: ```yaml services: @@ -94,7 +94,7 @@ services: - "80:80" frontend: - image: plone/plone-frontend:latest + image: plone/plone-frontend:${STACK_FRONTEND_TAG:?Set STACK_FRONTEND_TAG} environment: RAZZLE_INTERNAL_API_PATH: http://backend:8080/Plone ports: @@ -103,16 +103,35 @@ services: - backend backend: - image: plone/plone-backend:{PLONE_BACKEND_MINOR_VERSION} + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} environment: SITE: Plone volumes: - - data:/data + - vol-site-data:/data ports: - "8080:8080" volumes: - data: {} + vol-site-data: {} +``` + + +### Environment variables + +The {file}`docker-compose.yml` file reads the tags of its images from the following environment variables. +All of them are required, and `docker compose` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_FRONTEND_TAG` | Tag (version) of the image for the frontend | | {{PLONE_FRONTEND_VERSION}} | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | + +Create a {file}`.env` file in your project directory with your values. +Docker Compose reads it automatically when you run `docker compose` from that directory. + +```shell +STACK_FRONTEND_TAG={PLONE_FRONTEND_VERSION} +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} ``` diff --git a/docs/install/containers/examples/compose/traefik-plone.md b/docs/install/containers/examples/compose/traefik-plone.md new file mode 100644 index 0000000000..9d2b037d92 --- /dev/null +++ b/docs/install/containers/examples/compose/traefik-plone.md @@ -0,0 +1,124 @@ +--- +myst: + html_meta: + "description": "Traefik and a Classic UI backend in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:description": "Traefik and a Classic UI backend in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:title": "Traefik, Plone Classic container example" + "keywords": "Plone 6, Container, Docker, Traefik, Plone Classic" +--- + +# Traefik, Plone Classic container example + +This example is a simple setup with one backend, with data persisted in a Docker volume. + +In this example, {term}`Traefik Proxy` routes requests to the backend. + + +## Setup + +Create an empty project directory named {file}`traefik-plone`. + +```shell +mkdir traefik-plone +``` + +Change into your project directory. + +```shell +cd traefik-plone +``` + + +### Service configuration with Docker Compose + +Create a {file}`docker-compose.yml` file with the following content. +Traefik reads its routing configuration from the labels of the `backend` service, so this example doesn't need a separate proxy configuration file. + +```yaml +services: + + traefik: + image: traefik:${STACK_TRAEFIK_TAG:?Set STACK_TRAEFIK_TAG} + ports: + - "80:80" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + command: + - --providers.docker + - --providers.docker.exposedbydefault=false + - --entrypoints.http.address=:80 + - --accesslog + + backend: + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} + environment: + SITE: Plone + TYPE: classic + volumes: + - vol-site-data:/data + ports: + - "8080:8080" + labels: + - traefik.enable=true + # Service + - traefik.http.services.svc-backend.loadbalancer.server.port=8080 + # Middleware: Virtual Host Monster rewrite for the site + - "traefik.http.middlewares.mw-backend-vhm.replacepathregex.regex=^/(.*)" + - "traefik.http.middlewares.mw-backend-vhm.replacepathregex.replacement=/VirtualHostBase/http/plone.localhost/Plone/VirtualHostRoot/$$1" + # Router + - traefik.http.routers.rt-backend.rule=Host(`plone.localhost`) + - traefik.http.routers.rt-backend.entrypoints=http + - traefik.http.routers.rt-backend.service=svc-backend + - traefik.http.routers.rt-backend.middlewares=mw-backend-vhm + +volumes: + vol-site-data: {} +``` + +```{note} +Use `http://plone.localhost/` to access the website. +If `plone.localhost` doesn't resolve on your computer, add it to your {file}`/etc/hosts` file, pointing to the IP address of the Docker host. +``` + + +### Environment variables + +The {file}`docker-compose.yml` file reads the tags of its images from the following environment variables. +All of them are required, and `docker compose` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | +| `STACK_TRAEFIK_TAG` | Tag (version) of the image for Traefik | | {{TRAEFIK_VERSION}} | + +Create a {file}`.env` file in your project directory with your values. +Docker Compose reads it automatically when you run `docker compose` from that directory. + +```shell +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} +STACK_TRAEFIK_TAG={TRAEFIK_VERSION} +``` + + +## Build the project + +Start the stack with `docker compose`. + +```shell +docker compose up -d +``` + +This pulls the needed images and starts Plone. + + +## Access Plone in a browser + +After startup, go to `http://plone.localhost/` and you should see the site. +You can also open the main Plone control page, where you can create more Plone sites, at `http://localhost:8080`. + + +## Shutdown and cleanup + +The command `docker compose down` removes the containers and default network, but preserves the Plone database. + +The command `docker compose down --volumes` removes the containers, default network, and the Plone database. diff --git a/docs/install/containers/examples/compose/traefik-volto-plone-postgresql.md b/docs/install/containers/examples/compose/traefik-volto-plone-postgresql.md new file mode 100644 index 0000000000..3fab9e710f --- /dev/null +++ b/docs/install/containers/examples/compose/traefik-volto-plone-postgresql.md @@ -0,0 +1,160 @@ +--- +myst: + html_meta: + "description": "PostgreSQL server, Traefik, a frontend, and one or more backend instances in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:description": "PostgreSQL server, Traefik, a frontend, and one or more backend instances in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:title": "Traefik, Frontend, Backend, PostgreSQL container example" + "keywords": "Plone 6, Container, Docker, Traefik, Frontend, Backend, PostgreSQL" +--- + +# Traefik, Frontend, Backend, PostgreSQL container example + +This example is a simple setup with one or more backend instances accessing a PostgreSQL server, with data persisted in a Docker volume. + +In this example, {term}`Traefik Proxy` routes requests to the frontend and the backend. + + +## Setup + +Create an empty project directory named {file}`traefik-volto-plone-postgresql`. + +```shell +mkdir traefik-volto-plone-postgresql +``` + +Change into your project directory. + +```shell +cd traefik-volto-plone-postgresql +``` + + +### Service configuration with Docker Compose + +Create a {file}`docker-compose.yml` file with the following content. +Traefik reads its routing configuration from the labels of the `frontend` and `backend` services, so this example doesn't need a separate proxy configuration file. + +```yaml +services: + + traefik: + image: traefik:${STACK_TRAEFIK_TAG:?Set STACK_TRAEFIK_TAG} + ports: + - "80:80" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + command: + - --providers.docker + - --providers.docker.exposedbydefault=false + - --entrypoints.http.address=:80 + - --accesslog + + frontend: + image: plone/plone-frontend:${STACK_FRONTEND_TAG:?Set STACK_FRONTEND_TAG} + environment: + RAZZLE_INTERNAL_API_PATH: http://backend:8080/Plone + depends_on: + - backend + labels: + - traefik.enable=true + # Service + - traefik.http.services.svc-frontend.loadbalancer.server.port=3000 + # Router + - traefik.http.routers.rt-frontend.rule=Host(`plone.localhost`) + - traefik.http.routers.rt-frontend.entrypoints=http + - traefik.http.routers.rt-frontend.service=svc-frontend + + backend: + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} + environment: + SITE: Plone + RELSTORAGE_DSN: "dbname='plone' user='plone' host='db' password='plone'" + depends_on: + - db + labels: + - traefik.enable=true + # Service + - traefik.http.services.svc-backend.loadbalancer.server.port=8080 + # Middleware: Virtual Host Monster rewrite for /++api++/ + - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.regex=^/\\+\\+api\\+\\+($$|/.*)" + - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.replacement=/VirtualHostBase/http/plone.localhost/Plone/++api++/VirtualHostRoot$$1" + # Router + - traefik.http.routers.rt-backend-api.rule=Host(`plone.localhost`) && PathPrefix(`/++api++`) + - traefik.http.routers.rt-backend-api.entrypoints=http + - traefik.http.routers.rt-backend-api.service=svc-backend + - traefik.http.routers.rt-backend-api.middlewares=mw-backend-vhm-api + + db: + image: postgres:${STACK_POSTGRES_TAG:?Set STACK_POSTGRES_TAG} + environment: + POSTGRES_USER: plone + POSTGRES_PASSWORD: plone + POSTGRES_DB: plone + volumes: + - vol-site-data:/var/lib/postgresql + +volumes: + vol-site-data: {} +``` + +```{note} +Use `http://plone.localhost/` to access the website. +If `plone.localhost` doesn't resolve on your computer, add it to your {file}`/etc/hosts` file, pointing to the IP address of the Docker host. +``` + + +### Environment variables + +The {file}`docker-compose.yml` file reads the tags of its images from the following environment variables. +All of them are required, and `docker compose` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_FRONTEND_TAG` | Tag (version) of the image for the frontend | | {{PLONE_FRONTEND_VERSION}} | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | +| `STACK_POSTGRES_TAG` | Tag (version) of the image for PostgreSQL | | {{POSTGRES_VERSION}} | +| `STACK_TRAEFIK_TAG` | Tag (version) of the image for Traefik | | {{TRAEFIK_VERSION}} | + +Create a {file}`.env` file in your project directory with your values. +Docker Compose reads it automatically when you run `docker compose` from that directory. + +```shell +STACK_FRONTEND_TAG={PLONE_FRONTEND_VERSION} +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} +STACK_POSTGRES_TAG={POSTGRES_VERSION} +STACK_TRAEFIK_TAG={TRAEFIK_VERSION} +``` + + +## Build the project + +Start the stack with `docker compose`. + +```shell +docker compose up -d +``` + +This pulls the needed images and starts Plone. + + +## Access Plone in a browser + +After startup, go to `http://plone.localhost/` and you should see the site. + + +## Increase the number of backends + +To use two containers for the backend, run `docker compose` with `--scale`. + +```shell +docker compose up -d --scale backend=2 +``` + +Traefik distributes the requests to the backend among both containers. + + +## Shutdown and cleanup + +The command `docker compose down` removes the containers and default network, but preserves the Plone database. + +The command `docker compose down --volumes` removes the containers, default network, and the Plone database. diff --git a/docs/install/containers/examples/traefik-volto-plone-varnish.md b/docs/install/containers/examples/compose/traefik-volto-plone-varnish.md similarity index 86% rename from docs/install/containers/examples/traefik-volto-plone-varnish.md rename to docs/install/containers/examples/compose/traefik-volto-plone-varnish.md index 24d490115c..46b489108a 100644 --- a/docs/install/containers/examples/traefik-volto-plone-varnish.md +++ b/docs/install/containers/examples/compose/traefik-volto-plone-varnish.md @@ -1,13 +1,13 @@ --- myst: html_meta: - "description": "Basic Plone 6 setup with only one backend, a ZEO server, and data being persisted in a Docker volume." - "property=og:description": "Basic Plone 6 setup with only one backend, a ZEO server, and data being persisted in a Docker volume." - "property=og:title": "Traefik Proxy, Frontend, Backend, Varnish container example" - "keywords": "Plone 6, Container, Docker, Traefik Proxy, Frontend, Backend, Varnish" + "description": "Varnish cache with a purger, Traefik, a frontend, a backend, and a ZEO server in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:description": "Varnish cache with a purger, Traefik, a frontend, a backend, and a ZEO server in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:title": "Traefik, Frontend, Backend, ZEO, Varnish container example" + "keywords": "Plone 6, Container, Docker, Traefik, Frontend, Backend, ZEO, Varnish" --- -# Traefik Proxy, Frontend, Backend, ZEO, Varnish container example +# Traefik, Frontend, Backend, ZEO, Varnish container example This example is a basic setup with one backend accessing a ZEO server and data being persisted in a Docker volume. @@ -19,7 +19,7 @@ A purger component is also used. This solves the problem of invalidating the cac ## Create a project space -Create an empty project directory named `traefik-volto-plone-varnish`. +Create an empty project directory named {file}`traefik-volto-plone-varnish`. ```shell mkdir traefik-volto-plone-varnish @@ -34,7 +34,7 @@ cd traefik-volto-plone-varnish ## Varnish configuration -Create an empty directory named `etc`. +Create an empty directory named {file}`etc`. ```shell mkdir etc @@ -315,7 +315,7 @@ sub vcl_deliver { ```{note} `http://plone.localhost/` is the URL you will be using to access the website. -You can either use `localhost`, or add it in your `/etc/hosts` file or DNS to point to the Docker host IP. +You can either use `localhost`, or add it in your {file}`/etc/hosts` file or DNS to point to the Docker host IP. ``` ## Service configuration with Docker Compose @@ -325,7 +325,7 @@ Now let's create a {file}`docker-compose.yml` file: ```yaml services: webserver: - image: traefik + image: traefik:${STACK_TRAEFIK_TAG:?Set STACK_TRAEFIK_TAG} ports: - 80:80 @@ -364,7 +364,7 @@ services: - --api frontend: - image: plone/plone-frontend:latest + image: plone/plone-frontend:${STACK_FRONTEND_TAG:?Set STACK_FRONTEND_TAG} environment: RAZZLE_INTERNAL_API_PATH: http://backend:8080/Plone RAZZLE_API_PATH: http://plone.localhost @@ -389,17 +389,14 @@ services: - "3000:3000" backend: - image: plone/plone-backend:{PLONE_BACKEND_MINOR_VERSION} + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} environment: SITE: Plone PROFILES: "plone.app.caching:with-caching-proxy" - environment: ZEO_ADDRESS: db:8100 ZEO_SHARED_BLOB_DIR: on # otherwise the backend will create its own blob storage volumes: - - data:/data # the backend and database need access to the same volume - ports: - - 8080:8080 + - vol-site-data:/data # the backend and database need access to the same volume depends_on: - db # If the Docker container is run with a UID other than the UID which owns the local file system persistent storage, @@ -468,14 +465,35 @@ services: - backend db: - image: plone/plone-zeo:latest + image: plone/plone-zeo:${STACK_ZEO_TAG:?Set STACK_ZEO_TAG} volumes: - - data:/data - ports: - - "8100:8100" + - vol-site-data:/data volumes: - data: {} + vol-site-data: {} +``` + + +### Environment variables + +The {file}`docker-compose.yml` file reads the tags of its images from the following environment variables. +All of them are required, and `docker compose` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_FRONTEND_TAG` | Tag (version) of the image for the frontend | | {{PLONE_FRONTEND_VERSION}} | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | +| `STACK_ZEO_TAG` | Tag (version) of the image for the ZEO server | | {{PLONE_ZEO_VERSION}} | +| `STACK_TRAEFIK_TAG` | Tag (version) of the image for Traefik | | {{TRAEFIK_VERSION}} | + +Create a {file}`.env` file in your project directory with your values. +Docker Compose reads it automatically when you run `docker compose` from that directory. + +```shell +STACK_FRONTEND_TAG={PLONE_FRONTEND_VERSION} +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} +STACK_ZEO_TAG={PLONE_ZEO_VERSION} +STACK_TRAEFIK_TAG={TRAEFIK_VERSION} ``` diff --git a/docs/install/containers/examples/compose/traefik-volto-plone-zeo.md b/docs/install/containers/examples/compose/traefik-volto-plone-zeo.md new file mode 100644 index 0000000000..40a3e352e0 --- /dev/null +++ b/docs/install/containers/examples/compose/traefik-volto-plone-zeo.md @@ -0,0 +1,160 @@ +--- +myst: + html_meta: + "description": "ZEO server, Traefik, a frontend, and one or more backend instances in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:description": "ZEO server, Traefik, a frontend, and one or more backend instances in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:title": "Traefik, Frontend, Backend, ZEO container example" + "keywords": "Plone 6, Container, Docker, Traefik, Frontend, Backend, ZEO" +--- + +# Traefik, Frontend, Backend, ZEO container example + +This example is a simple setup with one or more backend instances accessing a ZEO server, with data persisted in a Docker volume. + +In this example, {term}`Traefik Proxy` routes requests to the frontend and the backend. + + +## Setup + +Create an empty project directory named {file}`traefik-volto-plone-zeo`. + +```shell +mkdir traefik-volto-plone-zeo +``` + +Change into your project directory. + +```shell +cd traefik-volto-plone-zeo +``` + + +### Service configuration with Docker Compose + +Create a {file}`docker-compose.yml` file with the following content. +Traefik reads its routing configuration from the labels of the `frontend` and `backend` services, so this example doesn't need a separate proxy configuration file. + +```yaml +services: + + traefik: + image: traefik:${STACK_TRAEFIK_TAG:?Set STACK_TRAEFIK_TAG} + ports: + - "80:80" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + command: + - --providers.docker + - --providers.docker.exposedbydefault=false + - --entrypoints.http.address=:80 + - --accesslog + + frontend: + image: plone/plone-frontend:${STACK_FRONTEND_TAG:?Set STACK_FRONTEND_TAG} + environment: + RAZZLE_INTERNAL_API_PATH: http://backend:8080/Plone + depends_on: + - backend + labels: + - traefik.enable=true + # Service + - traefik.http.services.svc-frontend.loadbalancer.server.port=3000 + # Router + - traefik.http.routers.rt-frontend.rule=Host(`plone.localhost`) + - traefik.http.routers.rt-frontend.entrypoints=http + - traefik.http.routers.rt-frontend.service=svc-frontend + + backend: + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} + environment: + SITE: Plone + ZEO_ADDRESS: db:8100 + ZEO_SHARED_BLOB_DIR: on # otherwise the backend will create its own blob storage + volumes: + - vol-site-data:/data # the backend and database need access to the same volume + depends_on: + - db + labels: + - traefik.enable=true + # Service + - traefik.http.services.svc-backend.loadbalancer.server.port=8080 + # Middleware: Virtual Host Monster rewrite for /++api++/ + - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.regex=^/\\+\\+api\\+\\+($$|/.*)" + - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.replacement=/VirtualHostBase/http/plone.localhost/Plone/++api++/VirtualHostRoot$$1" + # Router + - traefik.http.routers.rt-backend-api.rule=Host(`plone.localhost`) && PathPrefix(`/++api++`) + - traefik.http.routers.rt-backend-api.entrypoints=http + - traefik.http.routers.rt-backend-api.service=svc-backend + - traefik.http.routers.rt-backend-api.middlewares=mw-backend-vhm-api + + db: + image: plone/plone-zeo:${STACK_ZEO_TAG:?Set STACK_ZEO_TAG} + restart: always + volumes: + - vol-site-data:/data + +volumes: + vol-site-data: {} +``` + +```{note} +Use `http://plone.localhost/` to access the website. +If `plone.localhost` doesn't resolve on your computer, add it to your {file}`/etc/hosts` file, pointing to the IP address of the Docker host. +``` + + +### Environment variables + +The {file}`docker-compose.yml` file reads the tags of its images from the following environment variables. +All of them are required, and `docker compose` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_FRONTEND_TAG` | Tag (version) of the image for the frontend | | {{PLONE_FRONTEND_VERSION}} | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | +| `STACK_ZEO_TAG` | Tag (version) of the image for the ZEO server | | {{PLONE_ZEO_VERSION}} | +| `STACK_TRAEFIK_TAG` | Tag (version) of the image for Traefik | | {{TRAEFIK_VERSION}} | + +Create a {file}`.env` file in your project directory with your values. +Docker Compose reads it automatically when you run `docker compose` from that directory. + +```shell +STACK_FRONTEND_TAG={PLONE_FRONTEND_VERSION} +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} +STACK_ZEO_TAG={PLONE_ZEO_VERSION} +STACK_TRAEFIK_TAG={TRAEFIK_VERSION} +``` + + +## Build the project + +Start the stack with `docker compose`. + +```shell +docker compose up -d +``` + +This pulls the needed images and starts Plone. + + +## Access Plone in a browser + +After startup, go to `http://plone.localhost/` and you should see the site. + + +## Increase the number of backends + +To use two containers for the backend, run `docker compose` with `--scale`. + +```shell +docker compose up -d --scale backend=2 +``` + +Traefik distributes the requests to the backend among both containers. + + +## Shutdown and cleanup + +The command `docker compose down` removes the containers and default network, but preserves the Plone database. + +The command `docker compose down --volumes` removes the containers, default network, and the Plone database. diff --git a/docs/install/containers/examples/compose/traefik-volto-plone.md b/docs/install/containers/examples/compose/traefik-volto-plone.md new file mode 100644 index 0000000000..ea4001cb72 --- /dev/null +++ b/docs/install/containers/examples/compose/traefik-volto-plone.md @@ -0,0 +1,137 @@ +--- +myst: + html_meta: + "description": "Traefik, a frontend, and a single backend in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:description": "Traefik, a frontend, and a single backend in a Plone 6 project for Docker Compose, with data persisted in a Docker volume." + "property=og:title": "Traefik, Frontend, Backend container example" + "keywords": "Plone 6, Container, Docker, Traefik, Frontend, Backend" +--- + +# Traefik, Frontend, Backend container example + +This example is a simple setup with one frontend and one backend, with data persisted in a Docker volume. + +In this example, {term}`Traefik Proxy` routes requests to the frontend and the backend. + + +## Setup + +Create an empty project directory named {file}`traefik-volto-plone`. + +```shell +mkdir traefik-volto-plone +``` + +Change into your project directory. + +```shell +cd traefik-volto-plone +``` + + +### Service configuration with Docker Compose + +Create a {file}`docker-compose.yml` file with the following content. +Traefik reads its routing configuration from the labels of the `frontend` and `backend` services, so this example doesn't need a separate proxy configuration file. + +```yaml +services: + + traefik: + image: traefik:${STACK_TRAEFIK_TAG:?Set STACK_TRAEFIK_TAG} + ports: + - "80:80" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + command: + - --providers.docker + - --providers.docker.exposedbydefault=false + - --entrypoints.http.address=:80 + - --accesslog + + frontend: + image: plone/plone-frontend:${STACK_FRONTEND_TAG:?Set STACK_FRONTEND_TAG} + environment: + RAZZLE_INTERNAL_API_PATH: http://backend:8080/Plone + depends_on: + - backend + labels: + - traefik.enable=true + # Service + - traefik.http.services.svc-frontend.loadbalancer.server.port=3000 + # Router + - traefik.http.routers.rt-frontend.rule=Host(`plone.localhost`) + - traefik.http.routers.rt-frontend.entrypoints=http + - traefik.http.routers.rt-frontend.service=svc-frontend + + backend: + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} + environment: + SITE: Plone + volumes: + - vol-site-data:/data + labels: + - traefik.enable=true + # Service + - traefik.http.services.svc-backend.loadbalancer.server.port=8080 + # Middleware: Virtual Host Monster rewrite for /++api++/ + - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.regex=^/\\+\\+api\\+\\+($$|/.*)" + - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.replacement=/VirtualHostBase/http/plone.localhost/Plone/++api++/VirtualHostRoot$$1" + # Router + - traefik.http.routers.rt-backend-api.rule=Host(`plone.localhost`) && PathPrefix(`/++api++`) + - traefik.http.routers.rt-backend-api.entrypoints=http + - traefik.http.routers.rt-backend-api.service=svc-backend + - traefik.http.routers.rt-backend-api.middlewares=mw-backend-vhm-api + +volumes: + vol-site-data: {} +``` + +```{note} +Use `http://plone.localhost/` to access the website. +If `plone.localhost` doesn't resolve on your computer, add it to your {file}`/etc/hosts` file, pointing to the IP address of the Docker host. +``` + + +### Environment variables + +The {file}`docker-compose.yml` file reads the tags of its images from the following environment variables. +All of them are required, and `docker compose` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_FRONTEND_TAG` | Tag (version) of the image for the frontend | | {{PLONE_FRONTEND_VERSION}} | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | +| `STACK_TRAEFIK_TAG` | Tag (version) of the image for Traefik | | {{TRAEFIK_VERSION}} | + +Create a {file}`.env` file in your project directory with your values. +Docker Compose reads it automatically when you run `docker compose` from that directory. + +```shell +STACK_FRONTEND_TAG={PLONE_FRONTEND_VERSION} +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} +STACK_TRAEFIK_TAG={TRAEFIK_VERSION} +``` + + +## Build the project + +Start the stack with `docker compose`. + +```shell +docker compose up -d +``` + +This pulls the needed images and starts Plone. + + +## Access Plone in a browser + +After startup, go to `http://plone.localhost/` and you should see the site. + + +## Shutdown and cleanup + +The command `docker compose down` removes the containers and default network, but preserves the Plone database. + +The command `docker compose down --volumes` removes the containers, default network, and the Plone database. diff --git a/docs/install/containers/examples/index.md b/docs/install/containers/examples/index.md index 5af7f6315b..b2bc27e35b 100644 --- a/docs/install/containers/examples/index.md +++ b/docs/install/containers/examples/index.md @@ -13,21 +13,48 @@ myst: :maxdepth: 2 :hidden: true -nginx-volto-plone -nginx-volto-plone-zeo -nginx-volto-plone-postgresql -nginx-plone -haproxy-plone-zeo -traefik-volto-plone-varnish +compose/index +swarm/index ``` +## Docker Compose + Examples of projects running Plone using `docker compose`. +### Traefik + +| Project example | Description | +| --- | --- | +| {doc}`traefik-volto-plone ` | Stack with Traefik, Frontend, and Backend | +| {doc}`traefik-volto-plone-zeo ` | Stack with Traefik, Frontend, Backend, and ZEO server | +| {doc}`traefik-volto-plone-postgresql ` | Stack with Traefik, Frontend, Backend, and PostgreSQL DB | +| {doc}`traefik-plone ` | Stack with Traefik and Backend (Plone Classic) | +| {doc}`traefik-volto-plone-varnish ` | Stack with Traefik, Frontend, Backend, ZEO server, and Varnish | + +### nginx + | Project example | Description | | --- | --- | -| [nginx-volto-plone](nginx-volto-plone) | Stack with nginx, Frontend, and Backend | -| [nginx-volto-plone-zeo](nginx-volto-plone-zeo) | Stack with nginx, Frontend, Backend, and ZEO server | -| [nginx-volto-plone-postgresql](nginx-volto-plone-postgresql) | Stack with nginx, Frontend, Backend, and PostgreSQL DB | -| [nginx-plone](nginx-plone) | Stack with nginx and Backend (Plone Classic) | -| [haproxy-plone-zeo](haproxy-plone-zeo) | Stack with HAProxy, Backend, and ZEO server | -| [traefik-volto-plone-varnish](traefik-volto-plone-varnish) | Stack with traefik, Frontend, Backend, Varnish | +| {doc}`nginx-volto-plone ` | Stack with nginx, Frontend, and Backend | +| {doc}`nginx-volto-plone-zeo ` | Stack with nginx, Frontend, Backend, and ZEO server | +| {doc}`nginx-volto-plone-postgresql ` | Stack with nginx, Frontend, Backend, and PostgreSQL DB | +| {doc}`nginx-plone ` | Stack with nginx and Backend (Plone Classic) | + +### HAProxy + +| Project example | Description | +| --- | --- | +| {doc}`haproxy-plone-zeo ` | Stack with HAProxy, Backend, and ZEO server | + + +## Docker Swarm + +Examples of stacks running Plone on a Docker Swarm cluster. + +| Stack example | Description | +| --- | --- | +| {doc}`traefik-volto-plone ` | Stack with Traefik, Frontend, and Backend | +| {doc}`traefik-volto-plone-zeo ` | Stack with Traefik, Frontend, Backend, and ZEO server | +| {doc}`traefik-volto-plone-postgresql ` | Stack with Traefik, Frontend, Backend, and PostgreSQL DB | +| {doc}`traefik-plone ` | Stack with Traefik and Backend (Plone Classic) | +| {doc}`traefik-volto-plone-varnish ` | Stack with Traefik, Frontend, Backend, ZEO server, and Varnish | diff --git a/docs/install/containers/examples/swarm/index.md b/docs/install/containers/examples/swarm/index.md new file mode 100644 index 0000000000..51e1d389b6 --- /dev/null +++ b/docs/install/containers/examples/swarm/index.md @@ -0,0 +1,31 @@ +--- +myst: + html_meta: + "description": "Docker Swarm stacks that run Plone 6 behind Traefik, with a scalable frontend and backend." + "property=og:description": "Docker Swarm stacks that run Plone 6 behind Traefik, with a scalable frontend and backend." + "property=og:title": "Docker Swarm examples" + "keywords": "Plone 6, install, installation, Docker, Docker Swarm, containers" +--- + +# Docker Swarm examples + +```{toctree} +:maxdepth: 2 +:hidden: true + +traefik-volto-plone +traefik-volto-plone-zeo +traefik-volto-plone-postgresql +traefik-plone +traefik-volto-plone-varnish +``` + +Examples of stacks running Plone on a Docker Swarm cluster. + +| Stack example | Description | +| --- | --- | +| {doc}`traefik-volto-plone ` | Stack with Traefik, Frontend, and Backend | +| {doc}`traefik-volto-plone-zeo ` | Stack with Traefik, Frontend, Backend, and ZEO server | +| {doc}`traefik-volto-plone-postgresql ` | Stack with Traefik, Frontend, Backend, and PostgreSQL DB | +| {doc}`traefik-plone ` | Stack with Traefik and Backend (Plone Classic) | +| {doc}`traefik-volto-plone-varnish ` | Stack with Traefik, Frontend, Backend, ZEO server, and Varnish | diff --git a/docs/install/containers/examples/swarm/traefik-plone.md b/docs/install/containers/examples/swarm/traefik-plone.md new file mode 100644 index 0000000000..a08d858d1e --- /dev/null +++ b/docs/install/containers/examples/swarm/traefik-plone.md @@ -0,0 +1,276 @@ +--- +myst: + html_meta: + "description": "Classic UI backend and Traefik in a Plone 6 stack for Docker Swarm, with data stored in a Docker volume." + "property=og:description": "Classic UI backend and Traefik in a Plone 6 stack for Docker Swarm, with data stored in a Docker volume." + "property=og:title": "Traefik, Plone Classic example for Docker Swarm" + "keywords": "Plone 6, Container, Docker, Docker Swarm, Traefik, Plone Classic" +--- + +# Traefik, Plone Classic example for Docker Swarm + +This example deploys a Plone 6 site with the Classic UI as a stack on a Docker Swarm cluster. +The stack runs the following services. + +`traefik` +: {term}`Traefik Proxy` routes requests to the backend, and gets TLS certificates from Let's Encrypt. + +`socket-proxy` +: Gives Traefik access to the parts of the Docker API it needs, instead of the Docker socket itself. + +`backend` +: The Plone backend with the Classic UI, with its data persisted in a Docker volume. + + +## Prerequisites + +- A Docker Swarm cluster. + To create a cluster with a single node, run `docker swarm init` on that node. +- DNS records that point the host names of `STACK_HOSTNAME`, `STACK_HOSTNAME_REDIRECT`, and `TRAEFIK_HOSTNAME` to your cluster. +- Ports 80 and 443 open to the internet, so Let's Encrypt can validate your host names. + + +## Setup + +Create an empty project directory named {file}`swarm-traefik-plone`. + +```shell +mkdir swarm-traefik-plone +``` + +Change into your project directory. + +```shell +cd swarm-traefik-plone +``` + + +### Create the public network + +Traefik and the services it routes requests to share an overlay network named `nw-public`. +Create it once on a manager node, before you deploy the stack. + +```shell +docker network create --driver overlay nw-public +``` + + +### Stack file + +Create a {file}`stack.yml` file with the following content. + +```yaml +services: + + socket-proxy: + image: tecnativa/docker-socket-proxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + NODES: 1 + SERVICES: 1 + TASKS: 1 + NETWORKS: 1 + networks: + - nw-traefik + deploy: + placement: + constraints: + - node.role == manager + + traefik: + image: traefik:${STACK_TRAEFIK_TAG:?Set STACK_TRAEFIK_TAG} + ports: + - "80:80" + - "443:443" + volumes: + - vol-traefik-certs:/certificates + command: + - --providers.swarm + - --providers.swarm.endpoint=tcp://socket-proxy:2375 + - --providers.swarm.exposedbydefault=false + - --providers.swarm.network=nw-public + - --providers.swarm.constraints=Label(`traefik.constraint-label`, `public`) + - --entrypoints.http.address=:80 + - --entrypoints.https.address=:443 + - --certificatesresolvers.le.acme.email=${TRAEFIK_EMAIL:?Set TRAEFIK_EMAIL} + - --certificatesresolvers.le.acme.storage=/certificates/acme.json + - --certificatesresolvers.le.acme.tlschallenge=true + - --accesslog + - --accesslog.format=json + - --log.level=INFO + - --log.format=json + - --api + networks: + - nw-public + - nw-traefik + deploy: + replicas: 1 + placement: + constraints: + - node.role == manager + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + - traefik.http.services.traefik-public.loadbalancer.server.port=8000 + + # Dashboard + - traefik.http.middlewares.admin-auth.basicauth.users=${TRAEFIK_BASIC_AUTH:?Set TRAEFIK_BASIC_AUTH} + - traefik.http.routers.traefik-dashboard.rule=Host(`${TRAEFIK_HOSTNAME:?Set TRAEFIK_HOSTNAME}`) + - traefik.http.routers.traefik-dashboard.entrypoints=https + - traefik.http.routers.traefik-dashboard.tls=true + - traefik.http.routers.traefik-dashboard.tls.certresolver=le + - traefik.http.routers.traefik-dashboard.service=api@internal + - traefik.http.routers.traefik-dashboard.middlewares=admin-auth + + # Generic middlewares + - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https + - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true + - traefik.http.middlewares.gzip.compress=true + - traefik.http.middlewares.gzip.compress.excludedcontenttypes=image/png, image/jpeg, font/woff2 + + # Redirect every HTTP request to HTTPS + - traefik.http.routers.generic-https-redirect.entrypoints=http + - traefik.http.routers.generic-https-redirect.rule=HostRegexp(`^.+$$`) + - traefik.http.routers.generic-https-redirect.priority=1 + - traefik.http.routers.generic-https-redirect.middlewares=https-redirect + + backend: + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} + environment: + SITE: Plone + TYPE: classic + volumes: + - vol-site-data:/data + networks: + - nw-public + deploy: + replicas: 1 + update_config: + parallelism: 1 + delay: 5s + order: stop-first # only one process can open the database + labels: + - traefik.enable=true + - traefik.constraint-label=public + # Service + - traefik.http.services.svc-${STACK_NAME:?Set STACK_NAME}-backend.loadbalancer.server.port=8080 + + # Middlewares + ## Redirect + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.permanent=true + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.regex=^https://${STACK_HOSTNAME_REDIRECT:?Set STACK_HOSTNAME_REDIRECT}/(.*) + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.replacement=https://${STACK_HOSTNAME:?Set STACK_HOSTNAME}/$${1} + ## Virtual Host Monster rewrite for the site + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm.replacepathregex.regex=^/(.*)" + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm.replacepathregex.replacement=/VirtualHostBase/https/${STACK_HOSTNAME}/Plone/VirtualHostRoot/$$1" + + # Routers + ## Redirect + - traefik.http.routers.rt-${STACK_NAME}-backend-redirect.rule=Host(`${STACK_HOSTNAME_REDIRECT}`) + - traefik.http.routers.rt-${STACK_NAME}-backend-redirect.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-backend-redirect.tls=true + - traefik.http.routers.rt-${STACK_NAME}-backend-redirect.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-backend-redirect.middlewares=mw-${STACK_NAME}-redirect + ## / + - traefik.http.routers.rt-${STACK_NAME}-backend.rule=Host(`${STACK_HOSTNAME}`) + - traefik.http.routers.rt-${STACK_NAME}-backend.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-backend.tls=true + - traefik.http.routers.rt-${STACK_NAME}-backend.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-backend.service=svc-${STACK_NAME}-backend + - traefik.http.routers.rt-${STACK_NAME}-backend.middlewares=gzip,mw-${STACK_NAME}-backend-vhm + +volumes: + vol-traefik-certs: {} + vol-site-data: {} + +networks: + nw-public: + external: true + nw-traefik: + driver: overlay + internal: true +``` + +```{warning} +The backend stores its data in a Docker volume, and only one backend process can open that data at a time. +Keep the `backend` service at one replica. + +Docker volumes are also local to the node where they're created. +In a cluster with more than one node, add a placement constraint to the `backend` service, so it always runs on the node that holds its data. +``` + + +### Environment variables + +The stack reads its configuration from the following environment variables. +All of them are required, and `docker stack deploy` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_NAME` | Name of the stack, which must match the name you pass to `docker stack deploy`. Traefik uses it to name routers, services, and middleware. | | `plone` | +| `STACK_HOSTNAME` | Public host name of the site | | `www.example.com` | +| `STACK_HOSTNAME_REDIRECT` | Host name that permanently redirects to `STACK_HOSTNAME` | | `example.com` | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | +| `STACK_TRAEFIK_TAG` | Tag (version) of the image for Traefik | | {{TRAEFIK_VERSION}} | +| `TRAEFIK_HOSTNAME` | Host name of the Traefik dashboard | | `traefik.example.com` | +| `TRAEFIK_EMAIL` | Email address of your Let's Encrypt account | | `admin@example.com` | +| `TRAEFIK_BASIC_AUTH` | User name and password hash for the Traefik dashboard, in the format `user:hash` | | `admin:$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0` | + +To create a password hash, run the following command, with your password instead of `secret`. + +```shell +openssl passwd -apr1 secret +``` + +Create a {file}`.env` file with your values. +Wrap values that contain a dollar sign, such as password hashes, in single quotes. + +```shell +STACK_NAME=plone +STACK_HOSTNAME=www.example.com +STACK_HOSTNAME_REDIRECT=example.com +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} +STACK_TRAEFIK_TAG={TRAEFIK_VERSION} +TRAEFIK_HOSTNAME=traefik.example.com +TRAEFIK_EMAIL=admin@example.com +TRAEFIK_BASIC_AUTH='admin:$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0' +``` + +`docker stack deploy` doesn't read {file}`.env` files. +Load the variables into your shell before you deploy. + +```shell +set -a +. ./.env +set +a +``` + + +## Deploy the stack + +Deploy the stack with the name from `STACK_NAME`. + +```shell +docker stack deploy -c stack.yml "$STACK_NAME" +``` + +This pulls the needed images and starts Plone. + + +## Access Plone + +After startup, go to `https://www.example.com`, using your value of `STACK_HOSTNAME`, and you should see the site. + +Unlike the {doc}`Docker Compose example <../compose/traefik-plone>`, this stack doesn't publish port 8080 of the backend, so the page to create more Plone sites isn't reachable. + +The Traefik dashboard is available at the host name from `TRAEFIK_HOSTNAME`, behind the user name and password from `TRAEFIK_BASIC_AUTH`. + + +## Shutdown and cleanup + +The command `docker stack rm "$STACK_NAME"` removes the services and the stack networks, but preserves the Plone database and the TLS certificates in their volumes. diff --git a/docs/install/containers/examples/swarm/traefik-volto-plone-postgresql.md b/docs/install/containers/examples/swarm/traefik-volto-plone-postgresql.md new file mode 100644 index 0000000000..ffdba37d23 --- /dev/null +++ b/docs/install/containers/examples/swarm/traefik-volto-plone-postgresql.md @@ -0,0 +1,365 @@ +--- +myst: + html_meta: + "description": "PostgreSQL database, scalable frontend and backend, and Traefik in a Plone 6 stack for Docker Swarm." + "property=og:description": "PostgreSQL database, scalable frontend and backend, and Traefik in a Plone 6 stack for Docker Swarm." + "property=og:title": "Traefik, Frontend, Backend, PostgreSQL example for Docker Swarm" + "keywords": "Plone 6, Container, Docker, Docker Swarm, Traefik, Frontend, Backend, PostgreSQL" +--- + +# Traefik, Frontend, Backend, PostgreSQL example for Docker Swarm + +This example deploys Plone 6 as a stack on a Docker Swarm cluster. +The stack runs the following services. + +`traefik` +: {term}`Traefik Proxy` routes requests to the other services, and gets TLS certificates from Let's Encrypt. + +`socket-proxy` +: Gives Traefik access to the parts of the Docker API it needs, instead of the Docker socket itself. + +`frontend` +: The Plone frontend, with two replicas by default. + +`backend` +: The Plone backend, with two replicas by default, which stores its data in PostgreSQL. + +`db` +: The PostgreSQL database, with its data persisted in a Docker volume. + + +## Prerequisites + +- A Docker Swarm cluster. + To create a cluster with a single node, run `docker swarm init` on that node. +- DNS records that point the host names of `STACK_HOSTNAME`, `STACK_HOSTNAME_REDIRECT`, and `TRAEFIK_HOSTNAME` to your cluster. +- Ports 80 and 443 open to the internet, so Let's Encrypt can validate your host names. + + +## Setup + +Create an empty project directory named {file}`swarm-traefik-volto-plone-postgresql`. + +```shell +mkdir swarm-traefik-volto-plone-postgresql +``` + +Change into your project directory. + +```shell +cd swarm-traefik-volto-plone-postgresql +``` + + +### Create the public network + +Traefik and the services it routes requests to share an overlay network named `nw-public`. +Create it once on a manager node, before you deploy the stack. + +```shell +docker network create --driver overlay nw-public +``` + + +### Stack file + +Create a {file}`stack.yml` file with the following content. + +```yaml +services: + + socket-proxy: + image: tecnativa/docker-socket-proxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + NODES: 1 + SERVICES: 1 + TASKS: 1 + NETWORKS: 1 + networks: + - nw-traefik + deploy: + placement: + constraints: + - node.role == manager + + traefik: + image: traefik:${STACK_TRAEFIK_TAG:?Set STACK_TRAEFIK_TAG} + ports: + - "80:80" + - "443:443" + volumes: + - vol-traefik-certs:/certificates + command: + - --providers.swarm + - --providers.swarm.endpoint=tcp://socket-proxy:2375 + - --providers.swarm.exposedbydefault=false + - --providers.swarm.network=nw-public + - --providers.swarm.constraints=Label(`traefik.constraint-label`, `public`) + - --entrypoints.http.address=:80 + - --entrypoints.https.address=:443 + - --certificatesresolvers.le.acme.email=${TRAEFIK_EMAIL:?Set TRAEFIK_EMAIL} + - --certificatesresolvers.le.acme.storage=/certificates/acme.json + - --certificatesresolvers.le.acme.tlschallenge=true + - --accesslog + - --accesslog.format=json + - --log.level=INFO + - --log.format=json + - --api + networks: + - nw-public + - nw-traefik + deploy: + replicas: 1 + placement: + constraints: + - node.role == manager + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + - traefik.http.services.traefik-public.loadbalancer.server.port=8000 + + # Dashboard + - traefik.http.middlewares.admin-auth.basicauth.users=${TRAEFIK_BASIC_AUTH:?Set TRAEFIK_BASIC_AUTH} + - traefik.http.routers.traefik-dashboard.rule=Host(`${TRAEFIK_HOSTNAME:?Set TRAEFIK_HOSTNAME}`) + - traefik.http.routers.traefik-dashboard.entrypoints=https + - traefik.http.routers.traefik-dashboard.tls=true + - traefik.http.routers.traefik-dashboard.tls.certresolver=le + - traefik.http.routers.traefik-dashboard.service=api@internal + - traefik.http.routers.traefik-dashboard.middlewares=admin-auth + + # Generic middlewares + - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https + - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true + - traefik.http.middlewares.gzip.compress=true + - traefik.http.middlewares.gzip.compress.excludedcontenttypes=image/png, image/jpeg, font/woff2 + + # Redirect every HTTP request to HTTPS + - traefik.http.routers.generic-https-redirect.entrypoints=http + - traefik.http.routers.generic-https-redirect.rule=HostRegexp(`^.+$$`) + - traefik.http.routers.generic-https-redirect.priority=1 + - traefik.http.routers.generic-https-redirect.middlewares=https-redirect + + frontend: + image: plone/plone-frontend:${STACK_FRONTEND_TAG:?Set STACK_FRONTEND_TAG} + environment: + RAZZLE_INTERNAL_API_PATH: http://${STACK_NAME:?Set STACK_NAME}_backend:8080/Plone + RAZZLE_API_PATH: https://${STACK_HOSTNAME:?Set STACK_HOSTNAME} + networks: + - nw-public + - nw-internal + deploy: + replicas: ${STACK_FRONTEND_REPLICAS:-2} + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + # Service + - traefik.http.services.svc-${STACK_NAME}-frontend.loadbalancer.server.port=3000 + + # Middleware + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.permanent=true + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.regex=^https://${STACK_HOSTNAME_REDIRECT:?Set STACK_HOSTNAME_REDIRECT}/(.*) + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.replacement=https://${STACK_HOSTNAME}/$${1} + + # Routers + ## Redirect + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.rule=Host(`${STACK_HOSTNAME_REDIRECT}`) + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.tls=true + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.middlewares=mw-${STACK_NAME}-redirect + ## / + - traefik.http.routers.rt-${STACK_NAME}-frontend.rule=Host(`${STACK_HOSTNAME}`) + - traefik.http.routers.rt-${STACK_NAME}-frontend.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-frontend.tls=true + - traefik.http.routers.rt-${STACK_NAME}-frontend.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-frontend.service=svc-${STACK_NAME}-frontend + - traefik.http.routers.rt-${STACK_NAME}-frontend.middlewares=gzip + + backend: + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} + environment: + SITE: Plone + RELSTORAGE_DSN: "dbname='${DB_NAME:-plone}' user='${DB_USER:-plone}' host='${STACK_NAME}_db' password='${DB_PASSWORD:?Set DB_PASSWORD}'" + networks: + - nw-public + - nw-internal + deploy: + replicas: ${STACK_BACKEND_REPLICAS:-2} + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + # Service + - traefik.http.services.svc-${STACK_NAME}-backend.loadbalancer.server.port=8080 + + # Middlewares + ## Virtual Host Monster rewrite for /++api++/ + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-api.replacepathregex.regex=^/\\+\\+api\\+\\+($$|/.*)" + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-api.replacepathregex.replacement=/VirtualHostBase/https/${STACK_HOSTNAME}/Plone/++api++/VirtualHostRoot$$1" + ## Virtual Host Monster rewrite for /ClassicUI/ + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-classic.replacepathregex.regex=^/ClassicUI($$|/.*)" + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-classic.replacepathregex.replacement=/VirtualHostBase/https/${STACK_HOSTNAME}/Plone/VirtualHostRoot/_vh_ClassicUI$$1" + ## Basic authentication for /ClassicUI/ + - traefik.http.middlewares.mw-${STACK_NAME}-backend-auth.basicauth.users=${BASIC_AUTH_USER:?Set BASIC_AUTH_USER}:${BASIC_AUTH_PASSWORD_HASH:?Set BASIC_AUTH_PASSWORD_HASH} + + # Routers + ## /++api++ + - traefik.http.routers.rt-${STACK_NAME}-backend-api.rule=Host(`${STACK_HOSTNAME}`) && PathPrefix(`/++api++`) + - traefik.http.routers.rt-${STACK_NAME}-backend-api.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-backend-api.tls=true + - traefik.http.routers.rt-${STACK_NAME}-backend-api.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-backend-api.service=svc-${STACK_NAME}-backend + - traefik.http.routers.rt-${STACK_NAME}-backend-api.middlewares=gzip,mw-${STACK_NAME}-backend-vhm-api + ## /ClassicUI + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.rule=Host(`${STACK_HOSTNAME}`) && PathPrefix(`/ClassicUI`) + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.tls=true + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.service=svc-${STACK_NAME}-backend + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.middlewares=gzip,mw-${STACK_NAME}-backend-auth,mw-${STACK_NAME}-backend-vhm-classic + + db: + image: postgres:${STACK_POSTGRES_TAG:?Set STACK_POSTGRES_TAG} + environment: + POSTGRES_USER: ${DB_USER:-plone} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME:-plone} + volumes: + - vol-site-data:/var/lib/postgresql + networks: + - nw-internal + deploy: + replicas: 1 + update_config: + parallelism: 1 + delay: 1s + order: stop-first + +volumes: + vol-traefik-certs: {} + vol-site-data: {} + +networks: + nw-public: + external: true + nw-traefik: + driver: overlay + internal: true + nw-internal: + driver: overlay + internal: true +``` + +```{warning} +Docker volumes are local to the node where they're created. +In a cluster with more than one node, add a placement constraint to the `db` service, so the database always runs on the node that holds its data. +``` + + +### Environment variables + +The stack reads its configuration from the following environment variables. +Variables without a default value are required, and `docker stack deploy` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_NAME` | Name of the stack, which must match the name you pass to `docker stack deploy`. The services use it to reach each other, and Traefik uses it to name routers, services, and middleware. | | `plone` | +| `STACK_HOSTNAME` | Public host name of the site | | `www.example.com` | +| `STACK_HOSTNAME_REDIRECT` | Host name that permanently redirects to `STACK_HOSTNAME` | | `example.com` | +| `STACK_FRONTEND_REPLICAS` | Number of frontend replicas | `2` | `3` | +| `STACK_BACKEND_REPLICAS` | Number of backend replicas | `2` | `4` | +| `STACK_FRONTEND_TAG` | Tag (version) of the image for the frontend | | {{PLONE_FRONTEND_VERSION}} | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | +| `STACK_POSTGRES_TAG` | Tag (version) of the image for PostgreSQL | | {{POSTGRES_VERSION}} | +| `STACK_TRAEFIK_TAG` | Tag (version) of the image for Traefik | | {{TRAEFIK_VERSION}} | +| `TRAEFIK_HOSTNAME` | Host name of the Traefik dashboard | | `traefik.example.com` | +| `TRAEFIK_EMAIL` | Email address of your Let's Encrypt account | | `admin@example.com` | +| `TRAEFIK_BASIC_AUTH` | User name and password hash for the Traefik dashboard, in the format `user:hash` | | `admin:$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0` | +| `BASIC_AUTH_USER` | User name for the Classic UI at `/ClassicUI` | | `admin` | +| `BASIC_AUTH_PASSWORD_HASH` | Password hash for the Classic UI at `/ClassicUI` | | `$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0` | +| `DB_NAME` | Name of the PostgreSQL database | `plone` | `plone` | +| `DB_USER` | Name of the PostgreSQL user | `plone` | `plone` | +| `DB_PASSWORD` | Password of the PostgreSQL user | | `Correct-Horse-Battery-Staple` | + +To create a password hash, run the following command, with your password instead of `secret`. + +```shell +openssl passwd -apr1 secret +``` + +Create a {file}`.env` file with your values. +Wrap values that contain a dollar sign, such as password hashes, in single quotes. + +```shell +STACK_NAME=plone +STACK_HOSTNAME=www.example.com +STACK_HOSTNAME_REDIRECT=example.com +STACK_FRONTEND_TAG={PLONE_FRONTEND_VERSION} +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} +STACK_POSTGRES_TAG={POSTGRES_VERSION} +STACK_TRAEFIK_TAG={TRAEFIK_VERSION} +TRAEFIK_HOSTNAME=traefik.example.com +TRAEFIK_EMAIL=admin@example.com +TRAEFIK_BASIC_AUTH='admin:$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0' +BASIC_AUTH_USER=admin +BASIC_AUTH_PASSWORD_HASH='$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0' +DB_PASSWORD=Correct-Horse-Battery-Staple +``` + +`docker stack deploy` doesn't read {file}`.env` files. +Load the variables into your shell before you deploy. + +```shell +set -a +. ./.env +set +a +``` + + +## Deploy the stack + +Deploy the stack with the name from `STACK_NAME`. + +```shell +docker stack deploy -c stack.yml "$STACK_NAME" +``` + +This pulls the needed images and starts Plone. + + +## Access Plone + +After startup, go to `https://www.example.com`, using your value of `STACK_HOSTNAME`, and you should see the site. + +The Classic UI is available at `https://www.example.com/ClassicUI`, behind the user name and password from `BASIC_AUTH_USER` and `BASIC_AUTH_PASSWORD_HASH`. + +The Traefik dashboard is available at the host name from `TRAEFIK_HOSTNAME`, behind the user name and password from `TRAEFIK_BASIC_AUTH`. + + +## Increase the number of backends + +To run four backend replicas, scale the backend service. + +```shell +docker service scale "${STACK_NAME}_backend=4" +``` + +The next `docker stack deploy` sets the number of replicas back to the value of `STACK_BACKEND_REPLICAS`. + + +## Shutdown and cleanup + +The command `docker stack rm "$STACK_NAME"` removes the services and the stack networks, but preserves the Plone database and the TLS certificates in their volumes. diff --git a/docs/install/containers/examples/swarm/traefik-volto-plone-varnish.md b/docs/install/containers/examples/swarm/traefik-volto-plone-varnish.md new file mode 100644 index 0000000000..af724aa1d2 --- /dev/null +++ b/docs/install/containers/examples/swarm/traefik-volto-plone-varnish.md @@ -0,0 +1,718 @@ +--- +myst: + html_meta: + "description": "Varnish cache, ZEO server, scalable frontend and backend, and Traefik in a Plone 6 stack for Docker Swarm." + "property=og:description": "Varnish cache, ZEO server, scalable frontend and backend, and Traefik in a Plone 6 stack for Docker Swarm." + "property=og:title": "Traefik, Frontend, Backend, ZEO, Varnish example for Docker Swarm" + "keywords": "Plone 6, Container, Docker, Docker Swarm, Traefik, Frontend, Backend, ZEO, Varnish" +--- + +# Traefik, Frontend, Backend, ZEO, Varnish example for Docker Swarm + +This example deploys Plone 6 as a stack on a Docker Swarm cluster, with {term}`Varnish` caching the responses of the frontend and the backend. +The stack runs the following services. + +`traefik` +: {term}`Traefik Proxy` routes requests to the other services, and gets TLS certificates from Let's Encrypt. + +`socket-proxy` +: Gives Traefik access to the parts of the Docker API it needs, instead of the Docker socket itself. + +`varnish` +: Caches responses, with two replicas by default. + +`purger` +: Receives purge requests from the backend, and forwards them to every Varnish replica. + +`frontend` +: The Plone frontend, with two replicas by default. + +`backend` +: The Plone backend, with two replicas by default, which stores its data in the ZEO server. + +`db` +: The ZEO server, with its data persisted in a Docker volume. + +Traefik receives public requests over HTTPS, and sends them to Varnish. +Varnish adds the `X-Varnish-Routed: 1` header to each request that it doesn't serve from its cache, and sends the request back to Traefik on port 8081, which routes it to the frontend or the backend. +The stack doesn't publish port 8081, so clients can only reach the site over HTTPS. +Requests to the Classic UI at `/ClassicUI` go straight from Traefik to the backend. + + +## Prerequisites + +- A Docker Swarm cluster. + To create a cluster with a single node, run `docker swarm init` on that node. +- DNS records that point the host names of `STACK_HOSTNAME`, `STACK_HOSTNAME_REDIRECT`, and `TRAEFIK_HOSTNAME` to your cluster. +- Ports 80 and 443 open to the internet, so Let's Encrypt can validate your host names. + + +## Setup + +Create an empty project directory named {file}`swarm-traefik-volto-plone-varnish`. + +```shell +mkdir swarm-traefik-volto-plone-varnish +``` + +Change into your project directory. + +```shell +cd swarm-traefik-volto-plone-varnish +``` + + +### Create the public network + +Traefik and the services it routes requests to share an overlay network named `nw-public`. +Create it once on a manager node, before you deploy the stack. + +```shell +docker network create --driver overlay nw-public +``` + + +### Varnish configuration + +Create a directory named {file}`etc`. + +```shell +mkdir etc +``` + +Create a file named {file}`etc/varnish.vcl` with the following content. +It differs from the file in the {doc}`Docker Compose example <../compose/traefik-volto-plone-varnish>` in two places. +The backend is port 8081 of `traefik`, the name of the Traefik service in the stack, and the `purge` access control list doesn't include the `backend` host name. + +```vcl +vcl 4.0; + +import std; +import directors; + +backend traefik_loadbalancer { + .host = "traefik"; + .port = "8081"; + .connect_timeout = 2s; + .first_byte_timeout = 300s; + .between_bytes_timeout = 60s; +} + +/* Only allow PURGE from localhost and API-Server */ +acl purge { + "localhost"; + "127.0.0.1"; + "172.16.0.0/12"; + "10.0.0.0/8"; + "192.168.0.0/16"; +} + +sub detect_protocol{ + unset req.http.X-Forwarded-Proto; + set req.http.X-Forwarded-Proto = "http"; +} + +sub detect_debug{ + # Requests with X-Varnish-Debug will display additional + # information about requests + unset req.http.x-vcl-debug; + # Should be changed after switch to live + if (req.http.x-varnish-debug) { + set req.http.x-vcl-debug = false; + } +} + +sub detect_auth{ + unset req.http.x-auth; + if ( + (req.http.Cookie && ( + req.http.Cookie ~ "__ac(_(name|password|persistent))?=" || req.http.Cookie ~ "_ZopeId" || req.http.Cookie ~ "auth_token")) || + (req.http.Authenticate) || + (req.http.Authorization) + ) { + set req.http.x-auth = true; + } +} + +sub detect_requesttype{ + unset req.http.x-varnish-reqtype; + set req.http.x-varnish-reqtype = "Default"; + if (req.http.x-auth){ + set req.http.x-varnish-reqtype = "auth"; + } elseif (req.url ~ "\/@@(images|download|)\/?(.*)?$"){ + set req.http.x-varnish-reqtype = "blob"; + } elseif (req.url ~ "\/\+\+api\+\+/?(.*)?$") { + set req.http.x-varnish-reqtype = "api"; + } else { + set req.http.x-varnish-reqtype = "express"; + } +} + +sub process_redirects{ + // Add manual redurect + if (req.url ~ "^/old-folder/(.*)") { + set req.http.x-redirect-to = regsub(req.url, "^/old-folder/(.*)", "^/new-folder/\1"); + } + + if (req.http.x-redirect-to) { + return (synth(301, req.http.x-redirect-to)); + } +} + +sub vcl_init { + new cluster_loadbalancer = directors.round_robin(); + cluster_loadbalancer.add_backend(traefik_loadbalancer); +} + +sub vcl_recv { + set req.backend_hint = cluster_loadbalancer.backend(); + set req.http.X-Varnish-Routed = "1"; + + # Annotate request with x-forwarded-proto + # We always serve requests over https, but talk to Traefik + # and then to Volto and Plone using http. + call detect_protocol; + + # Annotate request with x-vcl-debug + call detect_debug; + + # Annotate request with x-auth indicating if request is authenticated or not + call detect_auth; + + # Annotate request with x-varnish-reqtype with a classification for the request + call detect_requesttype; + + # Process redirects + call process_redirects; + + # Sanitize cookies so they do not needlessly destroy cacheability for anonymous pages + if (req.http.Cookie) { + set req.http.Cookie = ";" + req.http.Cookie; + set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";"); + set req.http.Cookie = regsuball(req.http.Cookie, ";(sticky|I18N_LANGUAGE|statusmessages|__ac|_ZopeId|__cp|beaker\.session|authomatic|serverid|__rf|auth_token)=", "; \1="); + set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", ""); + set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", ""); + + if (req.http.Cookie == "") { + unset req.http.Cookie; + } + } + + if (req.http.x-auth) { + return(pass); + } + + if (req.method == "PURGE") { + if (!client.ip ~ purge) { + return (synth(405, "Not allowed.")); + } else { + ban("req.url == " + req.url); + return (synth(200, "Purged.")); + } + + } elseif (req.method == "BAN") { + # Same ACL check as above: + if (!client.ip ~ purge) { + return (synth(405, "Not allowed.")); + } + ban("req.http.host == " + req.http.host + "&& req.url == " + req.url); + # Throw a synthetic page so the + # request won't go to the backend. + return (synth(200, "Ban added")); + + } elseif (req.method != "GET" && + req.method != "HEAD" && + req.method != "PUT" && + req.method != "POST" && + req.method != "PATCH" && + req.method != "TRACE" && + req.method != "OPTIONS" && + req.method != "DELETE") { + /* Non-RFC2616 or CONNECT which is weird. */ + return (pipe); + } elseif (req.method != "GET" && + req.method != "HEAD" && + req.method != "OPTIONS") { + /* POST, PUT, PATCH will pass, always */ + return(pass); + } + + return(hash); +} + +sub vcl_pipe { + /* This is not necessary if you do not do any request rewriting. */ + set req.http.connection = "close"; +} + +sub vcl_purge { + return (synth(200, "PURGE: " + req.url + " - " + req.hash)); +} + +sub vcl_synth { + if (resp.status == 301) { + set resp.http.location = resp.reason; + set resp.reason = "Moved"; + return (deliver); + } +} + +sub vcl_hit { + if (obj.ttl >= 0s) { + // A pure unadulterated hit, deliver it + return (deliver); + } elsif (obj.ttl + obj.grace > 0s) { + // Object is in grace, deliver it + // Automatically triggers a background fetch + return (deliver); + } else { + return (restart); + } +} + + +sub vcl_backend_response { + + # Don't allow static files to set cookies. + # (?i) denotes case insensitive in PCRE (perl compatible regular expressions). + # make sure you edit both and keep them equal. + if (bereq.url ~ "(?i)\.(pdf|asc|dat|txt|doc|xls|ppt|tgz|png|gif|jpeg|jpg|ico|swf|css|js)(\?.*)?$") { + unset beresp.http.set-cookie; + } + if (beresp.http.Set-Cookie) { + set beresp.http.x-varnish-action = "FETCH (pass - response sets cookie)"; + set beresp.uncacheable = true; + set beresp.ttl = 120s; + return(deliver); + } + if (beresp.http.Cache-Control ~ "(private|no-cache|no-store)") { + set beresp.http.x-varnish-action = "FETCH (pass - cache control disallows)"; + set beresp.uncacheable = true; + set beresp.ttl = 120s; + return(deliver); + } + + # if (beresp.http.Authorization && !beresp.http.Cache-Control ~ "public") { + # Do NOT cache if there is an "Authorization" header + # beresp never has an Authorization header in beresp, right? + if (beresp.http.Authorization) { + set beresp.http.x-varnish-action = "FETCH (pass - authorized and no public cache control)"; + set beresp.uncacheable = true; + set beresp.ttl = 120s; + return(deliver); + } + + # Use this rule IF no cache-control (SSR content) + if ((bereq.http.x-varnish-reqtype ~ "express") && (!beresp.http.Cache-Control)) { + set beresp.http.x-varnish-action = "INSERT (30s caching / 60s grace)"; + set beresp.uncacheable = false; + set beresp.ttl = 30s; + set beresp.grace = 60s; + return(deliver); + } + + if (!beresp.http.Cache-Control) { + set beresp.http.x-varnish-action = "FETCH (override - backend not setting cache control)"; + set beresp.uncacheable = true; + set beresp.ttl = 120s; + return (deliver); + } + + if (beresp.http.X-Anonymous && !beresp.http.Cache-Control) { + set beresp.http.x-varnish-action = "FETCH (override - anonymous backend not setting cache control)"; + set beresp.ttl = 600s; + return (deliver); + } + + set beresp.http.x-varnish-action = "FETCH (insert)"; + return (deliver); +} + +sub vcl_deliver { + + if (req.http.x-vcl-debug) { + set resp.http.x-varnish-ttl = obj.ttl; + set resp.http.x-varnish-grace = obj.grace; + set resp.http.x-hits = obj.hits; + set resp.http.x-varnish-reqtype = req.http.x-varnish-reqtype; + if (req.http.x-auth) { + set resp.http.x-auth = "Logged-in"; + } else { + set resp.http.x-auth = "Anon"; + } + if (obj.hits > 0) { + set resp.http.x-cache = "HIT"; + } else { + set resp.http.x-cache = "MISS"; + } + } else { + unset resp.http.x-varnish-action; + unset resp.http.x-cache-operation; + unset resp.http.x-cache-rule; + unset resp.http.x-powered-by; + } +} +``` + + +### Stack file + +Create a {file}`stack.yml` file with the following content. + +```yaml +services: + + socket-proxy: + image: tecnativa/docker-socket-proxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + NODES: 1 + SERVICES: 1 + TASKS: 1 + NETWORKS: 1 + networks: + - nw-traefik + deploy: + placement: + constraints: + - node.role == manager + + traefik: + image: traefik:${STACK_TRAEFIK_TAG:?Set STACK_TRAEFIK_TAG} + ports: + - "80:80" + - "443:443" + volumes: + - vol-traefik-certs:/certificates + command: + - --providers.swarm + - --providers.swarm.endpoint=tcp://socket-proxy:2375 + - --providers.swarm.exposedbydefault=false + - --providers.swarm.network=nw-public + - --providers.swarm.constraints=Label(`traefik.constraint-label`, `public`) + - --entrypoints.http.address=:80 + - --entrypoints.https.address=:443 + - --entrypoints.varnish.address=:8081 # requests from Varnish, not published + - --certificatesresolvers.le.acme.email=${TRAEFIK_EMAIL:?Set TRAEFIK_EMAIL} + - --certificatesresolvers.le.acme.storage=/certificates/acme.json + - --certificatesresolvers.le.acme.tlschallenge=true + - --accesslog + - --accesslog.format=json + - --log.level=INFO + - --log.format=json + - --api + networks: + - nw-public + - nw-traefik + deploy: + replicas: 1 + placement: + constraints: + - node.role == manager + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + - traefik.http.services.traefik-public.loadbalancer.server.port=8000 + + # Dashboard + - traefik.http.middlewares.admin-auth.basicauth.users=${TRAEFIK_BASIC_AUTH:?Set TRAEFIK_BASIC_AUTH} + - traefik.http.routers.traefik-dashboard.rule=Host(`${TRAEFIK_HOSTNAME:?Set TRAEFIK_HOSTNAME}`) + - traefik.http.routers.traefik-dashboard.entrypoints=https + - traefik.http.routers.traefik-dashboard.tls=true + - traefik.http.routers.traefik-dashboard.tls.certresolver=le + - traefik.http.routers.traefik-dashboard.service=api@internal + - traefik.http.routers.traefik-dashboard.middlewares=admin-auth + + # Generic middlewares + - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https + - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true + - traefik.http.middlewares.gzip.compress=true + - traefik.http.middlewares.gzip.compress.excludedcontenttypes=image/png, image/jpeg, font/woff2 + + # Redirect every HTTP request to HTTPS + - traefik.http.routers.generic-https-redirect.entrypoints=http + - traefik.http.routers.generic-https-redirect.rule=HostRegexp(`^.+$$`) + - traefik.http.routers.generic-https-redirect.priority=1 + - traefik.http.routers.generic-https-redirect.middlewares=https-redirect + + varnish: + image: varnish + configs: + - source: varnish-vcl + target: /etc/varnish/default.vcl + networks: + - nw-public + - nw-internal + deploy: + replicas: ${STACK_VARNISH_REPLICAS:-2} + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + # Service + - traefik.http.services.svc-${STACK_NAME:?Set STACK_NAME}-varnish.loadbalancer.server.port=80 + + # Middleware + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.permanent=true + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.regex=^https://${STACK_HOSTNAME_REDIRECT:?Set STACK_HOSTNAME_REDIRECT}/(.*) + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.replacement=https://${STACK_HOSTNAME:?Set STACK_HOSTNAME}/$${1} + + # Routers + ## Redirect + - traefik.http.routers.rt-${STACK_NAME}-varnish-redirect.rule=Host(`${STACK_HOSTNAME_REDIRECT}`) + - traefik.http.routers.rt-${STACK_NAME}-varnish-redirect.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-varnish-redirect.tls=true + - traefik.http.routers.rt-${STACK_NAME}-varnish-redirect.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-varnish-redirect.middlewares=mw-${STACK_NAME}-redirect + ## Public requests + - traefik.http.routers.rt-${STACK_NAME}-varnish.rule=Host(`${STACK_HOSTNAME}`) + - traefik.http.routers.rt-${STACK_NAME}-varnish.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-varnish.tls=true + - traefik.http.routers.rt-${STACK_NAME}-varnish.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-varnish.service=svc-${STACK_NAME}-varnish + - traefik.http.routers.rt-${STACK_NAME}-varnish.middlewares=gzip + + purger: + image: ghcr.io/kitconcept/cluster-purger:latest + environment: + PURGER_MODE: swarm + PURGER_SERVICE_NAME: ${STACK_NAME}_varnish + PURGER_SERVICE_PORT: 80 + PURGER_PUBLIC_SITES: "['${STACK_HOSTNAME}']" + networks: + - nw-internal + deploy: + replicas: 2 + update_config: + parallelism: 1 + delay: 5s + order: start-first + + frontend: + image: plone/plone-frontend:${STACK_FRONTEND_TAG:?Set STACK_FRONTEND_TAG} + environment: + RAZZLE_INTERNAL_API_PATH: http://${STACK_NAME}_backend:8080/Plone + RAZZLE_API_PATH: https://${STACK_HOSTNAME} + networks: + - nw-public + - nw-internal + deploy: + replicas: ${STACK_FRONTEND_REPLICAS:-2} + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + # Service + - traefik.http.services.svc-${STACK_NAME}-frontend.loadbalancer.server.port=3000 + + # Routers + ## / (requests from Varnish) + - traefik.http.routers.rt-${STACK_NAME}-frontend.rule=Host(`${STACK_HOSTNAME}`) && Header(`X-Varnish-Routed`, `1`) + - traefik.http.routers.rt-${STACK_NAME}-frontend.entrypoints=varnish + - traefik.http.routers.rt-${STACK_NAME}-frontend.service=svc-${STACK_NAME}-frontend + + backend: + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} + environment: + SITE: Plone + PROFILES: "plone.app.caching:with-caching-proxy" + ZEO_ADDRESS: "${STACK_NAME}_db:8100" + networks: + - nw-public + - nw-internal + deploy: + replicas: ${STACK_BACKEND_REPLICAS:-2} + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + # Service + - traefik.http.services.svc-${STACK_NAME}-backend.loadbalancer.server.port=8080 + + # Middlewares + ## Virtual Host Monster rewrite for /++api++/ + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-api.replacepathregex.regex=^/\\+\\+api\\+\\+($$|/.*)" + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-api.replacepathregex.replacement=/VirtualHostBase/https/${STACK_HOSTNAME}/Plone/++api++/VirtualHostRoot$$1" + ## Virtual Host Monster rewrite for /ClassicUI/ + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-classic.replacepathregex.regex=^/ClassicUI($$|/.*)" + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-classic.replacepathregex.replacement=/VirtualHostBase/https/${STACK_HOSTNAME}/Plone/VirtualHostRoot/_vh_ClassicUI$$1" + ## Basic authentication for /ClassicUI/ + - traefik.http.middlewares.mw-${STACK_NAME}-backend-auth.basicauth.users=${BASIC_AUTH_USER:?Set BASIC_AUTH_USER}:${BASIC_AUTH_PASSWORD_HASH:?Set BASIC_AUTH_PASSWORD_HASH} + + # Routers + ## /++api++ (requests from Varnish) + - traefik.http.routers.rt-${STACK_NAME}-backend-api.rule=Host(`${STACK_HOSTNAME}`) && PathPrefix(`/++api++`) && Header(`X-Varnish-Routed`, `1`) + - traefik.http.routers.rt-${STACK_NAME}-backend-api.entrypoints=varnish + - traefik.http.routers.rt-${STACK_NAME}-backend-api.service=svc-${STACK_NAME}-backend + - traefik.http.routers.rt-${STACK_NAME}-backend-api.middlewares=mw-${STACK_NAME}-backend-vhm-api + ## /ClassicUI + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.rule=Host(`${STACK_HOSTNAME}`) && PathPrefix(`/ClassicUI`) + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.tls=true + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.service=svc-${STACK_NAME}-backend + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.middlewares=gzip,mw-${STACK_NAME}-backend-auth,mw-${STACK_NAME}-backend-vhm-classic + + db: + image: plone/plone-zeo:${STACK_ZEO_TAG:?Set STACK_ZEO_TAG} + volumes: + - vol-site-data:/data + networks: + - nw-internal + deploy: + replicas: 1 + update_config: + parallelism: 1 + delay: 1s + order: stop-first + +configs: + varnish-vcl: + file: ./etc/varnish.vcl + +volumes: + vol-traefik-certs: {} + vol-site-data: {} + +networks: + nw-public: + external: true + nw-traefik: + driver: overlay + internal: true + nw-internal: + driver: overlay + internal: true +``` + +Docker Swarm stores the content of {file}`etc/varnish.vcl` under the name `varnish-vcl`, and you can't change it after you deploy the stack. +To change the Varnish configuration later, rename `varnish-vcl` in both places in {file}`stack.yml`, for example to `varnish-vcl-2`, and deploy the stack again. + +```{warning} +Docker volumes are local to the node where they're created. +In a cluster with more than one node, add a placement constraint to the `db` service, so the ZEO server always runs on the node that holds its data. +``` + + +### Environment variables + +The stack reads its configuration from the following environment variables. +Variables without a default value are required, and `docker stack deploy` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_NAME` | Name of the stack, which must match the name you pass to `docker stack deploy`. The services use it to reach each other, and Traefik uses it to name routers, services, and middleware. | | `plone` | +| `STACK_HOSTNAME` | Public host name of the site | | `www.example.com` | +| `STACK_HOSTNAME_REDIRECT` | Host name that permanently redirects to `STACK_HOSTNAME` | | `example.com` | +| `STACK_VARNISH_REPLICAS` | Number of Varnish replicas | `2` | `3` | +| `STACK_FRONTEND_REPLICAS` | Number of frontend replicas | `2` | `3` | +| `STACK_BACKEND_REPLICAS` | Number of backend replicas | `2` | `4` | +| `STACK_FRONTEND_TAG` | Tag (version) of the image for the frontend | | {{PLONE_FRONTEND_VERSION}} | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | +| `STACK_ZEO_TAG` | Tag (version) of the image for the ZEO server | | {{PLONE_ZEO_VERSION}} | +| `STACK_TRAEFIK_TAG` | Tag (version) of the image for Traefik | | {{TRAEFIK_VERSION}} | +| `TRAEFIK_HOSTNAME` | Host name of the Traefik dashboard | | `traefik.example.com` | +| `TRAEFIK_EMAIL` | Email address of your Let's Encrypt account | | `admin@example.com` | +| `TRAEFIK_BASIC_AUTH` | User name and password hash for the Traefik dashboard, in the format `user:hash` | | `admin:$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0` | +| `BASIC_AUTH_USER` | User name for the Classic UI at `/ClassicUI` | | `admin` | +| `BASIC_AUTH_PASSWORD_HASH` | Password hash for the Classic UI at `/ClassicUI` | | `$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0` | + +To create a password hash, run the following command, with your password instead of `secret`. + +```shell +openssl passwd -apr1 secret +``` + +Create a {file}`.env` file with your values. +Wrap values that contain a dollar sign, such as password hashes, in single quotes. + +```shell +STACK_NAME=plone +STACK_HOSTNAME=www.example.com +STACK_HOSTNAME_REDIRECT=example.com +STACK_FRONTEND_TAG={PLONE_FRONTEND_VERSION} +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} +STACK_ZEO_TAG={PLONE_ZEO_VERSION} +STACK_TRAEFIK_TAG={TRAEFIK_VERSION} +TRAEFIK_HOSTNAME=traefik.example.com +TRAEFIK_EMAIL=admin@example.com +TRAEFIK_BASIC_AUTH='admin:$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0' +BASIC_AUTH_USER=admin +BASIC_AUTH_PASSWORD_HASH='$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0' +``` + +`docker stack deploy` doesn't read {file}`.env` files. +Load the variables into your shell before you deploy. + +```shell +set -a +. ./.env +set +a +``` + + +## Deploy the stack + +Deploy the stack with the name from `STACK_NAME`. + +```shell +docker stack deploy -c stack.yml "$STACK_NAME" +``` + +This pulls the needed images and starts Plone. + + +## Configure cache purging + +The `plone.app.caching:with-caching-proxy` profile sets up caching rules for the site, but it doesn't set where the backend sends purge requests. +Go to the caching control panel at `https://www.example.com/ClassicUI/@@caching-controlpanel`, using your value of `STACK_HOSTNAME`, and set the following options. + +{guilabel}`Enable purging` +: Selected + +{guilabel}`Caching proxies` +: `http://purger` + +{guilabel}`Send PURGE requests with virtual hosting paths` +: Cleared + +The backend then sends purge requests to the `purger` service, which forwards them to every Varnish replica. + + +## Access Plone + +After startup, go to `https://www.example.com`, using your value of `STACK_HOSTNAME`, and you should see the site. + +The Classic UI is available at `https://www.example.com/ClassicUI`, behind the user name and password from `BASIC_AUTH_USER` and `BASIC_AUTH_PASSWORD_HASH`. + +The Traefik dashboard is available at the host name from `TRAEFIK_HOSTNAME`, behind the user name and password from `TRAEFIK_BASIC_AUTH`. + + +## Increase the number of backends + +To run four backend replicas, scale the backend service. + +```shell +docker service scale "${STACK_NAME}_backend=4" +``` + +The next `docker stack deploy` sets the number of replicas back to the value of `STACK_BACKEND_REPLICAS`. + + +## Shutdown and cleanup + +The command `docker stack rm "$STACK_NAME"` removes the services and the stack networks, but preserves the Plone database and the TLS certificates in their volumes. diff --git a/docs/install/containers/examples/swarm/traefik-volto-plone-zeo.md b/docs/install/containers/examples/swarm/traefik-volto-plone-zeo.md new file mode 100644 index 0000000000..406a482406 --- /dev/null +++ b/docs/install/containers/examples/swarm/traefik-volto-plone-zeo.md @@ -0,0 +1,360 @@ +--- +myst: + html_meta: + "description": "ZEO server, scalable frontend and backend, and Traefik in a Plone 6 stack for Docker Swarm." + "property=og:description": "ZEO server, scalable frontend and backend, and Traefik in a Plone 6 stack for Docker Swarm." + "property=og:title": "Traefik, Frontend, Backend, ZEO example for Docker Swarm" + "keywords": "Plone 6, Container, Docker, Docker Swarm, Traefik, Frontend, Backend, ZEO" +--- + +# Traefik, Frontend, Backend, ZEO example for Docker Swarm + +This example deploys Plone 6 as a stack on a Docker Swarm cluster. +The stack runs the following services. + +`traefik` +: {term}`Traefik Proxy` routes requests to the other services, and gets TLS certificates from Let's Encrypt. + +`socket-proxy` +: Gives Traefik access to the parts of the Docker API it needs, instead of the Docker socket itself. + +`frontend` +: The Plone frontend, with two replicas by default. + +`backend` +: The Plone backend, with two replicas by default, which stores its data in the ZEO server. + +`db` +: The ZEO server, with its data persisted in a Docker volume. + + +## Prerequisites + +- A Docker Swarm cluster. + To create a cluster with a single node, run `docker swarm init` on that node. +- DNS records that point the host names of `STACK_HOSTNAME`, `STACK_HOSTNAME_REDIRECT`, and `TRAEFIK_HOSTNAME` to your cluster. +- Ports 80 and 443 open to the internet, so Let's Encrypt can validate your host names. + + +## Setup + +Create an empty project directory named {file}`swarm-traefik-volto-plone-zeo`. + +```shell +mkdir swarm-traefik-volto-plone-zeo +``` + +Change into your project directory. + +```shell +cd swarm-traefik-volto-plone-zeo +``` + + +### Create the public network + +Traefik and the services it routes requests to share an overlay network named `nw-public`. +Create it once on a manager node, before you deploy the stack. + +```shell +docker network create --driver overlay nw-public +``` + + +### Stack file + +Create a {file}`stack.yml` file with the following content. + +```yaml +services: + + socket-proxy: + image: tecnativa/docker-socket-proxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + NODES: 1 + SERVICES: 1 + TASKS: 1 + NETWORKS: 1 + networks: + - nw-traefik + deploy: + placement: + constraints: + - node.role == manager + + traefik: + image: traefik:${STACK_TRAEFIK_TAG:?Set STACK_TRAEFIK_TAG} + ports: + - "80:80" + - "443:443" + volumes: + - vol-traefik-certs:/certificates + command: + - --providers.swarm + - --providers.swarm.endpoint=tcp://socket-proxy:2375 + - --providers.swarm.exposedbydefault=false + - --providers.swarm.network=nw-public + - --providers.swarm.constraints=Label(`traefik.constraint-label`, `public`) + - --entrypoints.http.address=:80 + - --entrypoints.https.address=:443 + - --certificatesresolvers.le.acme.email=${TRAEFIK_EMAIL:?Set TRAEFIK_EMAIL} + - --certificatesresolvers.le.acme.storage=/certificates/acme.json + - --certificatesresolvers.le.acme.tlschallenge=true + - --accesslog + - --accesslog.format=json + - --log.level=INFO + - --log.format=json + - --api + networks: + - nw-public + - nw-traefik + deploy: + replicas: 1 + placement: + constraints: + - node.role == manager + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + - traefik.http.services.traefik-public.loadbalancer.server.port=8000 + + # Dashboard + - traefik.http.middlewares.admin-auth.basicauth.users=${TRAEFIK_BASIC_AUTH:?Set TRAEFIK_BASIC_AUTH} + - traefik.http.routers.traefik-dashboard.rule=Host(`${TRAEFIK_HOSTNAME:?Set TRAEFIK_HOSTNAME}`) + - traefik.http.routers.traefik-dashboard.entrypoints=https + - traefik.http.routers.traefik-dashboard.tls=true + - traefik.http.routers.traefik-dashboard.tls.certresolver=le + - traefik.http.routers.traefik-dashboard.service=api@internal + - traefik.http.routers.traefik-dashboard.middlewares=admin-auth + + # Generic middlewares + - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https + - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true + - traefik.http.middlewares.gzip.compress=true + - traefik.http.middlewares.gzip.compress.excludedcontenttypes=image/png, image/jpeg, font/woff2 + + # Redirect every HTTP request to HTTPS + - traefik.http.routers.generic-https-redirect.entrypoints=http + - traefik.http.routers.generic-https-redirect.rule=HostRegexp(`^.+$$`) + - traefik.http.routers.generic-https-redirect.priority=1 + - traefik.http.routers.generic-https-redirect.middlewares=https-redirect + + frontend: + image: plone/plone-frontend:${STACK_FRONTEND_TAG:?Set STACK_FRONTEND_TAG} + environment: + RAZZLE_INTERNAL_API_PATH: http://${STACK_NAME:?Set STACK_NAME}_backend:8080/Plone + RAZZLE_API_PATH: https://${STACK_HOSTNAME:?Set STACK_HOSTNAME} + networks: + - nw-public + - nw-internal + deploy: + replicas: ${STACK_FRONTEND_REPLICAS:-2} + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + # Service + - traefik.http.services.svc-${STACK_NAME}-frontend.loadbalancer.server.port=3000 + + # Middleware + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.permanent=true + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.regex=^https://${STACK_HOSTNAME_REDIRECT:?Set STACK_HOSTNAME_REDIRECT}/(.*) + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.replacement=https://${STACK_HOSTNAME}/$${1} + + # Routers + ## Redirect + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.rule=Host(`${STACK_HOSTNAME_REDIRECT}`) + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.tls=true + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.middlewares=mw-${STACK_NAME}-redirect + ## / + - traefik.http.routers.rt-${STACK_NAME}-frontend.rule=Host(`${STACK_HOSTNAME}`) + - traefik.http.routers.rt-${STACK_NAME}-frontend.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-frontend.tls=true + - traefik.http.routers.rt-${STACK_NAME}-frontend.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-frontend.service=svc-${STACK_NAME}-frontend + - traefik.http.routers.rt-${STACK_NAME}-frontend.middlewares=gzip + + backend: + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} + environment: + SITE: Plone + ZEO_ADDRESS: "${STACK_NAME}_db:8100" + networks: + - nw-public + - nw-internal + deploy: + replicas: ${STACK_BACKEND_REPLICAS:-2} + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + # Service + - traefik.http.services.svc-${STACK_NAME}-backend.loadbalancer.server.port=8080 + + # Middlewares + ## Virtual Host Monster rewrite for /++api++/ + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-api.replacepathregex.regex=^/\\+\\+api\\+\\+($$|/.*)" + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-api.replacepathregex.replacement=/VirtualHostBase/https/${STACK_HOSTNAME}/Plone/++api++/VirtualHostRoot$$1" + ## Virtual Host Monster rewrite for /ClassicUI/ + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-classic.replacepathregex.regex=^/ClassicUI($$|/.*)" + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-classic.replacepathregex.replacement=/VirtualHostBase/https/${STACK_HOSTNAME}/Plone/VirtualHostRoot/_vh_ClassicUI$$1" + ## Basic authentication for /ClassicUI/ + - traefik.http.middlewares.mw-${STACK_NAME}-backend-auth.basicauth.users=${BASIC_AUTH_USER:?Set BASIC_AUTH_USER}:${BASIC_AUTH_PASSWORD_HASH:?Set BASIC_AUTH_PASSWORD_HASH} + + # Routers + ## /++api++ + - traefik.http.routers.rt-${STACK_NAME}-backend-api.rule=Host(`${STACK_HOSTNAME}`) && PathPrefix(`/++api++`) + - traefik.http.routers.rt-${STACK_NAME}-backend-api.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-backend-api.tls=true + - traefik.http.routers.rt-${STACK_NAME}-backend-api.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-backend-api.service=svc-${STACK_NAME}-backend + - traefik.http.routers.rt-${STACK_NAME}-backend-api.middlewares=gzip,mw-${STACK_NAME}-backend-vhm-api + ## /ClassicUI + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.rule=Host(`${STACK_HOSTNAME}`) && PathPrefix(`/ClassicUI`) + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.tls=true + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.service=svc-${STACK_NAME}-backend + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.middlewares=gzip,mw-${STACK_NAME}-backend-auth,mw-${STACK_NAME}-backend-vhm-classic + + db: + image: plone/plone-zeo:${STACK_ZEO_TAG:?Set STACK_ZEO_TAG} + volumes: + - vol-site-data:/data + networks: + - nw-internal + deploy: + replicas: 1 + update_config: + parallelism: 1 + delay: 1s + order: stop-first + +volumes: + vol-traefik-certs: {} + vol-site-data: {} + +networks: + nw-public: + external: true + nw-traefik: + driver: overlay + internal: true + nw-internal: + driver: overlay + internal: true +``` + +Only the `db` service mounts the data volume. +The backends don't share the blob directory of the ZEO server, because `ZEO_SHARED_BLOB_DIR` is `off` by default, so they get blobs from the ZEO server through the network. + +```{warning} +Docker volumes are local to the node where they're created. +In a cluster with more than one node, add a placement constraint to the `db` service, so the ZEO server always runs on the node that holds its data. +``` + + +### Environment variables + +The stack reads its configuration from the following environment variables. +Variables without a default value are required, and `docker stack deploy` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_NAME` | Name of the stack, which must match the name you pass to `docker stack deploy`. The services use it to reach each other, and Traefik uses it to name routers, services, and middleware. | | `plone` | +| `STACK_HOSTNAME` | Public host name of the site | | `www.example.com` | +| `STACK_HOSTNAME_REDIRECT` | Host name that permanently redirects to `STACK_HOSTNAME` | | `example.com` | +| `STACK_FRONTEND_REPLICAS` | Number of frontend replicas | `2` | `3` | +| `STACK_BACKEND_REPLICAS` | Number of backend replicas | `2` | `4` | +| `STACK_FRONTEND_TAG` | Tag (version) of the image for the frontend | | {{PLONE_FRONTEND_VERSION}} | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | +| `STACK_ZEO_TAG` | Tag (version) of the image for the ZEO server | | {{PLONE_ZEO_VERSION}} | +| `STACK_TRAEFIK_TAG` | Tag (version) of the image for Traefik | | {{TRAEFIK_VERSION}} | +| `TRAEFIK_HOSTNAME` | Host name of the Traefik dashboard | | `traefik.example.com` | +| `TRAEFIK_EMAIL` | Email address of your Let's Encrypt account | | `admin@example.com` | +| `TRAEFIK_BASIC_AUTH` | User name and password hash for the Traefik dashboard, in the format `user:hash` | | `admin:$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0` | +| `BASIC_AUTH_USER` | User name for the Classic UI at `/ClassicUI` | | `admin` | +| `BASIC_AUTH_PASSWORD_HASH` | Password hash for the Classic UI at `/ClassicUI` | | `$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0` | + +To create a password hash, run the following command, with your password instead of `secret`. + +```shell +openssl passwd -apr1 secret +``` + +Create a {file}`.env` file with your values. +Wrap values that contain a dollar sign, such as password hashes, in single quotes. + +```shell +STACK_NAME=plone +STACK_HOSTNAME=www.example.com +STACK_HOSTNAME_REDIRECT=example.com +STACK_FRONTEND_TAG={PLONE_FRONTEND_VERSION} +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} +STACK_ZEO_TAG={PLONE_ZEO_VERSION} +STACK_TRAEFIK_TAG={TRAEFIK_VERSION} +TRAEFIK_HOSTNAME=traefik.example.com +TRAEFIK_EMAIL=admin@example.com +TRAEFIK_BASIC_AUTH='admin:$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0' +BASIC_AUTH_USER=admin +BASIC_AUTH_PASSWORD_HASH='$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0' +``` + +`docker stack deploy` doesn't read {file}`.env` files. +Load the variables into your shell before you deploy. + +```shell +set -a +. ./.env +set +a +``` + + +## Deploy the stack + +Deploy the stack with the name from `STACK_NAME`. + +```shell +docker stack deploy -c stack.yml "$STACK_NAME" +``` + +This pulls the needed images and starts Plone. + + +## Access Plone + +After startup, go to `https://www.example.com`, using your value of `STACK_HOSTNAME`, and you should see the site. + +The Classic UI is available at `https://www.example.com/ClassicUI`, behind the user name and password from `BASIC_AUTH_USER` and `BASIC_AUTH_PASSWORD_HASH`. + +The Traefik dashboard is available at the host name from `TRAEFIK_HOSTNAME`, behind the user name and password from `TRAEFIK_BASIC_AUTH`. + + +## Increase the number of backends + +To run four backend replicas, scale the backend service. + +```shell +docker service scale "${STACK_NAME}_backend=4" +``` + +The next `docker stack deploy` sets the number of replicas back to the value of `STACK_BACKEND_REPLICAS`. + + +## Shutdown and cleanup + +The command `docker stack rm "$STACK_NAME"` removes the services and the stack networks, but preserves the Plone database and the TLS certificates in their volumes. diff --git a/docs/install/containers/examples/swarm/traefik-volto-plone.md b/docs/install/containers/examples/swarm/traefik-volto-plone.md new file mode 100644 index 0000000000..9c27c4559f --- /dev/null +++ b/docs/install/containers/examples/swarm/traefik-volto-plone.md @@ -0,0 +1,332 @@ +--- +myst: + html_meta: + "description": "Scalable frontend, backend, and Traefik in a Plone 6 stack for Docker Swarm, with data stored in a Docker volume." + "property=og:description": "Scalable frontend, backend, and Traefik in a Plone 6 stack for Docker Swarm, with data stored in a Docker volume." + "property=og:title": "Traefik, Frontend, Backend example for Docker Swarm" + "keywords": "Plone 6, Container, Docker, Docker Swarm, Traefik, Frontend, Backend" +--- + +# Traefik, Frontend, Backend example for Docker Swarm + +This example deploys Plone 6 as a stack on a Docker Swarm cluster. +The stack runs the following services. + +`traefik` +: {term}`Traefik Proxy` routes requests to the other services, and gets TLS certificates from Let's Encrypt. + +`socket-proxy` +: Gives Traefik access to the parts of the Docker API it needs, instead of the Docker socket itself. + +`frontend` +: The Plone frontend, with two replicas by default. + +`backend` +: The Plone backend, with its data persisted in a Docker volume. + + +## Prerequisites + +- A Docker Swarm cluster. + To create a cluster with a single node, run `docker swarm init` on that node. +- DNS records that point the host names of `STACK_HOSTNAME`, `STACK_HOSTNAME_REDIRECT`, and `TRAEFIK_HOSTNAME` to your cluster. +- Ports 80 and 443 open to the internet, so Let's Encrypt can validate your host names. + + +## Setup + +Create an empty project directory named {file}`swarm-traefik-volto-plone`. + +```shell +mkdir swarm-traefik-volto-plone +``` + +Change into your project directory. + +```shell +cd swarm-traefik-volto-plone +``` + + +### Create the public network + +Traefik and the services it routes requests to share an overlay network named `nw-public`. +Create it once on a manager node, before you deploy the stack. + +```shell +docker network create --driver overlay nw-public +``` + + +### Stack file + +Create a {file}`stack.yml` file with the following content. + +```yaml +services: + + socket-proxy: + image: tecnativa/docker-socket-proxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + NODES: 1 + SERVICES: 1 + TASKS: 1 + NETWORKS: 1 + networks: + - nw-traefik + deploy: + placement: + constraints: + - node.role == manager + + traefik: + image: traefik:${STACK_TRAEFIK_TAG:?Set STACK_TRAEFIK_TAG} + ports: + - "80:80" + - "443:443" + volumes: + - vol-traefik-certs:/certificates + command: + - --providers.swarm + - --providers.swarm.endpoint=tcp://socket-proxy:2375 + - --providers.swarm.exposedbydefault=false + - --providers.swarm.network=nw-public + - --providers.swarm.constraints=Label(`traefik.constraint-label`, `public`) + - --entrypoints.http.address=:80 + - --entrypoints.https.address=:443 + - --certificatesresolvers.le.acme.email=${TRAEFIK_EMAIL:?Set TRAEFIK_EMAIL} + - --certificatesresolvers.le.acme.storage=/certificates/acme.json + - --certificatesresolvers.le.acme.tlschallenge=true + - --accesslog + - --accesslog.format=json + - --log.level=INFO + - --log.format=json + - --api + networks: + - nw-public + - nw-traefik + deploy: + replicas: 1 + placement: + constraints: + - node.role == manager + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + - traefik.http.services.traefik-public.loadbalancer.server.port=8000 + + # Dashboard + - traefik.http.middlewares.admin-auth.basicauth.users=${TRAEFIK_BASIC_AUTH:?Set TRAEFIK_BASIC_AUTH} + - traefik.http.routers.traefik-dashboard.rule=Host(`${TRAEFIK_HOSTNAME:?Set TRAEFIK_HOSTNAME}`) + - traefik.http.routers.traefik-dashboard.entrypoints=https + - traefik.http.routers.traefik-dashboard.tls=true + - traefik.http.routers.traefik-dashboard.tls.certresolver=le + - traefik.http.routers.traefik-dashboard.service=api@internal + - traefik.http.routers.traefik-dashboard.middlewares=admin-auth + + # Generic middlewares + - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https + - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true + - traefik.http.middlewares.gzip.compress=true + - traefik.http.middlewares.gzip.compress.excludedcontenttypes=image/png, image/jpeg, font/woff2 + + # Redirect every HTTP request to HTTPS + - traefik.http.routers.generic-https-redirect.entrypoints=http + - traefik.http.routers.generic-https-redirect.rule=HostRegexp(`^.+$$`) + - traefik.http.routers.generic-https-redirect.priority=1 + - traefik.http.routers.generic-https-redirect.middlewares=https-redirect + + frontend: + image: plone/plone-frontend:${STACK_FRONTEND_TAG:?Set STACK_FRONTEND_TAG} + environment: + RAZZLE_INTERNAL_API_PATH: http://${STACK_NAME:?Set STACK_NAME}_backend:8080/Plone + RAZZLE_API_PATH: https://${STACK_HOSTNAME:?Set STACK_HOSTNAME} + networks: + - nw-public + - nw-internal + deploy: + replicas: ${STACK_FRONTEND_REPLICAS:-2} + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - traefik.enable=true + - traefik.constraint-label=public + # Service + - traefik.http.services.svc-${STACK_NAME}-frontend.loadbalancer.server.port=3000 + + # Middleware + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.permanent=true + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.regex=^https://${STACK_HOSTNAME_REDIRECT:?Set STACK_HOSTNAME_REDIRECT}/(.*) + - traefik.http.middlewares.mw-${STACK_NAME}-redirect.redirectregex.replacement=https://${STACK_HOSTNAME}/$${1} + + # Routers + ## Redirect + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.rule=Host(`${STACK_HOSTNAME_REDIRECT}`) + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.tls=true + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-frontend-redirect.middlewares=mw-${STACK_NAME}-redirect + ## / + - traefik.http.routers.rt-${STACK_NAME}-frontend.rule=Host(`${STACK_HOSTNAME}`) + - traefik.http.routers.rt-${STACK_NAME}-frontend.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-frontend.tls=true + - traefik.http.routers.rt-${STACK_NAME}-frontend.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-frontend.service=svc-${STACK_NAME}-frontend + - traefik.http.routers.rt-${STACK_NAME}-frontend.middlewares=gzip + + backend: + image: plone/plone-backend:${STACK_BACKEND_TAG:?Set STACK_BACKEND_TAG} + environment: + SITE: Plone + volumes: + - vol-site-data:/data + networks: + - nw-public + - nw-internal + deploy: + replicas: 1 + update_config: + parallelism: 1 + delay: 5s + order: stop-first # only one process can open the database + labels: + - traefik.enable=true + - traefik.constraint-label=public + # Service + - traefik.http.services.svc-${STACK_NAME}-backend.loadbalancer.server.port=8080 + + # Middlewares + ## Virtual Host Monster rewrite for /++api++/ + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-api.replacepathregex.regex=^/\\+\\+api\\+\\+($$|/.*)" + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-api.replacepathregex.replacement=/VirtualHostBase/https/${STACK_HOSTNAME}/Plone/++api++/VirtualHostRoot$$1" + ## Virtual Host Monster rewrite for /ClassicUI/ + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-classic.replacepathregex.regex=^/ClassicUI($$|/.*)" + - "traefik.http.middlewares.mw-${STACK_NAME}-backend-vhm-classic.replacepathregex.replacement=/VirtualHostBase/https/${STACK_HOSTNAME}/Plone/VirtualHostRoot/_vh_ClassicUI$$1" + ## Basic authentication for /ClassicUI/ + - traefik.http.middlewares.mw-${STACK_NAME}-backend-auth.basicauth.users=${BASIC_AUTH_USER:?Set BASIC_AUTH_USER}:${BASIC_AUTH_PASSWORD_HASH:?Set BASIC_AUTH_PASSWORD_HASH} + + # Routers + ## /++api++ + - traefik.http.routers.rt-${STACK_NAME}-backend-api.rule=Host(`${STACK_HOSTNAME}`) && PathPrefix(`/++api++`) + - traefik.http.routers.rt-${STACK_NAME}-backend-api.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-backend-api.tls=true + - traefik.http.routers.rt-${STACK_NAME}-backend-api.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-backend-api.service=svc-${STACK_NAME}-backend + - traefik.http.routers.rt-${STACK_NAME}-backend-api.middlewares=gzip,mw-${STACK_NAME}-backend-vhm-api + ## /ClassicUI + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.rule=Host(`${STACK_HOSTNAME}`) && PathPrefix(`/ClassicUI`) + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.entrypoints=https + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.tls=true + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.tls.certresolver=le + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.service=svc-${STACK_NAME}-backend + - traefik.http.routers.rt-${STACK_NAME}-backend-classic.middlewares=gzip,mw-${STACK_NAME}-backend-auth,mw-${STACK_NAME}-backend-vhm-classic + +volumes: + vol-traefik-certs: {} + vol-site-data: {} + +networks: + nw-public: + external: true + nw-traefik: + driver: overlay + internal: true + nw-internal: + driver: overlay + internal: true +``` + +```{warning} +The backend stores its data in a Docker volume, and only one backend process can open that data at a time. +Keep the `backend` service at one replica. +To run more than one backend, use the {doc}`ZEO ` or {doc}`PostgreSQL ` example instead. + +Docker volumes are also local to the node where they're created. +In a cluster with more than one node, add a placement constraint to the `backend` service, so it always runs on the node that holds its data. +``` + + +### Environment variables + +The stack reads its configuration from the following environment variables. +Variables without a default value are required, and `docker stack deploy` stops with an error if one of them is missing. + +| Variable | Description | Default value | Example | +| --- | --- | --- | --- | +| `STACK_NAME` | Name of the stack, which must match the name you pass to `docker stack deploy`. The services use it to reach each other, and Traefik uses it to name routers, services, and middleware. | | `plone` | +| `STACK_HOSTNAME` | Public host name of the site | | `www.example.com` | +| `STACK_HOSTNAME_REDIRECT` | Host name that permanently redirects to `STACK_HOSTNAME` | | `example.com` | +| `STACK_FRONTEND_REPLICAS` | Number of frontend replicas | `2` | `3` | +| `STACK_FRONTEND_TAG` | Tag (version) of the image for the frontend | | {{PLONE_FRONTEND_VERSION}} | +| `STACK_BACKEND_TAG` | Tag (version) of the image for the backend | | {{PLONE_BACKEND_MINOR_VERSION}} | +| `STACK_TRAEFIK_TAG` | Tag (version) of the image for Traefik | | {{TRAEFIK_VERSION}} | +| `TRAEFIK_HOSTNAME` | Host name of the Traefik dashboard | | `traefik.example.com` | +| `TRAEFIK_EMAIL` | Email address of your Let's Encrypt account | | `admin@example.com` | +| `TRAEFIK_BASIC_AUTH` | User name and password hash for the Traefik dashboard, in the format `user:hash` | | `admin:$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0` | +| `BASIC_AUTH_USER` | User name for the Classic UI at `/ClassicUI` | | `admin` | +| `BASIC_AUTH_PASSWORD_HASH` | Password hash for the Classic UI at `/ClassicUI` | | `$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0` | + +To create a password hash, run the following command, with your password instead of `secret`. + +```shell +openssl passwd -apr1 secret +``` + +Create a {file}`.env` file with your values. +Wrap values that contain a dollar sign, such as password hashes, in single quotes. + +```shell +STACK_NAME=plone +STACK_HOSTNAME=www.example.com +STACK_HOSTNAME_REDIRECT=example.com +STACK_FRONTEND_TAG={PLONE_FRONTEND_VERSION} +STACK_BACKEND_TAG={PLONE_BACKEND_MINOR_VERSION} +STACK_TRAEFIK_TAG={TRAEFIK_VERSION} +TRAEFIK_HOSTNAME=traefik.example.com +TRAEFIK_EMAIL=admin@example.com +TRAEFIK_BASIC_AUTH='admin:$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0' +BASIC_AUTH_USER=admin +BASIC_AUTH_PASSWORD_HASH='$apr1$Zq3mQ2vH$IX.Ug0PreO6h.m494Bn9L0' +``` + +`docker stack deploy` doesn't read {file}`.env` files. +Load the variables into your shell before you deploy. + +```shell +set -a +. ./.env +set +a +``` + + +## Deploy the stack + +Deploy the stack with the name from `STACK_NAME`. + +```shell +docker stack deploy -c stack.yml "$STACK_NAME" +``` + +This pulls the needed images and starts Plone. + + +## Access Plone + +After startup, go to `https://www.example.com`, using your value of `STACK_HOSTNAME`, and you should see the site. + +The Classic UI is available at `https://www.example.com/ClassicUI`, behind the user name and password from `BASIC_AUTH_USER` and `BASIC_AUTH_PASSWORD_HASH`. + +The Traefik dashboard is available at the host name from `TRAEFIK_HOSTNAME`, behind the user name and password from `TRAEFIK_BASIC_AUTH`. + + +## Shutdown and cleanup + +The command `docker stack rm "$STACK_NAME"` removes the services and the stack networks, but preserves the Plone database and the TLS certificates in their volumes. diff --git a/docs/install/containers/images/aurora.md b/docs/install/containers/images/aurora.md new file mode 100644 index 0000000000..bbe2be681d --- /dev/null +++ b/docs/install/containers/images/aurora.md @@ -0,0 +1,160 @@ +--- +myst: + html_meta: + "description": "Using the plone/aurora image" + "property=og:description": "Using the plone/aurora image" + "property=og:title": "Plone Aurora image" + "keywords": "Plone 6, install, installation, docker, containers, Aurora, frontend, plone/aurora" +--- + +# `plone/aurora` + +This chapter covers the container images for [Aurora](https://github.com/plone/aurora), the future React frontend of Plone. +Aurora requires a Plone backend to be running and accessible. + +% TODO: Remove this admonition when Aurora has a final release. + +```{important} +Aurora has not reached a final release yet. +Evaluate it before you use it in production. +``` + + +## Using this image + + +### Simple usage + +Create a network, and start a Plone backend with a site named `Plone` on it. + +```shell +docker network create plone +docker run -d --name backend --network plone -e SITE=Plone plone/plone-backend:{PLONE_BACKEND_MINOR_VERSION} +``` + +Start Aurora on the same network, and point it at the backend. + +```shell +docker run -d --name aurora --network plone -p 3000:3000 -e PLONE_API_PATH=http://backend:8080/Plone plone/aurora:latest +``` + +Then point your browser at `http://localhost:3000`. + +If your Plone backend runs on your computer instead of in a container, use `host.docker.internal` to reach it from the Aurora container. + +```shell +docker run -d --name aurora -p 3000:3000 -e PLONE_API_PATH=http://host.docker.internal:8080/Plone plone/aurora:latest +``` + +% TODO: Confirm whether Linux hosts need `--add-host=host.docker.internal:host-gateway` for this example. + + +### Service configuration with Docker Compose + +Create a directory for your project, and inside it create a {file}`docker-compose.yml` file with the following content. + +```yaml +services: + + backend: + image: plone/plone-backend:{PLONE_BACKEND_MINOR_VERSION} + environment: + SITE: Plone + volumes: + - data:/data + + frontend: + image: plone/aurora:latest + environment: + PLONE_API_PATH: http://backend:8080/Plone + ports: + - "3000:3000" + depends_on: + - backend + +volumes: + data: {} +``` + +Now run `docker compose up -d` from your project directory. + +Point your browser at `http://localhost:3000`, and you should see your Plone site. +Until the backend finishes starting, Aurora returns an error page. + +The [{file}`examples` directory of the `plone/container-aurora` repository](https://github.com/plone/container-aurora/tree/main/examples) contains more complete configurations, with a web server in front of Aurora, and with ZEO or PostgreSQL as the database. + + +## Configuration variables + +| Environment variable | Description | Default value | +| --- | --- | --- | +| `PLONE_API_PATH` | Address of the Plone site that Aurora uses as its backend | `http://localhost:8080/Plone` | +| `COOKIE_SECRET` | Secret that signs the authentication cookie | `default` | +| `PORT` | Port where Aurora listens | `3000` | +| `HOST` | Network address where Aurora listens | All network interfaces | + +Always set `COOKIE_SECRET` to a long, random value in production. +Without it, Aurora signs the authentication cookie with a publicly known value, and logs a warning at startup. + +Aurora runs in production mode, and marks its authentication cookie as secure. +Serve Aurora over HTTPS, so browsers keep the login session. + + +## Images + +Each Aurora release publishes the following images, for the `linux/amd64` and `linux/arm64` platforms. + +| Image | Description | +| --- | --- | +| `plone/aurora` | Aurora, ready to run in production | +| `plone/aurora-builder` | An Aurora project with its dependencies installed, used to build images | +| `plone/aurora-dev` | The Aurora development server, which also exposes Storybook on port 6006 | +| `plone/aurora-prod-config` | A minimal Node.js runtime, used as the base of production images | + + +## Extending from this image + +The `plone/aurora` image is built in two stages. +The first stage builds Aurora in `plone/aurora-builder`, and the second copies the result onto `plone/aurora-prod-config`. +Use the same structure to build your own image. + +In a directory, create a {file}`Dockerfile` file. + +```Dockerfile +# syntax=docker/dockerfile:1 +FROM plone/aurora-builder:latest AS builder + +RUN <` maintained by the Plone community. -Also see some [examples](examples/index) of how to use the Official Images to bootstrap your projects. +Also see some {doc}`examples ` of how to use the container images to bootstrap your projects. diff --git a/docs/install/containers/recipes/index.md b/docs/install/containers/recipes/index.md index 8610d2bc6f..35401e1324 100644 --- a/docs/install/containers/recipes/index.md +++ b/docs/install/containers/recipes/index.md @@ -14,7 +14,7 @@ This chapter offers some useful recipes when working with Plone containers. ## Remove access log from Plone containers -When you generate a project using [Cookieplone](https://github.com/plone/cookieplone), it creates Plone containers for your project that are based on the official [`plone/plone-backend`](https://github.com/plone/plone-backend) images. +When you generate a project using [Cookieplone](https://github.com/plone/cookieplone), it creates Plone containers for your project that are based on the official [`plone/plone-backend`](https://github.com/plone/container-backend) images. When you run your container or the official `plone/plone-backend` image with logging, the output mixes both the event log and the access log, making it hard to follow the logs you may have added to your application. In such cases, you may have a Docker Compose setup with several components including a proxy server that already provides access logs. @@ -97,7 +97,7 @@ level = INFO formatter = generic ``` -Comparing this file with the [original `zope.ini` file](https://github.com/plone/plone-backend/blob/6.1.x/skeleton/etc/zope.ini) that comes with the `plone/plone-backend` container, you may realize that the only change is the `translogger` configuration was removed from the `pipeline` section. +Comparing this file with the [original {file}`zope.ini` file](https://github.com/plone/container-backend/blob/6.1.x/skeleton/etc/zope.ini) that comes with the `plone/plone-backend` container, you may realize that the only change is the `translogger` configuration was removed from the `pipeline` section. This [`translogger` middleware produces logs in the Apache Combined Log Format](https://docs.pylonsproject.org/projects/waitress/en/latest/logging.html). The above configuration removes it from the setup. diff --git a/styles/config/vocabularies/Plone/accept.txt b/styles/config/vocabularies/Plone/accept.txt index c0b583ac1c..80345051e2 100644 --- a/styles/config/vocabularies/Plone/accept.txt +++ b/styles/config/vocabularies/Plone/accept.txt @@ -4,6 +4,7 @@ accessor APIs [Aa]sync +Aurora [Aa]utosave [Bb]ackend backport(ed|ing) @@ -24,15 +25,18 @@ extranets folderish fieldset getter +HAProxy interoperate JavaScript [Jj]enkins jQuery +Let's Encrypt libxslt middleware Mockup mxdev namespaces? +nginx npm nvm Pastanaga @@ -42,6 +46,7 @@ Plate PLIP(s) Plone plonecli +Podman pluggab(le|ility) pnpm [Pp]ortlets? @@ -57,6 +62,7 @@ Schuko subfolder toggler [Tt]owncrier +Traefik transpilation transpile[drs]{0,1} [Uu]ncomment