web服务器Caddy初体验,真香!

2,001 阅读11分钟

很早之前就听说过caddy这个开源的web服务器,但一直也没尝试过。最近刚好使用caddy配置了一个站点,发现真香!

一、Caddy简介

Caddy是使用Go语言编写的一款开源Web服务器和反向代理服务器,设计目标是提供易于使用且高效的性能。它不仅支持常见的HTTP/HTTPS协议,还可以作为反向代理服务器、负载均衡器、WebSocket支持等。它的灵活性和模块化的架构,使其能够根据不同需求扩展功能,特别适合用于容器化环境和微服务架构。

个人体验下来Caddy的有几个比较大的特点

第一点、默认启用HTTPS,Caddy集成了Let’s Encrypt,可以自动为你的网站申请、更新和管理SSL证书,无需任何额外操作,免去繁琐的配置和证书管理流程。

第二点、配置简洁,与传统的Web服务器相比,Caddy的配置文件极为简洁,使用简易的配置文件(Caddyfile),极大降低了新手的学习成本。

第三点,除了传统的Caddyfile和JSON配置文件外,Caddy还提供了通过REST API动态管理和变更其配置的能力。这个API使得我们能够在运行时更改Caddy的配置,而无需重新启动服务器或手动编辑配置文件。

第四点,现代化,通过默认启用HTTPSREST API动态变更配置也能看出来,除此之外caddy还支持Prometheus metrics、默认使用结构化的json作为access日志。

对比传统的web服务器Nginx对比更能看出caddy的一系列特点

特性CaddyNginx
配置方式Caddyfile, JSON, REST APINginx配置文件(nginx.conf)
自动HTTPS支持是,默认启用自动TLS证书管理否,需手动配置SSL证书
适用范围7层(应用层),反向代理和Web服务,内置负载均衡支持4层(传输层)和7层(应用层)反向代理、负载均衡等
扩展性插件化架构,支持扩展模块化架构,支持静态编译的模块
性能较高(适合轻量应用)非常高(适合高并发应用)
配置简洁性Caddyfile格式简洁,易于上手配置相对复杂,灵活但不够直观
系统资源占用较低较低,适合高并发处理
编写语言Go语言C语言
Access日志格式结构化,默认JSON格式,支持自定义非结构化,默认标准日志格式,支持自定义

二、Caddy的基本用法

Caddy的安装和配置非常简便,下面是一些常见的配置示例。

1. Caddy的安装

Caddy可以通过多种方式进行安装,除了传统的安装方法,还可以通过Docker Compose来进行快速部署。

方法一:二进制安装

安装方式除了可以使用发行版提供的仓库之外

因为caddy使用Go编写,编译完成后只有一个二进制文件,所以也可以直接在官网或者github release页面进行下载,下载完成后把caddy移动到PATH下即可

直接启动Caddy

可以直接在命令行中手动启动 Caddy。运行以下命令:

caddy start

systemctl启动

官方也提供了systemd unit files,配置之后就可以使用systemd来启动了(限于篇幅这里就不介绍了)

也可以使用以下命令启动Caddy:

# 启动Caddy服务
sudo systemctl start caddy
​
# 设置Caddy开机自启
sudo systemctl enable caddy

这样,Caddy服务会在后台启动,并且会随系统开机自动启动。

如果想查看Caddy的运行状态,可以使用:

# 查看Caddy服务的状态
sudo systemctl status caddy

如果需要停止Caddy服务,可以执行:

# 停止Caddy服务
sudo systemctl stop caddy
方法二:使用Docker Compose安装

如果你希望通过Docker容器来运行Caddy,可以使用Docker Compose来进行安装和启动。首先,在项目目录下创建一个docker-compose.yml文件,内容如下:

version: "3.8"
services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    environment:
      - ACME_AGREE=true

volumes:
  caddy_data:
  caddy_config:

在上述配置中,

  • ./Caddyfile是你的Caddy配置文件,Docker容器将其挂载到Caddy的配置目录中。
  • 此外,caddy_datacaddy_config用于持久化存储Caddy的TLS证书和配置文件。

启动Caddy服务:

docker-compose up -d

通过此方式,你可以轻松地将Caddy部署到Docker容器中,并且无需关心手动管理TLS证书,Caddy会自动处理。

2. 配置方式

Caddy的配置可以通过两种方式来管理:

  • 配置文件方式:通过将配置写入Caddyfile或者JSON文件中,Caddy会自动加载配置,官方也提供了一系列的adapter来支持其他格式的配置文件
  • API方式:通过Caddy的API接口动态修改配置,适用于更复杂的环境和自动化场景。

在实际应用中,但大多数人会选择 JSON + API 或 Caddyfile + CLI 的组合方式,不会混合使用两者,避免出现配置不一致。

通过Caddyfile文件配置Caddy

Caddyfile是Caddy最常用的配置文件格式,以简洁明了著称。大多数用户和大部分文档推荐使用这种格式来配置Caddy。

Caddyfile是一种基于块结构的配置格式,语法非常简洁且易于理解。每个配置项通常以站点名称(通常是域名)作为起始,然后是需要的配置项。Caddyfile的基本结构如下:

example.com {
    reverse_proxy 127.0.0.1:3000
    log {
        output file /var/log/caddy/access.log {
            mode 644
        }
        format json
    }
}

在这个示例中,example.com是配置的站点名称,后续的内容是针对该站点的配置项。

Caddyfile的配置项可以包括但不限于:

  • 反向代理:将请求转发到后台服务。
  • TLS/SSL配置:启用HTTPS并管理证书。
  • 路径匹配和重定向:根据请求路径来定义不同的处理方式。

Caddyfile的每一行都代表一个配置项,它非常易于编写和阅读,且支持丰富的功能。

通过JSON文件配置Caddy

虽然Caddyfile格式更加简洁,但是在一些高级使用场景中,JSON格式的配置文件更加灵活和强大。特别是在需要动态配置或者通过API接口修改配置时,JSON格式是更常见的选择。一个Caddy JSON配置文件的例子如下:

{
  "apps": {
    "http": {
      "servers": {
        "example": {
          "listen": [":80"],
          "routes": [
            {
              "match": [
                {
                  "host": ["example.com"]
                }
              ],
              "handle": [
                {
                  "handler": "static_response",
                  "body": "Hello, world!"
                }
              ]
            }
          ]
        }
      }
    }
  }
}

尽管JSON格式更为复杂,但它支持更多的高级功能,如动态配置、分布式管理等。通常,开发者在需要与其他系统进行集成时会选择这种格式。

通过API配置Caddy

除了传统的Caddyfile和JSON配置文件外,Caddy还提供了通过REST API动态管理和变更其配置的能力

Caddy的REST API允许你通过HTTP请求来控制Caddy的配置、状态和TLS证书管理等,无需重新启动服务器或手动编辑配置文件

API默认情况下监听在Caddy的管理端口(默认为 localhost:2019)。通过API,你可以对Caddy进行以下操作

1. 获取当前配置

你可以通过API请求来获取当前Caddy的配置。默认情况下,Caddy配置是以JSON格式返回的。

curl -X GET http://localhost:2019/config/

返回的JSON数据将展示当前Caddy的所有配置,类似于Caddyfile的配置内容。

2. 添加配置

如果你需要动态修改配置,可以通过PUT请求来添加Caddy的配置。

举个例子,假设当前caddy没有加载任何配置文件,通过动态加载配置,创建一个server hello,它监听2015端口,并且返回"Hello, world!"

curl localhost:2019/load \
    -H "Content-Type: application/json" \
    -d @- << EOF
    {
        "apps": {
            "http": {
                "servers": {
                    "hello": {
                        "listen": [":2015"],
                        "routes": [
                            {
                                "handle": [{
                                    "handler": "static_response",
                                    "body": "Hello, world!"
                                }]
                            }
                        ]
                    }
                }
            }
        }
    }
