craftcms / cms

Build bespoke content experiences with Craft.
https://craftcms.com
Other
3.22k stars 624 forks source link

Run Craft behind a reverse proxy on subpath #9909

Open jan-thoma opened 2 years ago

jan-thoma commented 2 years ago

Description

When running craft behind a reverse proxy in this manner:

/ => frontend
/backend => Craft

Calls to index.php always resolve to the root of the domain and therefore fail. Is there a way to solve this problem. Without placing craft in that subdirectory. Background we run Craft and SSR Vue.js Docker Containers along. To avoid CORS Issues we would like to serve the Craft container from a subpath of the domain.

Additional info

timkelty commented 2 years ago

This seems like it would be addressed by web server. Can you provide some more info about your reverse proxy/web sever configuration?

Without placing craft in that subdirectory

when you say "placing craft", do you mean just index.php ?

jan-thoma commented 2 years ago

It's docker setup with traefik in front, with Craft, Vue and MySQL running in their separate containers. Traefik routes everything except urls starting with `/backend to the Vue container.

Urls starting with /backend will be routed to the craft container, traefik strips /backend part. The @web path is set to e.g. my.domain.com/backend.

What's working:

What's not working:

timkelty commented 2 years ago

@jan-thoma are you able to share anything where I could reproduce the behavior (eg dockerfiles/docker-compose)?

jan-thoma commented 2 years ago
version: '3.2'
services:
    viiv-podcast-node:
        image: registry.io.t-k-f.ch/viiv/viiv-podcast/viiv-podcast-frontend:latest
        restart: always
        networks:
            - external
        depends_on:
            - viiv-podcast-craft
        labels:
            - "traefik.enable=true"
            - "traefik.docker.network=${TRAEFIK_NETWORK}"

            - "traefik.http.routers.${TRAEFIK_NAME_FRONTEND}.rule=Host(${TRAEFIK_HOST})"
            - "traefik.http.routers.${TRAEFIK_NAME_FRONTEND}.entrypoints=websecure"
            - "traefik.http.routers.${TRAEFIK_NAME_FRONTEND}.tls.certresolver=letsencrypt-tls"
            - "traefik.http.routers.${TRAEFIK_NAME_FRONTEND}.middlewares=${TRAEFIK_NAME_FRONTEND}-www-to-non-www"

            - "traefik.http.middlewares.${TRAEFIK_NAME_FRONTEND}-www-to-non-www.redirectregex.regex=^https://www.(.*)"
            - "traefik.http.middlewares.${TRAEFIK_NAME_FRONTEND}-www-to-non-www.redirectregex.replacement=https://$${1}"
            - "traefik.http.middlewares.${TRAEFIK_NAME_FRONTEND}-www-to-non-www.redirectregex.permanent=true"

            - "traefik.http.services.${TRAEFIK_NAME_FRONTEND}.loadbalancer.server.port=${TRAEFIK_PORT_FRONTEND}"
        environment:
            - VITE_APP_BACKEND_FQDN=${BACKEND_URL}
            - VITE_APP_CACHE_PAGES=${VITE_APP_CACHE_PAGES}
            - VITE_APP_CACHE_AGE=${VITE_APP_CACHE_AGE}
    viiv-podcast-craft:
        image: registry.io.t-k-f.ch/viiv/viiv-podcast/viiv-podcast-backend:latest
        restart: always
        hostname: fuckyou
        networks:
            - external
            - internal
        volumes:
            - backend:/app/web/data
        depends_on:
            - viiv-podcast-mysql
        labels:
            - "traefik.enable=true"
            - "traefik.docker.network=${TRAEFIK_NETWORK}"

            - "traefik.http.routers.${TRAEFIK_NAME_BACKEND}.rule=Host(${TRAEFIK_HOST}) && PathPrefix(`/backend`)"
            - "traefik.http.routers.${TRAEFIK_NAME_BACKEND}.entrypoints=websecure"
            - "traefik.http.routers.${TRAEFIK_NAME_BACKEND}.tls.certresolver=letsencrypt-tls"
            - "traefik.http.routers.${TRAEFIK_NAME_BACKEND}.middlewares=${TRAEFIK_NAME_BACKEND}-www-to-non-www"
            - "traefik.http.routers.${TRAEFIK_NAME_FRONTEND}.middlewares=${TRAEFIK_NAME_FRONTEND}-stripprefix"

            - "traefik.http.middlewares.${TRAEFIK_NAME_BACKEND}-www-to-non-www.redirectregex.regex=^https://www.(.*)"
            - "traefik.http.middlewares.${TRAEFIK_NAME_BACKEND}-www-to-non-www.redirectregex.replacement=https://$${1}"
            - "traefik.http.middlewares.${TRAEFIK_NAME_BACKEND}-www-to-non-www.redirectregex.permanent=true"
            - "traefik.http.middlewares.${TRAEFIK_NAME_BACKEND}-stripprefix.stripprefix.prefixes=/backend"
        environment:
            - ENVIRONMENT=${ENVIRONMENT}
            - APP_ID=${APP_ID}

            - DB_DRIVER=${DB_DRIVER}
            - DB_SERVER=${DB_SERVER}
            - DB_PORT=${DB_PORT}
            - DB_DATABASE=${DB_DATABASE}
            - DB_USER=${DB_USER}
            - DB_PASSWORD=${DB_PASSWORD}
            - DB_SCHEMA=${DB_SCHEMA}
            - DB_TABLE_PREFIX=${DB_TABLE_PREFIX}

            - FROM_EMAIL=${FROM_EMAIL}
            - FROM_NAME=${FROM_NAME}
            - REPLY_TO_EMAIL=${REPLY_TO_EMAIL}
            - SMTP_HOST=${SMTP_HOST}
            - SMTP_PORT=${SMTP_PORT}

            - TO_EMAIL=${TO_EMAIL}
            - TO_SUBJECT=${TO_SUBJECT}

            - SECURITY_KEY=${SECURITY_KEY}
            - DEV_MODE=${DEV_MODE}
            - ALLOW_ADMIN_CHANGES=${ALLOW_ADMIN_CHANGES}
            - DISALLOW_ROBOTS=${DISALLOW_ROBOTS}
            - TIMEZONE=${TIMEZONE}
            - BACKEND=${BACKEND_URL}
            - FRONTEND=${FRONTEND_URL}
            - WEBROOT=${WEBROOT}
    viiv-podcast-mysql:
        image: mysql
        restart: always
        networks:
            - internal
        volumes:
            - database:/var/lib/mysql
        environment:
            - MYSQL_RANDOM_ROOT_PASSWORD=${DB_PASSWORD}
            - MYSQL_DATABASE=${DB_DATABASE}
            - MYSQL_USER=${DB_USER}
            - MYSQL_PASSWORD=${DB_PASSWORD}
    viiv-podcast-smtp:
        image: ixdotai/smtp
        restart: always
        hostname: ${HOSTNAME}
        networks:
            - internal
        volumes:
            - ./_docker_additional_macros:/etc/exim4/_docker_additional_macros
