Is this a good way to set up a react SPA with a reverse proxy?

I just spent a few hours reading the documentation and writing a Caddyfile for an application made using django and react (SPA using react-router). It took a while to figure out how to make the react-router work. Because of the order in which the directives are evaluated.

It works right now. But I do have one specific question. {path} or {uri} - which one should I use with try_files?
I’m also curious if there’s a better way to do this. I would appreciate your suggestions :slight_smile:

:80 {
    encode gzip zstd

    @back-end {
        path /api/* /django-admin/*
    }

    @front-end {
        not path @back-end
    }

    handle @front-end {
        root * /src
        file_server
        try_files {path} /index.html
    }

    handle @back-end {
        reverse_proxy {
            to back-end:8080

            header_up Host {http.request.host}
            header_up X-Real-IP {http.request.remote.host}
            header_up X-Forwarded-For {http.request.remote.host}
            header_up X-Forwarded-Port {http.request.port}
            header_up X-Forwarded-Proto {http.request.scheme}

            health_path /api/health/
            health_interval 10s
            health_timeout 5s
            health_status 200
            max_fails 5
        }
    }
}

First of all,

You should probably remove all these lines. They are not necessary, and in some cases override Caddy’s defaults which are more correct:

This matcher doesn’t make sense, you can’t use a named matcher as a path. You can just remove the @front-end matcher altogether actually, and use a handle block with no matcher, because handle blocks are mutually exclusive from eachother. You can write it like this:

@backend path /api/* /django-admin/*
handle @backend {
	...
}

handle {
	# Fallback for any other paths, i.e. your frontend
}

{path} is probably correct here. The try_files directive uses the rewrite handler under the hood (see the expanded form) which can replace both the path and query of the URL, depending on the inputs. The query part is preserved across path rewrites if there’s no ? in the rewrite.

I’d probably write your config like this:

:80 {
	encode gzip zstd

	@back-end path /api/* /django-admin/*
	handle @back-end {
		reverse_proxy back-end:8080 {
			health_path /api/health/
			health_interval 10s
			health_timeout 5s
			health_status 200
			max_fails 5
		}
	}

	handle {
		root * /src
		try_files {path} /index.html
		file_server
	}
}

One last note, I’m not sure the health check stuff will be so useful for you unless you specify more than one backend to proxy to. It’s mainly useful for load balancing across multiple backends. If you just have the one, it won’t have much benefit.

1 Like

Wow, that final config looks much neater.

I had a feeling that this is a hack. But it didn’t give an error, so I thought that it’s fine. Not sure how it ended up working though.

I don’t really remember why I put it in there. It’s going to be reverse proxy-ing to a kubernetes service. So I guess I could remove it.

Thanks a lot! I really appreciate it.

1 Like

Basically because not path <literally anything weird> will always be true if you never hit that path. Technically @foo can be a path, but nobody’s gonna make a request like http://localhost/@foo so :man_shrugging:

:smiley:

1 Like

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