Static brotli/gzip and reverse proxy config question

1. Caddy version (caddy version): V2

It’s more a configuration question… I’ve been trying out a NextJS app with pm2. The process is very easy so far:

nextjs.example.com {
    encode zstd gzip
    # Assets serving (js, css, ...) 
    handle /_next/static/* {
        # here we need to strip the _next prefix, it does not exists
        uri strip_prefix /_next
        file_server {
            root /home/xyz/nextjs-built-project/.next
        }
    }
    handle {
        reverse_proxy 127.0.0.1:3001
    }
} 

It’s very clean, but I want to take advantage of pre-static compression (either gzip, brotli or both).

So I’ve built all compatible assets in a pre-compressed format (suffixed by → js.br, js.gz, css,.br…).

And found out this thread: Why caddy 2 is not able to serve static brotli files? - #29 by matt

But I can’t really figure out how to make it work with the requirements I have (proxy, handle with strip prefix)

I’d like to have (see A, B, C)

nextjs.example.com {
    # A. REMOVE GLOBAL ENCODE
    # encode zstd gzip

    # Assets serving (js, css, ...) 
    handle /_next/static/* {
        uri strip_prefix /_next
        # B 
        #    IF A PRECOMPRESSED ASSET EXISTS, 
        #    SERVE IT ! 
        #    AND ALSO SPECIFY CACHE HEADERS 
        file_server {
            root /home/xyz/nextjs-built-project/.next
        }
    }
    handle {
        # HANDLE GZIP... here.
        encode zstd gzip
        reverse_proxy 127.0.0.1:3001
    }
} 

Caddy look great … It’s my first attempt at it. I’m really curious about to handle this kind of situations.

Help appreciated

PS/ Was used to do this trick in Apache: mfts/.htaccess.dist at master · contredanse/mfts · GitHub.

I’d like to add a personal note…

Nowadays in webpack based frontend projects, it’s very easy to pre-compress assets.

But AFAIK no webserver makes it easy to configure it (apache is tricky but possible, nginx in its opensource version pfff… need to a compile ourselves to get the static_gzip, they don’t even have a static_brotli one. The paying version is really better)

Using pre-compressed versions will make a real difference in speed especially on low-end servers (cheap droplets…).

I know cloudflare, cdn’s… gives a wider and more global solution. (i use them a lot)

But sometimes I feel that could give one more very nice reason to move to caddy. Especially for small / personal projects…

All the best

This thread shows an example of how to do it. It’s a bit complicated because you need to do rewrites and set headers based on whether a pre-compressed file exists on disk.

We’re tracking support for content negotiation in the following github issue. There’s a lot of design work and consideration that needs to be done to implement it correctly. It’s definitely something we want to get in eventually.

Finally just as a quick note, you can replace handle + uri strip_prefix with just handle_path, it has path prefix stripping logic built in.

Also, I recommend using the root directive rather than the subdirective to file_server, that way the root will be set for all directives in your handle (will be necessary since the file matcher also needs to know the root to work)

handle_path /_next/static/* {
	root * /home/xyz/nextjs-built-project/.next/static

	# Precompress rewrites goes here

	file_server
}
1 Like

Thanks @francislavoie for your advices, it works perfect !

I share the config here for others:

{
    experimental_http3
}

# See https://github.com/caddyserver/caddy/issues/2665

(precompressed) {
  @{args.0}{args.1} {
    header Accept-Encoding *{args.2}*
    path *.{args.0}
    file {path}.{args.1}
  }
  handle @{args.0}{args.1} {
    rewrite {path}.{args.1}
    header Content-Encoding {args.2}
    header Content-Type {args.3}
    header Cache-Control max-age={args.4},public
    header Vary "Accept-Encoding"
  }
}
(uncompressed) {
  @{args.0}{args.1} {
    path *.{args.0}
    file {path}.{args.1}
  }
  handle @{args.0}{args.1} {
    header Cache-Control max-age={args.4},public
    header Vary "Accept-Encoding"
  }
}

my-nextjs-site.example.com {

    log {
        output file my-nextjs-site.example.com.log {
            roll_size 5MiB
            roll_keep 8
            roll_keep_for 72h
        }
        format console
    }


    handle_path /_next/static/* {

        # Rewrite to next static

        root * /path/to/my-nextjs-site.example.com/.next/static

        # Precompress static assets

        import precompressed html br br text/html 300
        import precompressed html gz gzip text/html 300
        import uncompressed html - - - 300
        import precompressed js br br application/javascript 2628000
        import precompressed js gz gzip application/javascript 2628000
        import uncompressed js - - - 2628000
        import precompressed map br br application/json 2628000
        import precompressed map gz gzip application/json 2628000
        import uncompressed map - - - 2628000
        import precompressed css br br "text/css; charset=utf-8" 2628000
        import precompressed css gz gzip "text/css; charset=utf-8" 2628000
        import uncompressed css - - - 2628000

        # Add cache control 

        @static {
            file
            path *.ico *.gif *.jpg *.jpeg *.png *.svg *.woff *.map
        }

        header @static Cache-Control max-age=2628000

        file_server
    }

    # Reverse proxy the nextjs app for server side rendering

    handle {
        encode zstd gzip
        reverse_proxy 127.0.0.1:3001
    }
}
1 Like

Great! Thanks for sharing your full config :smiley:

I saw that you added another Cache-Control block. The path extensions you have in there have some overlap with the snippet ones. I’d suggest either updating the overlapping ones with the age you want and removing the overlapping extensions from the path matcher.

Also you have this:

    tls admin@example.com {
        protocols tls1.2 tls1.3
    }

Setting protocols here is not useful, because those are already Caddy’s defaults. I recommend you remove that (and the braces) so that when you later upgrade Caddy, if some tls1.4 spec is released and support is added to Caddy for example, it would just work.

Updated the config in previous thread. Thanks a lot !

1 Like

To clarify, you can leave in tls <email> (it’s good to have an email set if you can, but it’s not necessary). I was just saying the protocol bit isn’t needed.

Hi,
I noticed that when I enable use encode gzip, the response seems to be encoded (as expected), but the content-type is given as text/html. Nevertheless, Chrome seems to figure out that it’s a binary file and not a text file that it’s getting and successfully showing the webpage.
Is that fine, or do we need to manually add a ‘content encoding: gzip’ header?

Caddy definitely should be setting the Content-Encoding header. Are you sure it’s not set?

I think Caddy doesn’t add content encoding header when it’s a ‘HEAD’ request.

This gives content encoding: gzip

curl -v -H "accept-encoding:br,deflate,gzip" -o /dev/null -s https://blah.blah

But this doesn’t:

curl -I -H "accept-encoding:br,deflate,gzip" https://blah.blah

That seems fine then, since HEAD never has any content. I think it’s because of this condition rw.buf.Len() >= rw.config.MinLength, i.e. the response has too few bytes to be worth encoding.

A response to a HEAD method should not have a body. If it has one anyway, that body must be ignored: any entity headers that might describe the erroneous body are instead assumed to describe the response which a similar GET request would have received.

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

For whoever finds this in the future, this is now possible without the funky snippets since v2.4.0, using the file_server directive’s precompressed option.