networks:
    external:
        external: true
    internal:
        driver: bridge
volumes:
    database:
    backend:
timkelty commented 2 years ago

@jan-thoma perfect, thanks! I'll dig into this asap.

timkelty commented 2 years ago

@jan-thoma thank you for your patience.

Some follow-up questions so I can reproduce what you're seeing:

jan-thoma commented 2 years ago

Here's my Dockerfile to build the image


FROM composer:2 as vendor
COPY composer.json composer.json
COPY composer.lock composer.lock
RUN composer install --ignore-platform-reqs --no-interaction --prefer-dist

FROM craftcms/nginx:7.4

WORKDIR /app

COPY --chown=www-data:www-data --from=vendor /app/vendor/ /app/vendor/
COPY --chown=www-data:www-data . .

RUN chmod +x bootstrap.sh
RUN mkdir storage

ENTRYPOINT ["./bootstrap.sh"]

And my Traefik config

version: '3'
services:
    reverse-proxy:
        image: traefik:v2.4
        restart: always
        command:
            - "--accessLog=true"
            - "--api.dashboard=true"
            - "--providers.docker=true"
            - "--providers.docker.exposedbydefault=false"
            - "--entrypoints.ssh.address=:22"
            - "--entrypoints.web.address=:80"
            - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
            - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
            - "--entrypoints.websecure.address=:443"
            - "--entrypoints.tcp1883.address=:1883"
            - "--entrypoints.tcp3306.address=:3306"
            - "--entrypoints.tcp6524.address=:6524"
            - "--entrypoints.tcp6525.address=:6525"
            - "--entrypoints.tcp6526.address=:6526"
            - "--certificatesresolvers.letsencrypt-tls.acme.tlschallenge=true"
            - "--certificatesresolvers.letsencrypt-tls.acme.email=example@example.org"
            - "--certificatesresolvers.letsencrypt-tls.acme.storage=/letsencrypt/acme.json"
        networks:
            - external
        ports:
            - "22:22"
            - "80:80"
            - "443:443"
            - "6524:6524"
            - "6525:6525"
            - "6526:6526"
        volumes:
            - traefik-letsencrypt:/letsencrypt
            - /var/run/docker.sock:/var/run/docker.sock
        labels:
            - "traefik.enable=true"
            - "traefik.docker.network=${TRAEFIK_NETWORK}"

            - "traefik.http.routers.${TRAEFIK_NAME}.rule=Host(${TRAEFIK_HOST})"
            - "traefik.http.routers.${TRAEFIK_NAME}.service=api@internal"
            - "traefik.http.routers.${TRAEFIK_NAME}.entrypoints=websecure"
            - "traefik.http.routers.${TRAEFIK_NAME}.tls.certresolver=letsencrypt-tls"
            - "traefik.http.routers.${TRAEFIK_NAME}.middlewares=${TRAEFIK_NAME}-www-to-non-www"
            - "traefik.http.routers.${TRAEFIK_NAME}.middlewares=${TRAEFIK_NAME}-auth"

            - "traefik.http.middlewares.${TRAEFIK_NAME}-www-to-non-www.redirectregex.regex=^https://www.(.*)"
            - "traefik.http.middlewares.${TRAEFIK_NAME}-www-to-non-www.redirectregex.replacement=https://$${1}"
            - "traefik.http.middlewares.${TRAEFIK_NAME}-www-to-non-www.redirectregex.permanent=true"

            - "traefik.http.services.${TRAEFIK_NAME}.loadbalancer.server.port=${TRAEFIK_PORT}"
