web服务器Caddy初体验

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

为什么要使用 Caddy

  1. 安全 Caddy 是一个默认使用https协议的web服务器
  2. 无依赖 Caddy 使用 Go 语言编写,编译好的二进制文件能够运行在任何支持Go语言的平台,不需要自己安装任何库。
  3. 使用简单 Caddy 的配置简单,不管你是新的web开发者,还是专业人士,都能够快速上手
  4. 可实现自动SSL证书申请和续期

一、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的一系列特点

特性 Caddy Nginx
配置方式 Caddyfile, JSON, REST API Nginx配置文件(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下即可

# 1、下载
wget https://github.com/caddyserver/caddy/releases/download/v2.9.1/caddy_2.9.1_linux_amd64.tar.gz
 
# 2、解压
tar -xf caddy_2.9.1_linux_amd64.tar.gz
 
# 3、移动文件到/usr/local/bin/
mv caddy /usr/local/bin/
 
# 4、查看版本

caddy version
直接启动Caddy

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

caddy start

systemctl启动

官方也提供了systemd unit files,配置之后就可以使用systemd来启动了。

我们需要将以下内容写入 /etc/systemd/system/caddy.service 文件:

[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

也可以使用以下命令启动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会自动处理。

方法三:编译安装

Caddy是使用Golang语言开发,自行编译需要先安装Golang开发环境:

安装Golang

# 1、下载
# 下载地址:https://go.dev/dl/
wget https://go.dev/dl/go1.23.5.linux-amd64.tar.gz
 
# 2、解压
tar -xf go1.23.5.linux-amd64.tar.gz -C /usr/local/
 
# 3、添加环境变量
# 添加到环境变量,编辑/root/.bash_profile文件,内容如下: 
GO_PATH=/usr/local/go/bin
PATH=$PATH:$HOME/bin:$GO_PATH
export PATH
 
# 使环境变量生效
source /root/.bash_profile
 
# 4、查看版本
go version
 
# 5、配置goproxy
go env -w GOPROXY=https://goproxy.cn,direct

编译Caddy

# 1、下载
git clone "https://github.com/caddyserver/caddy.git"
 
# 2、切换目录
cd caddy/cmd/caddy/
 
# 3、构建
go build
 
# 4、复制文件到/usr/local/bin/
cp caddy /usr/local/bin/
 
# 5、其他
caddy
 
# 6、启动
# 6.1、前台运行
caddy run
 
# 或者
# 6.2、后台运行
# caddy start --config caddy.json
# caddy run --config nginx.conf --adapter nginx
# caddy run --config caddy.file --adapter caddyfile
caddy start

方法四:官方脚本安装

caddy 官方给出了一个安装脚本,执行上面的命令就可以一键安装 caddy,等执行结束后,使用 which caddy,可以看到 caddy 已经被安装到了 /usr/local/bin/caddy:

curl -s https://getcaddy.com | bash

2. 配置方式

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

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

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

通过Caddyfile文件配置Caddy

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

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

默认情况下Caddyfile文件为位于/etc/caddy/Caddyfile , Caddyfile的基本结构如下:

https://study.disign.me {
    //TLS节点用于自动申请和配置SSL证书
    tls {
        dns cloudflare 这里是操作DNS的Token值。
        resolvers 1.1.1.1
    }
    //这里是反向代理后端服务
    reverse_proxy 127.0.0.1:3000
    log {
        output file /var/log/caddy/access.log {
            mode 644
        }
        format json
    }
}

在这个示例中, https://study.disign.me 是配置的站点名称,后续的内容是针对该站点的配置项。

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/,它是一个目录

没设置的话则取决于系统

OS Data 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.

没设置的话则取决于系统

OS Config 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 常用命令

如果仅仅是本地调试而不是需要长久运行,可以通过简单命令实现启动一个HTTP服务器:

直接启动

caddy run

指定文件启动

你也可以指定一个 Caddyfile:

caddy run --config /path/to/Caddyfile

启动静态文件服务器

如果你需要启动一个静态文件服务器,你可以使用以下命令:

caddy file-server --listen :2015 --root ~/mysite

启动文件浏览器

如果你需要启动一个文件浏览器,你可以使用以下命令:

caddy file-server --browse

反向代理

如果你需要启动一个反向代理,你可以使用以下命令:

caddy reverse-proxy --from :2080 --to :9000

格式化配置文件

如果你需要格式化你的 Caddyfile,你可以使用以下命令:

caddy fmt --overwrite

六、Caddy 安装插件

上面文章介绍了如何安装使用 Caddy,但是 Caddy 的功能有时候并不能满足业务需求,如果想要使用更多的功能,就需要自定义编译 Caddy。

xcaddy 是 Caddy 官方制作的用于自定义编译 Caddy 的工具,它可以帮助我们快速的编译出符合自己需求的 Caddy。

安装 xcaddy

以Debian为例,我们按照官方的安装方法,首先,安装一些必要的软件包:

apt update
apt upgrade -y
apt install curl vim wget gnupg dpkg apt-transport-https lsb-release ca-certificates

配置Golang环境参考上面教程,此处略过。

然后把 go 加入系统环境变量:

curl -sSL https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key | gpg --dearmor > /usr/share/keyrings/xcaddy.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/xcaddy.gpg] https://dl.cloudsmith.io/public/caddy/xcaddy/deb/debian any-version main" > /etc/apt/sources.list.d/xcaddy.list

然后更新系统后即可安装 xcaddy:

apt update
apt install xcaddy

重启打开 SSH 后检查一下 go 和 xcaddy 的版本:

root@debian ~ # go version
go version go1.19.5 linux/amd64

root@debian ~ # xcaddy version
v0.3.1 h1:XKmnGnGTeB53hLUgnGr/R4JbTNSxh8IBAFcJkrtycso=

自定义编译 Caddy

我们可以选择一些自己喜欢的模块,比如缓存模块和 Brotli 压缩模块:

xcaddy build \
  --with github.com/caddy-dns/alidns \
  --with github.com/caddy-dns/cloudflare \
  --with github.com/caddy-dns/dnspod \
  --with github.com/caddy-dns/godaddy \
  --with github.com/caddy-dns/googleclouddns \
  --with github.com/caddyserver/cache-handler \
  --with github.com/ueffel/caddy-brotli

经过一段时间的编译以后,我们就可以在当前目录下看到一个名为 caddy 的二进制文件,这就是我们自定义编译的 Caddy 了。

总结

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

原文阅读