Multiple branch static websites

hey folks, I have a Jenkins instance running with multi branch builds. The Jenkins builds generate static website folders for each project in different folders. These projects are React apps which need to have some sort of try_files {path} /index.html to work correctly

For example, here are the paths where Jenkins store all the static files + index.html after each build on each branch:

app1 - /opt/jenkins/static/app1/
app2 - /opt/jenkins/static/app2/

I’m trying to put a Caddy v2.1b1 in front of Jenkins to serve these folders as follows:

https://static-staging.demo.site/app1/
https://static-staging.demo.site/app2/

Here’s my Caddyfile:

{
  https_port 8443
  auto_https off
  admin off
}

(global-log) {
  log {
    output stdout
    format single_field common_log
  }
}

(global-encode) {
  encode zstd gzip
}

(global-headers) {
  header {
    Cache-Control no-cache
    X-Content-Type-Options nosniff
  }
}

(global-tls) {
  tls /secrets/cert.crt /secrets/cert.key
}

https://static-staging.demo.site {
  import global-log
  import global-encode
  import global-tls
  import global-headers

  root /app1/* /opt/jenkins/static/app1
  root /app2/* /opt/jenkins/static/app2
  root * /opt/jenkins/static/global
  file_server browse
  try_files {path} /index.html
}

Not sure what I’m doing wrong but the React routing is not working in any of the cases above and for all the /app1/* and /app2/* Caddy is unable to even do the browse showing the folders there.

If anyone can guide me on the best way to achieve this

I think the issue is that root * will also match and will override any other roots. Instead, you should either use handle to wrap each of your root directives to make them mutually exclusive, or you can use one route to order them in a specific way (root * should be first so it acts as the default to be overwritten by the rest)

1 Like

Ok, so something like this?

{
  https_port 8443
  auto_https off
  admin off
}

(global-log) {
  log {
    output stdout
    format single_field common_log
  }
}

(global-encode) {
  encode zstd gzip
}

(global-headers) {
  header {
    Cache-Control no-cache
    X-Content-Type-Options nosniff
  }
}

(global-tls) {
  tls /secrets/cert.crt /secrets/cert.key
}

https://static-staging.demo.site {
  import global-log
  import global-encode
  import global-tls
  import global-headers

  route {
    root * /opt/jenkins/static/global
    root /app1/* /opt/jenkins/static/app1
    root /app2/* /opt/jenkins/static/app2
  }

  file_server browse
  try_files {path} /index.html
}

Will file_server and try_files use the right root path or I need those in different handles
?

root essentially just sets a global context variable on a per-request basis that subsequent handlers read from. route is ordered before file_server, so that should work fine, but try_files and rewrite are ordered before route, so I think you should put try_files at the end of the route block.

1 Like

Ah, but root is one of three mutually-exclusive directives: see Composing in the Caddyfile.

So only one root will ever have effect.

2 Likes

I believe I found the issue and I believe I’ll have to use a path_regexp matcher to have this working and add the match group as part of my try_files {path} {http.regexp.static.1}/index.html.

The matching group will be the branch name

Ahhh yeah that does make sense. Because your URLs have /app1/ in them, if you set the root to /opt/jenkins/static/app1, it’ll actually be looking inside of /opt/jenkins/static/app1/{path} which would be like /opt/jenkins/static/app1/app1/foo.html.

I think you should strip the prefix from the URI.

Maybe do this:

root * /opt/jenkins/static/global

root /app1/* /opt/jenkins/static/app1
uri /app1/* strip_prefix /app1

root /app2/* /opt/jenkins/static/app2
uri /app2/* strip_prefix /app2

try_files {path} /index.html
1 Like

If you’re using v2.1 beta 1, you can use snippets with arguments to make this less annoying:

(static-site) {
	root {args.0}/* /opt/jenkins/static/{args.0}
	uri {args.0}/* strip_prefix {args.0}
}

... later inside your site

import static-site /app1
import static-site /app2

you are right… I didn’t realize /app1 was being added to the root path. Good catch!

Would handle_path be a better solution in this case?

Yeah, handle_path would work great here as well. handle_path is really just a shortcut for a handle with a uri strip_prefix immediately inside it.

1 Like

actually, handle_path is not working… it is doing a rewrite to / after it matches the routing, instead of keeping the /app1/ in the uri… expected behaviour!

What does your config look like for handle_path?

https://static-staging.demo.site {
  import global-log
  import global-encode
  import global-tls
  import global-headers

  handle_path /app1/* {
    root * /opt/jenkins/static/app1
  }

  handle {
    root * /opt/jenkins/static/global
  }

  try_files {path} /index.html
  file_server browse
}

ha! looks like this is a very specific case!

This seems to work!

https://static-staging.demo.site {
  import global-log
  import global-encode
  import global-tls
  import global-headers

  route /app1/* {
    uri strip_prefix /app1
    root * /opt/jenkins/static/app1
  }

  handle {
    root * /opt/jenkins/static/global
  }

  try_files {path} /index.html
  file_server browse
}

with route I’m forcing the strip_prefix before setting root. If I change the order, setting root before strip_prefix, it rewrites to /. Does that make any sense?

To be clear, the root directive doesn’t add anything to the path. It just matches an incoming request and sets a variable in the handler chain. The path is unmodified. If you want to rewrite the request you have to do that explicitly. Caddy shouldn’t just change requests willy-nilly without the user explicitly saying it should be different than it is.

yup… bad expression… I meant to say that I didn’t realize /app1 was part of the path for the root directive and wrongly assumed root was stripping the prefix by itself.

the route directive is the way to go then :smiley:

Thanks for the help!

1 Like

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