networks:
    external:
        external: true
volumes:
    traefik-letsencrypt:
timkelty commented 2 years ago

@jan-thoma can you try setting 'baseCpUrl' => 'my.domain.com/backend', (the same value you're setting the @web alias to in config/general.php?

jan-thoma commented 2 years ago
WEB=https://viiv.podcast.nitro/backend
WEBROOT=/app/web
BASE_CP_URL='https://viiv.podcast.nitro/backend'

With this settings the login attempt still hits an error 404

Bildschirmfoto 2021-11-08 um 08 42 22

timkelty commented 2 years ago

@jan-thoma thanks again for your patience! I've managed to get this all working and reproducible, here: https://github.com/timkelty/craftcms-subpath-proxy

It is a default Craft install, running with docker-compose, and a stripped down version of your Traefik rules (just the subpath stuff).

There a number of important settings required, and a bugfix in Craft. Please see the README.

Let me know when you get a chance to test!

jan-thoma commented 2 years ago

@timkelty

If i run your example assets won't get served correctly. I allready found the culprit in the labels section of the docker-compose file. There was a typo in my labels section, which caused the stripprefix middleware to not working properly. it should look like this, otherwise the stripprefix middleware is not working and gets skipped.

    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.${TRAEFIK_NAME_BACKEND}.rule=Host(`${TRAEFIK_HOST}`) && PathPrefix(`/backend`)"
      - "traefik.http.routers.${TRAEFIK_NAME_BACKEND}.entrypoints=web"
      - "traefik.http.routers.${TRAEFIK_NAME_BACKEND}.middlewares=${TRAEFIK_NAME_BACKEND}-stripprefix"
      - "traefik.http.middlewares.${TRAEFIK_NAME_BACKEND}-stripprefix.stripprefix.prefixes=/backend"

This solved the asset issue, but caused a new one. The CP is now reachable under /backend/backend/admin and tries to redirect all request to /backend/admin

If i run it without the stripprefix middleware actions are working but CP resources calls hitting a 404

timkelty commented 2 years ago

@jan-thoma when you say "assets", are you referring to cp resources? Everything in the CP loads find for me, using my example.

CleanShot 2021-11-21 at 23 20 44@2x

If you go to the login page before installing via cli, they won't load, because craft throws a 503, even for those cp resources.

There was a typo in my labels section

The missing backticks, right? My version has the backticks.

The CP is now reachable under /backend/backend/admin

Hmmm. That's not happening in my example…

Holler if you'd rather hop on a call and hash it out together. GIven that it works in my example (for me), I'm either misunderstanding your aim, or it isn't working for you for some other reason.

jan-thoma commented 2 years ago

@timkelty

Let me try to explain it a little further.

First the traefik config.

At the moment it's

- "traefik.http.routers.${TRAEFIK_NAME_FRONTEND}.middlewares=${TRAEFIK_NAME_FRONTEND}-stripprefix"

It should be

- "traefik.http.routers.${TRAEFIK_NAME_FRONTEND}.middlewares=${TRAEFIK_NAME_BACKEND}-stripprefix"

for the stripprefix middleware to work. This was a typo from my side sorry about that. Anyway we have two options here to run craft on the subpath behind traefik.

Without stripprefix middleware

A request to mysite.com/backend/admin will be seen from craft's perspective as a request to mysite.com/backend/admin

In this scenario everything works fine, but calls to files in a local Assets Volumes folder will end in a 404. Here's the Volume config

Bildschirmfoto 2021-11-22 um 08 33 13

With stripprefix middleware

A request to mysite.com/backend/admin will be seen from craft's perspective as a request to mysite.com/admin

In this case the CP is at mysite.com/backend/backend/admin instead of mysite.com/backend/admin. Links and action in the CP redirect to mysite.com/backend/*, files in Asset Volumes are loaded correctly.

From a technical standpoint booth, with or without the stripprefix middleware are valid options to go, the question is which configuration would work the best. I hope my explanations made it clearer to get a grasp of the problem i'm facing here. Let's schedule a call if more details are needed.

jan-thoma commented 2 years ago

@timkelty

I found another approach to solve the problem. To get Craft working on the same domain as the frontend there a set of paths which needs to be routed to backend container.

/admin => CP
/cpressources => CP Resources
/actions => Action trigger
/graphql => GraphQL Endpoint (can be any path)
/data => Asset Volume (can be any path)

With an traefik config like this everything seems to work fine

        labels:
            - "traefik.enable=true"
            - "traefik.docker.network=${TRAEFIK_NETWORK}"

            - "traefik.http.routers.${TRAEFIK_NAME_BACKEND}.rule=Host(${TRAEFIK_HOST}) && PathPrefix(`/admin`, `/data`, `/graphql`, `/cpresources`, `/actions`)"
            - "traefik.http.routers.${TRAEFIK_NAME_BACKEND}.entrypoints=websecure"
            - "traefik.http.routers.${TRAEFIK_NAME_BACKEND}.tls.certresolver=letsencrypt-tls"
            - "traefik.http.routers.${TRAEFIK_NAME_BACKEND}.middlewares=${TRAEFIK_NAME_BACKEND}-www-to-non-www"

            - "traefik.http.middlewares.${TRAEFIK_NAME_BACKEND}-www-to-non-www.redirectregex.regex=^https://www.(.*)"
            - "traefik.http.middlewares.${TRAEFIK_NAME_BACKEND}-www-to-non-www.redirectregex.replacement=https://$${1}"
            - "traefik.http.middlewares.${TRAEFIK_NAME_BACKEND}-www-to-non-www.redirectregex.permanent=true"

There could be some issues with custom plugin frontend routes but these should be callable with their respective action trigger. Did i missed some routes which are necessary for Craft to work properly?

timkelty commented 2 years ago

Did i missed some routes which are necessary for Craft to work properly?

@jan-thoma Nope, those are all the paths! Worth noting that all of those paths are configurable as well.

In this scenario everything works fine, but calls to files in a local Assets Volumes folder will end in a 404. Here's the Volume config

At this point in the request, Craft should have nothing to do with it. They're falling back to a Craft 404 because Nginx is doing try_files and including the prefix.

One easy option is to add a rule to the nginx config to rewrite those, like I've added to my example.

I'm sure you could do this directly in Traefik as well, I just know know how off-hand.

In this case the CP is at mysite.com/backend/backend/admin

I'm still never seeing this behavior in my example.

–––

I've now updated my example that seems to check all your requirements. Care to give it another try?