[Docker] Reverse-proxy in path for a React containerized app

1. Caddy version (caddy version):

I’m running caddy:2.4.6 (docker image)

2. How I run Caddy:

I’m running a set of services in docker-compose and I wanted to expose one particular service (a web app built with react and served in a container with caddy) and a api (python’s FastAPI) running in another container vía a reverse proxy executed as a container with caddy image.

The api and client are accessible in http://client:8888 and http://server:8000, both in the same docker network as the proxy container.

a. System environment:

  • Ubuntu 20.04.4 LTS
  • Kernel Version: 5.13.0-39-generic
  • Architecture: x86_64
  • Docker App (Docker Inc., v0.9.1-beta3)
  • Docker Buildx (Docker Inc., v0.8.1-docker)
  • Docker Scan (Docker Inc., v0.17.0)

b. Command:

docker-compose up 

c. Service/unit/compose file:

version: "3.9"
services:

  proxy:
    image: caddy:2.4.6
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "caddy_data:/data"
      - "caddy_config:/config"
      - type: bind
        source: ./config/Caddyfile
        target: /etc/caddy/Caddyfile
        read_only: true
    depends_on:
      - server
      - client

  client:
    image: my-app-react
    volumes:
      - "client_data:/data"
      - "client_config:/config"
    # ports:
    #   - "127.0.0.1:8888:8888"
    depends_on:
      - server

  server:
    image: my-backend-server
    entrypoint: "python -m server.app"
    # ports:
    #   - "127.0.0.1:8000:8000"

d. My complete Caddyfile or JSON config:

localhost {
	# reverse_proxy http://client:8888
	handle_path /client* {
		basicauth * {
			test_c JDJhJDE0JHNVR1ZWci8zRGpLNGZBaVh0NVppZS5BTWQuTS9RTzdGY0ZuNGN3Q2VnT2x3LnhCZ1ZaUUJX
		}
		reverse_proxy http://client:8888 {
			header_up X-Forwarded-Host {host}
			header_up Host {upstream_hostport}
			header_up X-Real-IP {remote_host}
		}
	}

	handle_path /backend* {
		basicauth * {
			test_s JDJhJDE0JHNVR1ZWci8zRGpLNGZBaVh0NVppZS5BTWQuTS9RTzdGY0ZuNGN3Q2VnT2x3LnhCZ1ZaUUJX
		}
		reverse_proxy http://server:8000 {
			header_up X-Forwarded-Host {host}
			header_up Host {upstream_hostport}
			header_up X-Real-IP {remote_host}
		}
	}
}

Usernames/Passwords are dummy, do not worry

3. The problem I’m having:

I was trying to give two different endpoints vía paths, one for each service (/client and /backend) in order to have different auth credentials for each one of them.

The error/problem I’m facing is that backend works nice and well but client only returns empty responses as only fetches “/” (as seen in docker-compose logs) insted of retrieving static files too (like /static/css/main.b612e82a.css). The client is working well (or so I suppose).

In the logs

4. Error messages and/or full log output:

HTML blank response, no headers, no hard errors.

Excerpt of docker-compose log:

client_1   | 1.6487547796050353e+09     info    http.log.access.log0    handled request {"request": {"remote_addr": "192.168.96.3:43232", "proto": "HTTP/1.1", "method": "GET", "host": "client:8888", "uri": "/", "headers": {"X-Forwarded-Proto": ["https"], "Accept-Encoding": ["gzip, deflate, br"], "Upgrade-Insecure-Requests": ["1"], "X-Forwarded-For": ["192.168.96.1"], "User-Agent": ["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36"], "Sec-Ch-Ua-Mobile": ["?0"], "Sec-Ch-Ua-Platform": ["\"Linux\""], "Sec-Fetch-Mode": ["navigate"], "Sec-Fetch-Site": ["none"], "X-Forwarded-Host": ["localhost"], "X-Real-Ip": ["192.168.96.1"], "Dnt": ["1"], "Sec-Ch-Ua": ["\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"99\", \"Google Chrome\";v=\"99\""], "Sec-Fetch-User": ["?1"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"], "Accept-Language": ["en-GB,en;q=0.9"], "Sec-Fetch-Dest": ["document"]}}, "common_log": "192.168.96.3 - - [31/Mar/2022:19:26:19 +0000] \"GET / HTTP/1.1\" 200 846", "user_id": "", "duration": 0.000102318, "size": 846, "status": 200, "resp_headers": {"Etag": ["\"r9mij8ni\""], "Content-Type": ["text/html; charset=utf-8"], "Last-Modified": ["Thu, 31 Mar 2022 19:09:08 GMT"], "Accept-Ranges": ["bytes"], "Content-Length": ["846"], "Server": ["Caddy"]}}

With alternative presented in #5:

client_1   | 1.648754875040305e+09      info    http.log.access.log0    handled request {"request": {"remote_addr": "192.168.128.3:57980", "proto": "HTTP/1.1", "method": "GET", "host": "client:8888", "uri": "/", "headers": {"Sec-Fetch-Site": ["none"], "Accept-Encoding": ["gzip, deflate, br"], "Cache-Control": ["max-age=0"], "Sec-Ch-Ua-Platform": ["\"Linux\""], "Sec-Fetch-Mode": ["navigate"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"], "Sec-Fetch-Dest": ["document"], "Upgrade-Insecure-Requests": ["1"], "X-Forwarded-Proto": ["https"], "X-Real-Ip": ["192.168.128.1"], "Accept-Language": ["en-GB,en;q=0.9"], "Sec-Ch-Ua": ["\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"99\", \"Google Chrome\";v=\"99\""], "Sec-Fetch-User": ["?1"], "X-Forwarded-Host": ["localhost"], "User-Agent": ["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36"], "Authorization": ["Basic dGVzdF9jOnRlc3Q="], "Dnt": ["1"], "Sec-Ch-Ua-Mobile": ["?0"], "X-Forwarded-For": ["192.168.128.1"]}}, "common_log": "192.168.128.3 - - [31/Mar/2022:19:27:55 +0000] \"GET / HTTP/1.1\" 200 846", "user_id": "", "duration": 0.002590092, "size": 846, "status": 200, "resp_headers": {"Last-Modified": ["Thu, 31 Mar 2022 19:09:08 GMT"], "Accept-Ranges": ["bytes"], "Content-Length": ["846"], "Server": ["Caddy"], "Etag": ["\"r9mij8ni\""], "Content-Type": ["text/html; charset=utf-8"]}}
client_1   | 1.648754875053717e+09      info    http.log.access.log0    handled request {"request": {"remote_addr": "192.168.128.3:57980", "proto": "HTTP/1.1", "method": "GET", "host": "client:8888", "uri": "/static/js/main.ba4e6b81.js", "headers": {"Accept": ["*/*"], "Dnt": ["1"], "Sec-Ch-Ua-Platform": ["\"Linux\""], "Authorization": ["Basic dGVzdF9jOnRlc3Q="], "Referer": ["https://localhost/"], "Sec-Fetch-Site": ["same-origin"], "User-Agent": ["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36"], "Accept-Language": ["en-GB,en;q=0.9"], "Sec-Ch-Ua-Mobile": ["?0"], "X-Forwarded-For": ["192.168.128.1"], "X-Forwarded-Host": ["localhost"], "X-Real-Ip": ["192.168.128.1"], "Accept-Encoding": ["gzip, deflate, br"], "Sec-Ch-Ua": ["\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"99\", \"Google Chrome\";v=\"99\""], "Sec-Fetch-Dest": ["script"], "Sec-Fetch-Mode": ["no-cors"], "X-Forwarded-Proto": ["https"]}}, "common_log": "192.168.128.3 - - [31/Mar/2022:19:27:55 +0000] \"GET /static/js/main.ba4e6b81.js HTTP/1.1\" 200 606760", "user_id": "", "duration": 0.000559949, "size": 606760, "status": 200, "resp_headers": {"Server": ["Caddy"], "Etag": ["\"r9mij8d06g\""], "Content-Type": ["application/javascript"], "Last-Modified": ["Thu, 31 Mar 2022 19:09:08 GMT"], "Accept-Ranges": ["bytes"], "Content-Length": ["606760"]}}
client_1   | 1.6487548750539007e+09     info    http.log.access.log0    handled request {"request": {"remote_addr": "192.168.128.3:57982", "proto": "HTTP/1.1", "method": "GET", "host": "client:8888", "uri": "/static/css/main.b612e82a.css", "headers": {"Accept": ["text/css,*/*;q=0.1"], "Sec-Ch-Ua-Platform": ["\"Linux\""], "Sec-Fetch-Mode": ["no-cors"], "X-Forwarded-For": ["192.168.128.1"], "User-Agent": ["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36"], "Sec-Ch-Ua-Mobile": ["?0"], "Sec-Fetch-Dest": ["style"], "Sec-Fetch-Site": ["same-origin"], "Referer": ["https://localhost/"], "X-Forwarded-Host": ["localhost"], "Dnt": ["1"], "Sec-Ch-Ua": ["\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"99\", \"Google Chrome\";v=\"99\""], "X-Forwarded-Proto": ["https"], "X-Real-Ip": ["192.168.128.1"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-GB,en;q=0.9"], "Authorization": ["Basic dGVzdF9jOnRlc3Q="]}}, "common_log": "192.168.128.3 - - [31/Mar/2022:19:27:55 +0000] \"GET /static/css/main.b612e82a.css HTTP/1.1\" 200 1087", "user_id": "", "duration": 0.000127709, "size": 1087, "status": 200, "resp_headers": {"Content-Length": ["1087"], "Server": ["Caddy"], "Etag": ["\"r9mij8u7\""], "Content-Type": ["text/css; charset=utf-8"], "Last-Modified": ["Thu, 31 Mar 2022 19:09:08 GMT"], "Accept-Ranges": ["bytes"]}}
client_1   | 1.6487548756851745e+09     info    http.log.access.log0    handled request {"request": {"remote_addr": "192.168.128.3:57980", "proto": "HTTP/1.1", "method": "GET", "host": "client:8888", "uri": "/favicon.ico", "headers": {"Authorization": ["Basic dGVzdF9jOnRlc3Q="], "Sec-Ch-Ua": ["\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"99\", \"Google Chrome\";v=\"99\""], "Sec-Fetch-Mode": ["no-cors"], "Accept-Encoding": ["gzip, deflate, br"], "Sec-Ch-Ua-Mobile": ["?0"], "Sec-Ch-Ua-Platform": ["\"Linux\""], "Sec-Fetch-Dest": ["image"], "Referer": ["https://localhost/"], "Sec-Fetch-Site": ["same-origin"], "X-Forwarded-For": ["192.168.128.1"], "X-Forwarded-Host": ["localhost"], "User-Agent": ["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36"], "Accept": ["image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"], "Accept-Language": ["en-GB,en;q=0.9"], "Dnt": ["1"], "X-Real-Ip": ["192.168.128.1"], "X-Forwarded-Proto": ["https"]}}, "common_log": "192.168.128.3 - - [31/Mar/2022:19:27:55 +0000] \"GET /favicon.ico HTTP/1.1\" 200 3870", "user_id": "", "duration": 0.000330509, "size": 3870, "status": 200, "resp_headers": {"Server": ["Caddy"], "Etag": ["\"r9miit2zi\""], "Content-Type": ["image/vnd.microsoft.icon"], "Last-Modified": ["Thu, 31 Mar 2022 19:08:53 GMT"], "Accept-Ranges": ["bytes"], "Content-Length": ["3870"]}}

5. What I already tried:

If I use the reverse proxy that is commented out in the sencond line of the Caddyfile (commenting out the rest of the caddyfile) I got the app in the browser and everithing works fine.

6. Links to relevant resources:

None

FYI, you can simplify this:

     - "./config/Caddyfile:/etc/caddy/Caddyfile:ro"

This will set the Host header to client. Are you sure that’s what you want to do? That doesn’t make too much sense. You can probably remove both these header_up lines.

You can remove this too, Caddy passes through X-Forwarded-For already.

Your backend needs to be aware of the subpaths you’re trying to route with. See this article which explains in more detail:

You can probably only put the backend/API in a subpath, and let the frontend stuff sit at the root (i.e. all other requests). It would look like this:

localhost {
	handle_path /backend* {
		reverse_proxy server:8000
	}

	handle {
		reverse_proxy http://client:8888
	}
}

Using a handle with no matcher will make it act as a fallback, when no other handle block matched.

3 Likes

This topic was automatically closed after 30 days. New replies are no longer allowed.