Caddy2配置

Caddyfile:反向代理以及rewrite

Caddy 是一个使用Go语言开发的开源web服务器,自动支持 HTTPS 证书的生成,不需要像 nginx 一样手工指定数字证书的配置,大大简化了现代web服务器的配置。通过本文可以看到,caddy 2.x的配置文件Caddyfile非常简洁。运行时也仅仅需要一条简单的命令:

1
caddy run --config Caddyfile

或者干脆简化成:

1
2
# 默认配置文件为 'Caddyfile'
caddy run

在使用caddy的过程中,陆陆续续遇到过一些问题,通过搜索缝缝补补的积攒了一些小小的经验,把一些常见的问题汇总一下:

Caddyfile

配置基本格式

Caddyfile 样例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
	servers {
		metrics
	}
	admin :2019
}

raeiou.com, www.raeiou.com {	
    handle /* {
		reverse_proxy 127.0.0.1:9090
	}
        root * www/
	file_server
}

blog.raeiou.com {
    handle_path /pic/* {
        root * /www/images/
        encode gzip
        file_server
    }
    
	root * blog/
	encode gzip
	file_server
}

http://10.6.0.1:8066 {
        root * deploy/static/
        encode gzip
        file_server
}

配置解释:

以上配置定义了3个站点:

  • raeiou.comwww.raeiou.com

    站点主页,默认支持 HTTP (80端口) 和 HTTPS (443端口)
    虽然用 file_server 指定了静态 web server,但是由于handle /*的存在,会将所有请求都反向代理到9090端口。

  • blog.raeiou.com

    标准的静态页面站点,支持 HTTP (80端口) 和 HTTPS (443端口) 通过 file_server 指定静态页面路径为 blog/,同时将路径/api/*路径的请求都从本地路径/www/images下的文件去响应。

  • http://10.6.0.1:8066

    绑定IP地址,可以控制仅通过VPN访问,例如一些管理后台不通过公网开放访问

反向代理

相较于nginx的反向代理,caddy的配置显然简单了许多,就像前面的配置一样

1
2
3
handle /wss/* {
    reverse_proxy 127.0.0.1:18180
}

等价于以下nginx配置,但是要注意 reverse_proxy 只能指定代理转发的 server 和 port,而不能指定路径,如果你写成 reverse_proxy 127.0.0.1:18180/wss/,那么你可能会看到Caddy日志输出以下错误:

2024/01/19 16:19:55.267 ERROR http.log.error dial tcp: lookup gw on 127.0.0.53:53: server misbehaving {“request”: {“remote_ip”: …, “uri”: “/gw?api= …, “status”: 502, “err_id”: “76fp0g9hn”, “err_trace”: “reverseproxy.statusError (reverseproxy.go:1248)”}

1
2
3
4
5
6
7
8
9
# 等价的nginx配置
location /wss/  {
    proxy_pass http://localhost:18180/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header X-real-ip $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr;
}

反向代理 + uri

前面说到 reverse_proxy 不支持指定路径,因此就常常需要把反向代理和uri结合到一起使用:

1
2
3
4
handle /wss/* {
    uri path_regexp ^/wss/(.*) /websocket/$1
    reverse_proxy 127.0.0.1:18180
}

如此,/wss/queryStatus 就会转发到 http://127.0.0.1:18180/websocket/queryStatus。

uri 的用法参考 官方文档

rewrite 与 redir

rewrite(文档) 的作用是改写请求路径,例如将 url 中的 /api/queryStatus 改写成 /queryStatus,那么就可以用到以下配置。

1
2
3
handle /api/* {
    rewrite /api/(.*) /$1
}

redir(文件) 的区别在于,redir主要是应答response code为302或301,引导客户端重定向。而 rewrite 的重写在服务端内部进行处理,但是rewrite也还有一些别的需要注意的地方,参考文档。

handle 与 handle_path

关于 handlehandle_path 用法的差异,可以参考官方文档。handle_path 会隐式的使用 uri strip_prefix 去掉匹配的前缀。

因此当访问 /wss/query 时,handle /wss* + reverse_proxy,反向代理收到的请求路径是 /wss/query,而 handle_path /wss* + reverse_proxy时,反向代理收到的路径是 /query。

配置文件的拆分

参考 社区答案,简言之,就是使用import关键字,引入外部配置文件,例如上面的基础配置就可以写成:

1
2
3
import conf/www.caddy
import conf/blog.caddy
import conf/vpn_admin.caddy

或者使用通配符:

1
import conf/*.caddy

问题排查工具

配置文件最常见的问题就是反向代理目标不正确,或是路径地址rewrite错误。

为了排查caddy的转发配置(反向代理或rewrite)是否正确,我们需要一点小小的技巧。最简单的方法之一,是使用一个测试用的目标服务器,来观察转发的参数。从服务器返回的应答可以直观的看到是否已经正确配置。以下使用Go代码作为示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// test_server.go
package main

import (
  "fmt"
  "log"
  "net/http"
  "strings"
)

func main() {
  http.HandleFunc("/", handleRequests)
  log.Println("Starting server on :11180")
  err := http.ListenAndServe(":11180", nil)
  if err != nil {
    log.Fatal("ListenAndServe: ", err)
  }
}

func handleRequests(w http.ResponseWriter, r *http.Request) {
  w.WriteHeader(http.StatusOK)
  
  var headersBuilder strings.Builder
  for name, values := range r.Header {
    for _, value := range values {
      headersBuilder.WriteString(fmt.Sprintf("%v: %v\n", name, value))
    }
  }
  headersString := headersBuilder.String()

  // response output: URL & HEADER
  fmt.Fprintf(w, "%s\n\nHEADERS:\n%s", r.URL.String(), headersString)
}

使用 go build test_server.go 编译生成测试服务器启动,在 Caddyfile 中测试配置:

1
2
3
4
    handle /gw* {
        reverse_proxy 127.0.0.1:11180
        rewrite /gw(.*) /gw$1
    }

通过浏览器请求 http://<ip or domain>/gw?a=1&b=2 观察应答

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus