Migration from v1 to v2: Difficult to achieve the old result

1. Caddy version (caddy version):

v2.2.1

2. How I run Caddy:

Docker, from the official image

a. System environment:

Docker

b. Command:

docker compose up -d

c. Service/unit/compose file:

reverse-proxy:
    image: <REDACTED>:latest
    restart: always
    container_name: reverse-proxy
    volumes:
      - $HOME/data/static:/static
      - $HOME/data/caddy/config:/config
      - $HOME/data/caddy/data:/data
    environment:
        <REDACTED>
    ports:
      - 80:80
      - 443:443
    networks:
      - <REDACTED>

d. My complete Caddyfile or JSON config:

www.{$CLIENT_DOMAIN}, {$CLIENT_DOMAIN} {
    handle_errors {
        @maintenance expression {http.error.status_code} == 502
        rewrite @maintenance index.html
        root * /etc/caddy/maintenance_page
        file_server
    }

    # Rules
    @users path_regexp ^/users/?
    @trailing_slash path_regexp ^/(.*)/$

    # Remove trailing slashes
    rewrite @trailing_slash /{1}

    # Force a redirect
    # redir 301 {
    #     if {path} not /
    #     if {path} ends_with /
    #     / {rewrite_uri}
    # }

    # Redirect the old users to the new admin
    redir @users https://{$ADMIN_DOMAIN}

    # Special mappings
    redir 301 {
        <REDACTED>
    }

    reverse_proxy /sitemap.xml {$API_CONTAINER_URL}

    reverse_proxy {$CLIENT_CONTAINER_URL}

    encode gzip
}

3. The problem I’m having:

After being told that Caddy v1 was EOL, I have set aside time to migrate to the new version.

I am sorry to say, but since the migration from v1 to v2 the docs have become unreadable and really difficult to understand. What before was easy to achieve, is now unclear and has become a sort of try-fix-rince-repeat cycle.

I have been working on this for the past two days, and I can’t seem to figure this out. I am trying to achive something very simple:

  1. When the service is not available, show a maintenance page. This is working, although it needs some tuning
  2. When the URL starts with /users/, redirect to another URL (not sure 100% working)
  3. Remove trailing slashes from any URL. Not working, all the answers I have found are for v1
  4. Global error catcher. No idea is this is possible, but I have multiple domains and I need to show a generic maintenance page when the service in one of those is not available
  5. An old question of mine: https://caddy.community/t/v1-rewrite-with-url-parameters/10612?u=ruben1691

I apologize if I sound quite rude, but as already said the documentation is just not up to what it was in v1. And I am starting to get frustrated.

Thanks to anyone willing to lend a hand

4. Error messages and/or full log output:

5. What I already tried:

All I have tried is listed in the Caddyfile above

6. Links to relevant resources:

That looks fine to me. I’d move your matcher to be just before the thing that uses it though, makes it easier to follow what’s going on in your config.

Do you need to preserve the original request URI when you redirect? If so, just append {uri} to the end of your redirect.

To do your trailing slash rewrite, you need to use a named regexp:

# Remove trailing slashes
@trailing_slash path_regexp trailing_slash ^/(.*)/$
rewrite @trailing_slash /{re.trailing_slash.1}

This is explained in the docs here:

And the {re.*.*} shortcut is mentioned here:

You can make use of snippets to copy commonly used bits of config into each site block:

There’s much more documentation for Caddy v2, because it’s way more powerful.

It was rewritten from the ground up to solve many fundamental flaws in the way Caddy was configured in v1. For example, the primary configuration language is now JSON, which you can use directly if you need to control the configuration at a more intimate level; the Caddyfile is now a config adapter which maps to JSON, with a bunch of magic to make it easy to use.

In v1, request matching was a concept that was re-implemented differently for every directive. This caused a mess of behaviour, and it ultimately resulted in people using the rewrite directive to set temporary state for other directives to pick up later, since it had the most powerful request matching functionality. We called this “rewrite hacks”. You no longer need to do those things in v2.

We’re doing our best with writing the docs. If you could be specific about what you find lacking in the docs, we can address it, but there’s not much we can do to improve it with vague comments like that.

1 Like

In addition to the docs @francislavoie linked to, it’s also explained here in a dedicated section about trailing slashes:

I think it’s because it’s just different from what you’re used to. Sorry; but with vague, intangible feedback like that there’s no useful improvements we can make to the docs (which are open source btw; anyone can discuss and contribute improvements to them).

So at this point – and after Francis’ help above – what exactly is not working for you?

First of all, thanks for getting back to me.

I have gone through the different links you have provided, and after a lot of trial and error I have gotten to a point where I have been able to partially replicate what was done with v1.

  1. Maintenance page. As said in the original version, It worked but I made a couple of fixes in the HTML to make it easier to change.
  2. URL Redirection. This works using the following regex pattern:
    @users path_regexp ^/users(?:/?)
    redir @users https://{$ADMIN_DOMAIN}
    
  3. Trailing slash. This has been solved thanks to the regex method. More specifically:
    # Remove trailing slashes
    @trailing_slash path_regexp trailing_slash ^/(.*)/$
    rewrite @trailing_slash /{re.trailing_slash.1}
    