EOF
​
curl localhost:2015
Hello, world!

此时你想动态的修改server hello

curl -X PATCH http://localhost:2019/config/apps/http/servers/hello/routes \
    -H "Content-Type: application/json" \
    -d '[
        {
          "handle": [
            {
              "handler": "static_response",
              "body": "Hello from Caddy API!"
            }
          ]
        }
      ]'
​
curl localhost:2015
Hello from Caddy API!

这个请求会将一个修改静态响应,返回 Hello from Caddy API!

3. 删除站点配置

通过API,你也可以删除某个站点或相关配置。例如,删除一个指定的站点配置:

curl -X DELETE http://localhost:2019/config/apps/http/servers/hello

这将删除server hello的配置

三、 常见配置示例

为了简化配置过程和提升可读性,我们将在后续的示例中使用Caddyfile格式,以下是几个常见的配置示例。

直接回复

localhost:2017 {                   # 要server的站点名,不写端口则默认443(https)或者80(http)
    respond "Hello, world!"        # 直接响应内容
}

如果配置只有一行,{}在caddyfile中是可以省略的。但我还是习惯用{}包裹

localhost:2017
respond "Hello, world!"     

配置静态文件

localhost:2016 {                   # 要server的站点名,不写端口则默认443(https)或者80(http)
    root * /var/www/mysite         # 静态文件的根路径
    file_server {                  # 静态文件处理
        browse                     # 如果没有index文件,则展示目录浏览模式
        hide .git                  # 隐藏 .git
        precompressed zstd br gzip # 启用压缩
    }
}

如果只有localhost:2016并且上面的如果file_server不需要配置其他选项的时候

localhost:2016
root * /var/www/mysite 
file_server browse

配置反向代理

这个配置将所有访问example.com的请求反向代理到本地的8080端口。

example.com {
    reverse_proxy localhost:8000
}

还可以针对不同的path进行反向代理

example.com {
    reverse_proxy /bar localhost:8000  # example.com/bar的内容会被转发到localhost:8000/bar
    
    reverse_proxy /foo localhost:8001  # example.com/foo的内容会被转发到localhost:8000/foo
}

还可以针对反向代理配置更复杂的策略,如改写请求与响应等

example.com {
    reverse_proxy /bar localhost:8000      # example.com/bar的内容会被转发到localhost:8000/bar
    
    reverse_proxy /foo {                   # 针对example.com/foo配置更复杂的策略
        to localhost:8001                  # 转发到localhost:8001
        rewrite /                          # 改写path,example.com/foo会被转发成localhost:8001/
        header_up X-Forwarded-For {remote} # 增加新的header:X-Forwarded-For,内容为client ip
    }
}

配置负载均衡

example.com {
    reverse_proxy / backend1.example.com backend2.example.com
}

此配置将请求负载均衡地分发到backend1.example.combackend2.example.com

负载均衡也类似,有很多参数可以设置

一个复杂的DEMO

# 要server的站点名,不写端口则默认443(https)或者80(http)
# 使用http://代表不启用https
http://localhost:8000 {          
    respond "Hello, world!"
}
​
http://localhost:8001 {            
    respond "{path}"
}
​
localhost:8002 {                  
    # 记录所有路径的访问日志
    log {
        # 访问日志写入/path/to/access.log
        output file /path/to/access.log {
            # 设置日志文件的权限
            mode 644
        }
​
        # 日志格式为json
        format json
    }
​
    # 使用handle来匹配路径
    # 它和下面等价
    # reverse_proxy /lb/* localhost:8000 localhost:8001
    handle /lb/* {
        reverse_proxy localhost:8000 localhost:8001
    }
​
    handle /proxy/* {
        reverse_proxy {                 
            to localhost:8001                  # 转发到localhost:8001
            rewrite /                          # 改写path,/proxy/*会被转发到/*
            header_up X-Forwarded-For {remote} # 增加新的header:X-Forwarded-For,内容为client ip
        }
    } 
   
    handle /static/* {
        uri strip_prefix /static       # 移除/static前缀
​
        root * /var/www/mysite         # 静态文件的根路径
​
        file_server {                  # 静态文件处理
            browse                     # 如果没有index文件,则展示目录浏览模式
            hide .git                  # 隐藏 .git
            precompressed zstd br gzip # 启用压缩
        }
    }
}

四. Caddy的重要持久化存储

在Caddy的配置中,有几个重要的持久化存储目录,它们用于存储TLS证书、配置文件和其他关键数据。理解这些存储路径的作用可以帮助你更好地管理和迁移Caddy的部署。

1. 自定义的配置文件

这个就不多说了,就是上文一直提到配置文件,你需要放置到合理的位置

对于容器内,默认配置文件位置在/etc/caddy/Caddyfile。因此可以挂载这个文件来提供自定义的配置文件

# ...
volumes:
  - ./Caddyfile:/etc/caddy/Caddyfile

2. Data Directory(数据目录)

Caddy会自动为每个网站生成并管理SSL/TLS证书,这些证书存储在Caddy的数据目录中。

默认情况下,如果设置了 XDG_DATA_HOME 环境变量,那么位置就是 $XDG_DATA_HOME/caddy/,它是一个目录

没设置的话则取决于系统

OSData directory path
Linux, BSD$HOME/.local/share/caddy
Windows%AppData%\Caddy
macOS$HOME/Library/Application Support/Caddy
Plan 9$HOME/lib/caddy
Android$HOME/caddy (or /sdcard/caddy)
docker/data

这个目录用于存储所有与Caddy运行相关的数据,例如:

  • TLS证书和私钥:Caddy会自动申请和续订证书,并将这些证书文件保存在数据目录中。
  • 证书缓存:为了提高性能,Caddy会缓存证书验证和其他相关数据。
  • ACME(自动证书管理环境)缓存:Caddy使用ACME协议与Let's Encrypt等证书颁发机构通信,该缓存存储了所有的ACME响应数据。

所以数据目录不能被视为缓存,其内容并非临时的,也不仅仅是为了性能。因此,在不了解其影响的情况下,不应清除数据目录的内容。

因此在容器中我们也需要挂载对应目录,不然重启之后数据就没了

# ...
   volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data # 数据目录volumes:
  caddy_data: # 这里使用了docker volume来存储

3. Configuration Directory(配置目录)

caddy会把最后一次有效的配置保存到该目录中,也就是说如果你通过API设置的配置也会被持久化到这里。

caddy run --resume 命令启动的时候,就可以继续使用之前的配置,这时候你的自定义配置文件是不生效的

如果设置了 XDG_CONFIG_HOME 环境变量, 位置在 $XDG_CONFIG_HOME/caddy.

没设置的话则取决于系统

OSConfig directory path
Linux, BSD$HOME/.config/caddy
Windows%AppData%\Caddy
macOS$HOME/Library/Application Support/Caddy
Plan 9$HOME/lib/caddy
docker/config

因此在容器中我们也需要挂载对应目录,不然重启之后数据就没了

 # ...
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data     # 数据目录
      - caddy_config:/config # 配置目录volumes:
  caddy_data:
  caddy_config: # 这里使用了docker volume来存储

总结

Caddy是一款易于使用、功能强大的现代Web服务器,适合快速部署,尤其是自动申请和续期Let’s Encrypt的HTTPS证书,真香!其与Nginx相比,最大的优势在于配置简便、内置HTTPS支持及开箱即用的功能,尤其适合中小型网站和开发环境。


✨ 微信公众号【凉凉的知识库】同步更新,欢迎关注获取最新最有用的知识 ✨