@matt This is where the documentation shows the first issue. The page you pointed to shows two examples, which to me have no reason to be there. I do not see why someone would want to hardcode them, for each of the pages interested. Also, mentioning how the file_server directive will take care of it for me makes me think that I need to include this directive. A nice example (or a simple mention) of the regex method would make a lot more sense imo.

  1. Global error catcher: Thanks to @francislavoie link to the docs this works flawlessly. For completeness’ sake, here is my solution:
    # Define this block BEFORE any domain-specific block
    (maintenance_page) {
        handle_errors {
            @maintenance expression {http.error.status_code} == 502
            rewrite @maintenance index.html
            root * /etc/caddy/maintenance_page
            file_server
        }
    }
    
    domain.com {
        # Import the maintenance page
        import maintenance_page
    
        # Other directives
    }
    
  2. Redirection with parameters: Haven’t had the time to look into it, but I guess I’ll be using the regex method once again with capture groups

After looking once again at the docs and spending a bit more time looking for things, I believe the major issues are the examples and the ability to quickly understand what is possible. Let me explain: When I look at a directive, I need to immediately understand what the different options are, what the directive can help me achieve, but more crucial is how to do common tasks. The trailing slash issue shows you exactly what I mean: Why spend two paragraphs telling me the difference between the internal and the external enforcement, but there is not even a single mention of how more advanced patterns can be applied?

I believe with v1 this was made a lot easier. With v2, I get the feeling that the documentation (and the examples) have taken a hit in quality. The old examples repo was incredibly useful, and provided straight-to-the-point examples for even the most complex of applications. Yes, it’s true that the wiki exists. But I want the config, not the whole story around it. The repository made it easy to browse and especially, it was easily searchable through Google.

Another example: I am trying to serve static files (images and pdf files) through caddy. Before, this was trivial, now I find myself with a few questions:

  1. Should I always pair a file_server directive with a root directive?
  2. Why do I get a 200 when the file does not exist? The server should return a 404
  3. Using try_files, how do I serve a default (fallback) image?

In v1, I had the following configuration:

{$STATIC_ASSETS_DOMAIN}/images {
    root /static
    gzip
}

This would serve the images from the static folder. Looking at the docs , this should be equivalent in v2 to (or, better said, from what I understood):

{$STATIC_ASSETS_DOMAIN}/images {
    file_server /static/*
    
    encode gzip
}

And yet I can’t get this to work, especially the fallback image when the originally requested image is not available.

I am going to spend a couple more days on this, but if I can’t figure this out without ripping my hair out I’ll have to abandon caddy (and all the nice to have’s) for another solution. It hurts me to abandon such a solution, as it worked flawlessly in v1, but I can;t spend three days on something that should take me at most one afternoon. Especially given that my configuration is incredibly trivial.

Again, I don’t want to be rude, but you asked what was wrong. Hopefully it can open a line of conversation and we can all work together to fix these issues.

Yes. In v1, file_server was implicit. In v2, it needs to be explicitly enabled. There are plenty of reasons why having it be implicit was a bad idea. And if you don’t configure root, then Caddy will assume the present-working-directory. Best to always specify it to avoid surprises.

That’s not the case, if you enable file_server.

Caddy responds with 200 if it was not configured to do anything else. This is Caddy is doing exactly what you configured it to do. If you didn’t tell Caddy to do something with a request, then Caddy can only say “I did what I was configured to do” and return a successful response. It’s up to the user to cover all the possible requests with responses.

I’m not sure I understand the question.

In Caddy v2, path matching is exact. That means that /images will only match exactly /images, and not /images/foo. So put a * at the end for that to work.

Also, you’re then matching on /static/* afterwards. That will never be true, because the path can’t simultaneously be /images* and /static/*.

But personally, I recommend against using path matchers in site blocks; it’s confusing. I recommend using handle directives instead.

{$STATIC_ASSETS_DOMAIN} {
	handle /images* {
		root * /static
		file_server
	}

	handle /api* {
		reverse_proxy 127.0.0.1:8080
	}

	# Fallback for any other requests not otherwise handled
	handle {

	}
}

Note that handle doesn’t strip the path from the request, so for a request such as /images/foo.png, the file_server would be looking for files in /static/images/foo.png. If you have your files in /static/foo.png (i.e. no images directory) then you should use handle_path instead, which strips the path prefix from the request before continuing.

1 Like

Thanks once again for the help. I am now able to serve the files directly from within Caddy and I made a couple of small modifications to my Caddyfile thanks to your input.

What I have is now as follows:

{$STATIC_ASSETS_DOMAIN} {
    encode gzip

    handle_path /images* {
	root * /static
        file_server

        # this should be returned when an image is not found
        try_files {path} /static/default.jpg
    }

    handle_path /files* {
        root * /static
	    file_server

        header {
            Access-Control-Allow-Origin *
            -Server
        }
    }

    handle {
        # Respond with an error
        respond "Asset not found" 404 
    }
}

Now, regarding the try_files directive. Let’s say the user requests the following URL: domain.com/images/myImage.png. In case the image exists, it’s served correctly, but in the case the image does not exist, then a default image should be served. In nginx the try_files directive is used for that purpose. Is this the same in Caddy v2?

Yes, but the path you specify needs to be the request path, not the file path. So you would do try_files {path} default.jpg. The try_files directive does a rewrite of the request path.

1 Like

Yep that did it. Now there is a default image served when the requested image cannot be found.

Once again, thanks for the help.

Unless you want to continue with the discussion about the docs, feel free to close the thread

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