·
·
258 views
· ·

使用Caddy托管多个网站

该文章是由 LLM 从英文版本 翻译而来。

尽管已经做了检查和润色,仍可能存在错翻之处。

This article is part of Myriad Dreamin Blog 2025-06.



我购买了VPS来托管我的网站:个人主页(i.myriad-dreamin.com)和博客镜像站(cn.myriad-dreamin.com)。由于Cloudflare在我国不可用,最好在自有服务器上托管而非通过Cloudflare代理。

目录结构

网站目录结构如下:


            
deployment

            
├── docker-compose.yml

            
├── caddy

            
│   ├── config

            
│   │   └── Caddyfile

            
│   ├── log

            
│   └── data

            
├── nginx

            
│   ├── conf

            
│   │   └── nginx.conf

            
│   └── log

            
├── dist

            
│   ├── i.myriad-dreamin.com

            
│   │   └── index.html

            
│   └── cn.myriad-dreamin.com

            
│       └── index.html

            
└── certbot

            
        ├── ssl

            
        └── www

            
deployment

            
├── docker-compose.yml

            
├── caddy

            
│   ├── config

            
│   │   └── Caddyfile

            
│   ├── log

            
│   └── data

            
├── nginx

            
│   ├── conf

            
│   │   └── nginx.conf

            
│   └── log

            
├── dist

            
│   ├── i.myriad-dreamin.com

            
│   │   └── index.html

            
│   └── cn.myriad-dreamin.com

            
│       └── index.html

            
└── certbot

            
        ├── ssl

            
        └── www

            
deployment

            
├── docker-compose.yml

            
├── caddy

            
│   ├── config

            
│   │   └── Caddyfile

            
│   ├── log

            
│   └── data

            
├── nginx

            
│   ├── conf

            
│   │   └── nginx.conf

            
│   └── log

            
├── dist

            
│   ├── i.myriad-dreamin.com

            
│   │   └── index.html

            
│   └── cn.myriad-dreamin.com

            
│       └── index.html

            
└── certbot

            
        ├── ssl

            
        └── www

            
deployment

            
├── docker-compose.yml

            
├── caddy

            
│   ├── config

            
│   │   └── Caddyfile

            
│   ├── log

            
│   └── data

            
├── nginx

            
│   ├── conf

            
│   │   └── nginx.conf

            
│   └── log

            
├── dist

            
│   ├── i.myriad-dreamin.com

            
│   │   └── index.html

            
│   └── cn.myriad-dreamin.com

            
│       └── index.html

            
└── certbot

            
        ├── ssl

            
        └── www

docker-compose.ymldocker-compose.yml包含所有运行网站的容器。distdist目录存储各网站的静态文件。caddycaddynginxnginx拥有独立目录存放配置文件和日志。certbotcertbot目录包含SSL证书和certbot的webroot。

通过HTTP文件服务器提供distdist内容

我不想使用caddycaddynginxnginx内置的文件服务器,需要更精细的控制(例如永久缓存字体)。因此寻找简单的HTTP文件服务器实现。照例先尝试Rust方案但未成功。

必须承认Rust并非构建Web服务的最佳(或简单)选择。虽有重型框架但不符合需求。转向轻量方案时发现它们维护不善或功能不全。最后尝试了tiny-http,值得关注但仍有不足。

既然要构建网络工具,为何不用Go?我对用Go编写网络工具印象深刻。这是无可争议的良好起点,不到10行代码即可运行:


            
package main

            


            
import (

            
  "log"

            
  "net/http"

            
  "os"

            
)

            


            
func main() {

            
  if len(os.Args) < 2 {

            
    log.Fatal("Usage: file-server <port> (:80)")

            
  }

            
  var port = os.Args[1]

            
  

            
  http.Handle("/", http.FileServer(http.Dir(".")))

            
  

            
  log.Println("Server listening on", port)

            
  log.Fatal(http.ListenAndServe(port, nil))

            
}

            
package main

            


            
import (

            
  "log"

            
  "net/http"

            
  "os"

            
)

            


            
func main() {

            
  if len(os.Args) < 2 {

            
    log.Fatal("Usage: file-server <port> (:80)")

            
  }

            
  var port = os.Args[1]

            
  

            
  http.Handle("/", http.FileServer(http.Dir(".")))

            
  

            
  log.Println("Server listening on", port)

            
  log.Fatal(http.ListenAndServe(port, nil))

            
}

            
package main

            


            
import (

            
  "log"

            
  "net/http"

            
  "os"

            
)

            


            
func main() {

            
  if len(os.Args) < 2 {

            
    log.Fatal("Usage: file-server <port> (:80)")

            
  }

            
  var port = os.Args[1]

            
  

            
  http.Handle("/", http.FileServer(http.Dir(".")))

            
  

            
  log.Println("Server listening on", port)

            
  log.Fatal(http.ListenAndServe(port, nil))

            
}

            
package main

            


            
import (

            
  "log"

            
  "net/http"

            
  "os"

            
)

            


            
func main() {

            
  if len(os.Args) < 2 {

            
    log.Fatal("Usage: file-server <port> (:80)")

            
  }

            
  var port = os.Args[1]

            
  

            
  http.Handle("/", http.FileServer(http.Dir(".")))

            
  

            
  log.Println("Server listening on", port)

            
  log.Fatal(http.ListenAndServe(port, nil))

            
}

我还添加了gzipgzip压缩等改进:


            
// https://gist.github.com/bryfry/09a650eb8aac0fb76c24

            
import (

            
  "compress/gzip"

            
  "io"

            
  "strings"

            
)

            


            
type GzipResponseWriter struct {

            
  io.Writer

            
  http.ResponseWriter

            
}

            


            
func (w GzipResponseWriter) Write(b []byte) (int, error) {

            
  return w.Writer.Write(b)

            
}

            


            
func Gzip(handler http.Handler) http.Handler {

            
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

            
    if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {

            
      handler.ServeHTTP(w, r)

            
      return

            
    }

            
    w.Header().Set("Content-Encoding", "gzip")

            
    gz := gzip.NewWriter(w)

            
    defer gz.Close()

            
    gzw := GzipResponseWriter{Writer: gz, ResponseWriter: w}

            
    handler.ServeHTTP(gzw, r)

            
  })

            
}

            
// https://gist.github.com/bryfry/09a650eb8aac0fb76c24

            
import (

            
  "compress/gzip"

            
  "io"

            
  "strings"

            
)

            


            
type GzipResponseWriter struct {

            
  io.Writer

            
  http.ResponseWriter

            
}

            


            
func (w GzipResponseWriter) Write(b []byte) (int, error) {

            
  return w.Writer.Write(b)

            
}

            


            
func Gzip(handler http.Handler) http.Handler {

            
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

            
    if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {

            
      handler.ServeHTTP(w, r)

            
      return

            
    }

            
    w.Header().Set("Content-Encoding", "gzip")

            
    gz := gzip.NewWriter(w)

            
    defer gz.Close()

            
    gzw := GzipResponseWriter{Writer: gz, ResponseWriter: w}

            
    handler.ServeHTTP(gzw, r)

            
  })

            
}

            
// https://gist.github.com/bryfry/09a650eb8aac0fb76c24

            
import (

            
  "compress/gzip"

            
  "io"

            
  "strings"

            
)

            


            
type GzipResponseWriter struct {

            
  io.Writer

            
  http.ResponseWriter

            
}

            


            
func (w GzipResponseWriter) Write(b []byte) (int, error) {

            
  return w.Writer.Write(b)

            
}

            


            
func Gzip(handler http.Handler) http.Handler {

            
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

            
    if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {

            
      handler.ServeHTTP(w, r)

            
      return

            
    }

            
    w.Header().Set("Content-Encoding", "gzip")

            
    gz := gzip.NewWriter(w)

            
    defer gz.Close()

            
    gzw := GzipResponseWriter{Writer: gz, ResponseWriter: w}

            
    handler.ServeHTTP(gzw, r)

            
  })

            
}

            
// https://gist.github.com/bryfry/09a650eb8aac0fb76c24

            
import (

            
  "compress/gzip"

            
  "io"

            
  "strings"

            
)

            


            
type GzipResponseWriter struct {

            
  io.Writer

            
  http.ResponseWriter

            
}

            


            
func (w GzipResponseWriter) Write(b []byte) (int, error) {

            
  return w.Writer.Write(b)

            
}

            


            
func Gzip(handler http.Handler) http.Handler {

            
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

            
    if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {

            
      handler.ServeHTTP(w, r)

            
      return

            
    }

            
    w.Header().Set("Content-Encoding", "gzip")

            
    gz := gzip.NewWriter(w)

            
    defer gz.Close()

            
    gzw := GzipResponseWriter{Writer: gz, ResponseWriter: w}

            
    handler.ServeHTTP(gzw, r)

            
  })

            
}

并修改主函数使用GzipGzip中间件:


            
 func main() {

            
   ...

            
-  http.Handle("/", http.FileServer(http.Dir(".")))

            
+  fs := http.FileServer(http.Dir("."))

            
+  http.Handle("/", Gzip(fs))

            
   ...

            
 }

            
 func main() {

            
   ...

            
-  http.Handle("/", http.FileServer(http.Dir(".")))

            
+  fs := http.FileServer(http.Dir("."))

            
+  http.Handle("/", Gzip(fs))

            
   ...

            
 }

            
 func main() {

            
   ...

            
-  http.Handle("/", http.FileServer(http.Dir(".")))

            
+  fs := http.FileServer(http.Dir("."))

            
+  http.Handle("/", Gzip(fs))

            
   ...

            
 }

            
 func main() {

            
   ...

            
-  http.Handle("/", http.FileServer(http.Dir(".")))

            
+  fs := http.FileServer(http.Dir("."))

            
+  http.Handle("/", Gzip(fs))

            
   ...

            
 }

再次仅用标准库构建自定义工具。最爱的语言服务器goplsgopls自动完成了所有包导入。

需要HTTPS文件服务器吗?

四年前曾用Go构建HTTPS文件服务器,但这不是最佳实践。考虑到需设置入口控制器,SSL/TLS可在中间层处理,降低复杂度和攻击面。

构建HTTP文件服务器容器

若使用以下命令构建Go程序,无需自定义镜像:


            
CGO_ENABLED=0 go build -tags netgo -o target/file-server ./cmd/file-server

            
CGO_ENABLED=0 go build -tags netgo -o target/file-server ./cmd/file-server

            
CGO_ENABLED=0 go build -tags netgo -o target/file-server ./cmd/file-server

            
CGO_ENABLED=0 go build -tags netgo -o target/file-server ./cmd/file-server

只需启动alpinealpine容器并挂载文件服务器二进制文件即可正常工作。docker-compose.ymldocker-compose.yml如下:


            
services:

            
    homepage:

            
        container_name: homepage

            
        image: alpine:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        working_dir: /app

            
        volumes:

            
            - /usr/local/bin/file-server:/usr/local/bin/file-server:ro

            
            - ./dist/homepage/:/app/

            
        command: 'file-server :80'

            
services:

            
    homepage:

            
        container_name: homepage

            
        image: alpine:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        working_dir: /app

            
        volumes:

            
            - /usr/local/bin/file-server:/usr/local/bin/file-server:ro

            
            - ./dist/homepage/:/app/

            
        command: 'file-server :80'

            
services:

            
    homepage:

            
        container_name: homepage

            
        image: alpine:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        working_dir: /app

            
        volumes:

            
            - /usr/local/bin/file-server:/usr/local/bin/file-server:ro

            
            - ./dist/homepage/:/app/

            
        command: 'file-server :80'

            
services:

            
    homepage:

            
        container_name: homepage

            
        image: alpine:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        working_dir: /app

            
        volumes:

            
            - /usr/local/bin/file-server:/usr/local/bin/file-server:ro

            
            - ./dist/homepage/:/app/

            
        command: 'file-server :80'

使用Nginx构建入口

我同时尝试了Caddy和Nginx,两者都不错。由于试错成本不高,先尝试了Docker官方维护的Nginx镜像:

首先在docker-compose.ymldocker-compose.yml添加Nginx容器:


            
services:

            
  nginx:

            
      container_name: nginx

            
      image: nginx

            
      restart: unless-stopped

            
      ports:

            
          - "80:80"

            
          - "443:443"

            
      environment:

            
          TZ : 'Asia/Shanghai'

            
      volumes:

            
          - ./nginx/conf:/etc/nginx

            
          - ./nginx/web:/usr/share/nginx

            
          - ./nginx/log:/var/log/nginx

            
          - ./certbot/www:/usr/share/certbot/www:ro

            
          - ./certbot/ssl:/usr/share/certbot/ssl:ro

            
      command:  nginx -g 'daemon off;'

            
services:

            
  nginx:

            
      container_name: nginx

            
      image: nginx

            
      restart: unless-stopped

            
      ports:

            
          - "80:80"

            
          - "443:443"

            
      environment:

            
          TZ : 'Asia/Shanghai'

            
      volumes:

            
          - ./nginx/conf:/etc/nginx

            
          - ./nginx/web:/usr/share/nginx

            
          - ./nginx/log:/var/log/nginx

            
          - ./certbot/www:/usr/share/certbot/www:ro

            
          - ./certbot/ssl:/usr/share/certbot/ssl:ro

            
      command:  nginx -g 'daemon off;'

            
services:

            
  nginx:

            
      container_name: nginx

            
      image: nginx

            
      restart: unless-stopped

            
      ports:

            
          - "80:80"

            
          - "443:443"

            
      environment:

            
          TZ : 'Asia/Shanghai'

            
      volumes:

            
          - ./nginx/conf:/etc/nginx

            
          - ./nginx/web:/usr/share/nginx

            
          - ./nginx/log:/var/log/nginx

            
          - ./certbot/www:/usr/share/certbot/www:ro

            
          - ./certbot/ssl:/usr/share/certbot/ssl:ro

            
      command:  nginx -g 'daemon off;'

            
services:

            
  nginx:

            
      container_name: nginx

            
      image: nginx

            
      restart: unless-stopped

            
      ports:

            
          - "80:80"

            
          - "443:443"

            
      environment:

            
          TZ : 'Asia/Shanghai'

            
      volumes:

            
          - ./nginx/conf:/etc/nginx

            
          - ./nginx/web:/usr/share/nginx

            
          - ./nginx/log:/var/log/nginx

            
          - ./certbot/www:/usr/share/certbot/www:ro

            
          - ./certbot/ssl:/usr/share/certbot/ssl:ro

            
      command:  nginx -g 'daemon off;'

nginx/confnginx/conf目录创建配置文件nginx.confnginx.conf


            
events {

            
    worker_connections  4096;

            
}

            
http {

            
    server {

            
        listen 80;

            
        listen [::]:80;

            
        

            
        server_name  orange.myriad-dreamin.com;

            
        server_tokens off;

            
        

            
        location /.well-known/acme-challenge/ {

            
            root /usr/share/certbot/www;

            
        }

            
        location / {

            
            return 301 https://orange.myriad-dreamin.com$request_uri;

            
        }

            
    }

            
}

            
events {

            
    worker_connections  4096;

            
}

            
http {

            
    server {

            
        listen 80;

            
        listen [::]:80;

            
        

            
        server_name  orange.myriad-dreamin.com;

            
        server_tokens off;

            
        

            
        location /.well-known/acme-challenge/ {

            
            root /usr/share/certbot/www;

            
        }

            
        location / {

            
            return 301 https://orange.myriad-dreamin.com$request_uri;

            
        }

            
    }

            
}

            
events {

            
    worker_connections  4096;

            
}

            
http {

            
    server {

            
        listen 80;

            
        listen [::]:80;

            
        

            
        server_name  orange.myriad-dreamin.com;

            
        server_tokens off;

            
        

            
        location /.well-known/acme-challenge/ {

            
            root /usr/share/certbot/www;

            
        }

            
        location / {

            
            return 301 https://orange.myriad-dreamin.com$request_uri;

            
        }

            
    }

            
}

            
events {

            
    worker_connections  4096;

            
}

            
http {

            
    server {

            
        listen 80;

            
        listen [::]:80;

            
        

            
        server_name  orange.myriad-dreamin.com;

            
        server_tokens off;

            
        

            
        location /.well-known/acme-challenge/ {

            
            root /usr/share/certbot/www;

            
        }

            
        location / {

            
            return 301 https://orange.myriad-dreamin.com$request_uri;

            
        }

            
    }

            
}

注意location /.well-known/acme-challenge/location /.well-known/acme-challenge/用于certbot的HTTP挑战认证,location /location /将所有HTTP流量重定向到HTTPS。

运行docker compose up -d nginxdocker compose up -d nginx启动Nginx容器,监听80和443端口。

使用Certbot生成SSL证书

docker-compose.ymldocker-compose.yml添加certbotcertbot容器:


            
services:

            
    certbot:

            
      container_name: certbot

            
      image: certbot/certbot

            
      volumes:

            
          - ./certbot/www:/usr/share/certbot/www:rw

            
          - ./certbot/ssl:/etc/letsencrypt:rw

            
services:

            
    certbot:

            
      container_name: certbot

            
      image: certbot/certbot

            
      volumes:

            
          - ./certbot/www:/usr/share/certbot/www:rw

            
          - ./certbot/ssl:/etc/letsencrypt:rw

            
services:

            
    certbot:

            
      container_name: certbot

            
      image: certbot/certbot

            
      volumes:

            
          - ./certbot/www:/usr/share/certbot/www:rw

            
          - ./certbot/ssl:/etc/letsencrypt:rw

            
services:

            
    certbot:

            
      container_name: certbot

            
      image: certbot/certbot

            
      volumes:

            
          - ./certbot/www:/usr/share/certbot/www:rw

            
          - ./certbot/ssl:/etc/letsencrypt:rw

执行试运行检查配置:


            
docker compose run --rm  certbot certonly --webroot --webroot-path /usr/share/certbot/www/ --dry-run -d orange.myriad-dreamin.com

            
docker compose run --rm  certbot certonly --webroot --webroot-path /usr/share/certbot/www/ --dry-run -d orange.myriad-dreamin.com

            
docker compose run --rm  certbot certonly --webroot --webroot-path /usr/share/certbot/www/ --dry-run -d orange.myriad-dreamin.com

            
docker compose run --rm  certbot certonly --webroot --webroot-path /usr/share/certbot/www/ --dry-run -d orange.myriad-dreamin.com

移除--dry-run--dry-run标志获取真实证书。成功后证书将存储在certbot/sslcertbot/ssl目录。

通过Nginx提供HTTPS服务

SSL证书应位于/usr/share/certbot/ssl/live/orange.myriad-dreamin.com/usr/share/certbot/ssl/live/orange.myriad-dreamin.com。在nginx.confnginx.conf添加服务器块处理HTTPS流量:


            
http {

            
    log_format main  '$remote_addr - $remote_user [$time_local] "$request" '

            
                  'status=$status body_bytes_sent=$body_bytes_sent http_referer="$http_referer" '

            
                  'http_user_agent="$http_user_agent" http_x_forwarded_for="$http_x_forwarded_for"';

            
                  

            
    server {

            
        listen       443 ssl;

            
        listen [::]:443  ssl;

            
        server_name  orange.myriad-dreamin.com;

            
        

            
        access_log  /var/log/nginx/orange.myriad-dreamin.com.access.log  main;

            
        error_log  /var/log/nginx/orange.myriad-dreamin.com.error.log;

            
        

            
        ssl_certificate /usr/share/certbot/ssl/live/orange.myriad-dreamin.com/fullchain.pem;

            
        ssl_certificate_key /usr/share/certbot/ssl/live/orange.myriad-dreamin.com/privkey.pem;

            
        ssl_session_timeout 5m;

            
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

            
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;

            
        ssl_prefer_server_ciphers on;

            
        

            
        location / {

            
            proxy_pass http://homepage;

            
            proxy_set_header Host $http_host;

            
            proxy_set_header X-Real-IP $remote_addr;

            
            proxy_set_header REMOTE-HOST $remote_addr;

            
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            
        }

            
    }

            
}

            
http {

            
    log_format main  '$remote_addr - $remote_user [$time_local] "$request" '

            
                  'status=$status body_bytes_sent=$body_bytes_sent http_referer="$http_referer" '

            
                  'http_user_agent="$http_user_agent" http_x_forwarded_for="$http_x_forwarded_for"';

            
                  

            
    server {

            
        listen       443 ssl;

            
        listen [::]:443  ssl;

            
        server_name  orange.myriad-dreamin.com;

            
        

            
        access_log  /var/log/nginx/orange.myriad-dreamin.com.access.log  main;

            
        error_log  /var/log/nginx/orange.myriad-dreamin.com.error.log;

            
        

            
        ssl_certificate /usr/share/certbot/ssl/live/orange.myriad-dreamin.com/fullchain.pem;

            
        ssl_certificate_key /usr/share/certbot/ssl/live/orange.myriad-dreamin.com/privkey.pem;

            
        ssl_session_timeout 5m;

            
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

            
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;

            
        ssl_prefer_server_ciphers on;

            
        

            
        location / {

            
            proxy_pass http://homepage;

            
            proxy_set_header Host $http_host;

            
            proxy_set_header X-Real-IP $remote_addr;

            
            proxy_set_header REMOTE-HOST $remote_addr;

            
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            
        }

            
    }

            
}

            
http {

            
    log_format main  '$remote_addr - $remote_user [$time_local] "$request" '

            
                  'status=$status body_bytes_sent=$body_bytes_sent http_referer="$http_referer" '

            
                  'http_user_agent="$http_user_agent" http_x_forwarded_for="$http_x_forwarded_for"';

            
                  

            
    server {

            
        listen       443 ssl;

            
        listen [::]:443  ssl;

            
        server_name  orange.myriad-dreamin.com;

            
        

            
        access_log  /var/log/nginx/orange.myriad-dreamin.com.access.log  main;

            
        error_log  /var/log/nginx/orange.myriad-dreamin.com.error.log;

            
        

            
        ssl_certificate /usr/share/certbot/ssl/live/orange.myriad-dreamin.com/fullchain.pem;

            
        ssl_certificate_key /usr/share/certbot/ssl/live/orange.myriad-dreamin.com/privkey.pem;

            
        ssl_session_timeout 5m;

            
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

            
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;

            
        ssl_prefer_server_ciphers on;

            
        

            
        location / {

            
            proxy_pass http://homepage;

            
            proxy_set_header Host $http_host;

            
            proxy_set_header X-Real-IP $remote_addr;

            
            proxy_set_header REMOTE-HOST $remote_addr;

            
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            
        }

            
    }

            
}

            
http {

            
    log_format main  '$remote_addr - $remote_user [$time_local] "$request" '

            
                  'status=$status body_bytes_sent=$body_bytes_sent http_referer="$http_referer" '

            
                  'http_user_agent="$http_user_agent" http_x_forwarded_for="$http_x_forwarded_for"';

            
                  

            
    server {

            
        listen       443 ssl;

            
        listen [::]:443  ssl;

            
        server_name  orange.myriad-dreamin.com;

            
        

            
        access_log  /var/log/nginx/orange.myriad-dreamin.com.access.log  main;

            
        error_log  /var/log/nginx/orange.myriad-dreamin.com.error.log;

            
        

            
        ssl_certificate /usr/share/certbot/ssl/live/orange.myriad-dreamin.com/fullchain.pem;

            
        ssl_certificate_key /usr/share/certbot/ssl/live/orange.myriad-dreamin.com/privkey.pem;

            
        ssl_session_timeout 5m;

            
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

            
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;

            
        ssl_prefer_server_ciphers on;

            
        

            
        location / {

            
            proxy_pass http://homepage;

            
            proxy_set_header Host $http_host;

            
            proxy_set_header X-Real-IP $remote_addr;

            
            proxy_set_header REMOTE-HOST $remote_addr;

            
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            
        }

            
    }

            
}

在Docker Compose环境下,http://homepagehttp://homepage通过Docker内部DNS解析到文件服务器容器。添加新站点只需复制orange.myriad-dreamin.comorange.myriad-dreamin.com的两个服务器块并修改server_nameserver_name

恶意访问者

日志显示有恶意尝试访问/admin/admin/login/login等常见路径。幸运的是网站只有静态文件,且Nginx/Golang文件服务器足够健壮。虽然Nginx已使用20年,但仍有CVE漏洞。Caddy性能稍弱但个人网站无需高吞吐。traefiktraefik过于复杂,最终决定尝试Caddy。

使用Caddy提供HTTP服务

首先在docker-compose.ymldocker-compose.yml添加caddycaddy容器:


            
services:

            
    caddy:

            
        container_name: caddy

            
        image: caddy:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        ports:

            
        - "80:80"

            
        - "443:443"

            
        - "443:443/udp"

            
        volumes:

            
        - ./caddy/config:/etc/caddy

            
        - ./caddy/data:/data

            
        - ./caddy/log:/var/log/caddy

            
services:

            
    caddy:

            
        container_name: caddy

            
        image: caddy:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        ports:

            
        - "80:80"

            
        - "443:443"

            
        - "443:443/udp"

            
        volumes:

            
        - ./caddy/config:/etc/caddy

            
        - ./caddy/data:/data

            
        - ./caddy/log:/var/log/caddy

            
services:

            
    caddy:

            
        container_name: caddy

            
        image: caddy:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        ports:

            
        - "80:80"

            
        - "443:443"

            
        - "443:443/udp"

            
        volumes:

            
        - ./caddy/config:/etc/caddy

            
        - ./caddy/data:/data

            
        - ./caddy/log:/var/log/caddy

            
services:

            
    caddy:

            
        container_name: caddy

            
        image: caddy:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        ports:

            
        - "80:80"

            
        - "443:443"

            
        - "443:443/udp"

            
        volumes:

            
        - ./caddy/config:/etc/caddy

            
        - ./caddy/data:/data

            
        - ./caddy/log:/var/log/caddy

caddy/configcaddy/config目录创建CaddyfileCaddyfile


            
:80 {

            
  respond "Hello World!"

            
}

            
:80 {

            
  respond "Hello World!"

            
}

            
:80 {

            
  respond "Hello World!"

            
}

            
:80 {

            
  respond "Hello World!"

            
}

运行docker compose up -d caddydocker compose up -d caddy后访问http://localhost:80http://localhost:80应显示"Hello World!"。

通过Caddy提供HTTPS服务

Caddy可自动维护SSL证书,无需certbot。配置HTTPS服务器异常简单:


            
orange.myriad-dreamin.com {

            
  reverse_proxy homepage

            
}

            
orange.myriad-dreamin.com {

            
  reverse_proxy homepage

            
}

            
orange.myriad-dreamin.com {

            
  reverse_proxy homepage

            
}

            
orange.myriad-dreamin.com {

            
  reverse_proxy homepage

            
}

其中homepagehomepage由Docker内部DNS解析。执行以下命令热重载配置:


            
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

            
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

            
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

            
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

比Nginx更简洁!且Caddy用Go编写,避免内存错误。

记录访问日志

Caddy支持纯文本和JSON格式访问日志。启用日志需在CaddyfileCaddyfile添加:


            
(subdomain-log) {

            
  log {

            
    hostnames {args[0]}

            
    format json

            
    output file /var/log/caddy/{args[0]}.jsonl {

            
      roll_size 100MiB

            
      roll_keep 3

            
      roll_keep_for 720h

            
    }

            
  }

            
}

            
(subdomain-log) {

            
  log {

            
    hostnames {args[0]}

            
    format json

            
    output file /var/log/caddy/{args[0]}.jsonl {

            
      roll_size 100MiB

            
      roll_keep 3

            
      roll_keep_for 720h

            
    }

            
  }

            
}

            
(subdomain-log) {

            
  log {

            
    hostnames {args[0]}

            
    format json

            
    output file /var/log/caddy/{args[0]}.jsonl {

            
      roll_size 100MiB

            
      roll_keep 3

            
      roll_keep_for 720h

            
    }

            
  }

            
}

            
(subdomain-log) {

            
  log {

            
    hostnames {args[0]}

            
    format json

            
    output file /var/log/caddy/{args[0]}.jsonl {

            
      roll_size 100MiB

            
      roll_keep 3

            
      roll_keep_for 720h

            
    }

            
  }

            
}

并在各站点块引用:


            
 orange.myriad-dreamin.com {

            
+  import subdomain-log orange.myriad-dreamin.com

            
   reverse_proxy homepage

            
 }

            
 orange.myriad-dreamin.com {

            
+  import subdomain-log orange.myriad-dreamin.com

            
   reverse_proxy homepage

            
 }

            
 orange.myriad-dreamin.com {

            
+  import subdomain-log orange.myriad-dreamin.com

            
   reverse_proxy homepage

            
 }

            
 orange.myriad-dreamin.com {

            
+  import subdomain-log orange.myriad-dreamin.com

            
   reverse_proxy homepage

            
 }

我偏好结构化JSON日志便于解析。hl是优秀的JSON日志解析工具:


            
$ hl caddy/log/orange.myriad-dreamin.com.jsonl

            
Jun 01 01:02:03.456 [INF] http.log.access.log0: handled request request.remote-ip=a.b.c.d request.remote-port="xyz" request.client-ip=a.b.c.d ...

            
$ hl caddy/log/orange.myriad-dreamin.com.jsonl

            
Jun 01 01:02:03.456 [INF] http.log.access.log0: handled request request.remote-ip=a.b.c.d request.remote-port="xyz" request.client-ip=a.b.c.d ...

            
$ hl caddy/log/orange.myriad-dreamin.com.jsonl

            
Jun 01 01:02:03.456 [INF] http.log.access.log0: handled request request.remote-ip=a.b.c.d request.remote-port="xyz" request.client-ip=a.b.c.d ...

            
$ hl caddy/log/orange.myriad-dreamin.com.jsonl

            
Jun 01 01:02:03.456 [INF] http.log.access.log0: handled request request.remote-ip=a.b.c.d request.remote-port="xyz" request.client-ip=a.b.c.d ...

实际上copilot帮助我更可读地聚合展示了访问日志。

代码清单

docker-compose.ymldocker-compose.yml:


            
services:

            
    caddy:

            
        container_name: caddy

            
        image: caddy:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        ports:

            
        - "80:80"

            
        - "443:443"

            
        - "443:443/udp"

            
        volumes:

            
        - ./caddy/config:/etc/caddy

            
        - ./caddy/data:/data

            
        - ./caddy/log:/var/log/caddy

            
    homepage:

            
        container_name: homepage

            
        image: alpine:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        working_dir: /app

            
        volumes:

            
            - /usr/local/bin/file-server:/usr/local/bin/file-server:ro

            
            - ./dist/homepage/:/app/

            
        command: 'file-server :80'

            
services:

            
    caddy:

            
        container_name: caddy

            
        image: caddy:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        ports:

            
        - "80:80"

            
        - "443:443"

            
        - "443:443/udp"

            
        volumes:

            
        - ./caddy/config:/etc/caddy

            
        - ./caddy/data:/data

            
        - ./caddy/log:/var/log/caddy

            
    homepage:

            
        container_name: homepage

            
        image: alpine:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        working_dir: /app

            
        volumes:

            
            - /usr/local/bin/file-server:/usr/local/bin/file-server:ro

            
            - ./dist/homepage/:/app/

            
        command: 'file-server :80'

            
services:

            
    caddy:

            
        container_name: caddy

            
        image: caddy:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        ports:

            
        - "80:80"

            
        - "443:443"

            
        - "443:443/udp"

            
        volumes:

            
        - ./caddy/config:/etc/caddy

            
        - ./caddy/data:/data

            
        - ./caddy/log:/var/log/caddy

            
    homepage:

            
        container_name: homepage

            
        image: alpine:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        working_dir: /app

            
        volumes:

            
            - /usr/local/bin/file-server:/usr/local/bin/file-server:ro

            
            - ./dist/homepage/:/app/

            
        command: 'file-server :80'

            
services:

            
    caddy:

            
        container_name: caddy

            
        image: caddy:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        ports:

            
        - "80:80"

            
        - "443:443"

            
        - "443:443/udp"

            
        volumes:

            
        - ./caddy/config:/etc/caddy

            
        - ./caddy/data:/data

            
        - ./caddy/log:/var/log/caddy

            
    homepage:

            
        container_name: homepage

            
        image: alpine:latest

            
        restart: unless-stopped

            
        environment:

            
            TZ : 'Asia/Shanghai'

            
        working_dir: /app

            
        volumes:

            
            - /usr/local/bin/file-server:/usr/local/bin/file-server:ro

            
            - ./dist/homepage/:/app/

            
        command: 'file-server :80'

caddy/config/Caddyfilecaddy/config/Caddyfile:


            
(subdomain-log) {

            
  log {

            
    hostnames {args[0]}

            
    format json

            
    output file /var/log/caddy/{args[0]}.jsonl {

            
      roll_size 100MiB

            
      roll_keep 3

            
      roll_keep_for 720h

            
    }

            
  }

            
}

            


            
orange.myriad-dreamin.com {

            
  import subdomain-log orange.myriad-dreamin.com

            
  reverse_proxy homepage

            
}

            
(subdomain-log) {

            
  log {

            
    hostnames {args[0]}

            
    format json

            
    output file /var/log/caddy/{args[0]}.jsonl {

            
      roll_size 100MiB

            
      roll_keep 3

            
      roll_keep_for 720h

            
    }

            
  }

            
}

            


            
orange.myriad-dreamin.com {

            
  import subdomain-log orange.myriad-dreamin.com

            
  reverse_proxy homepage

            
}

            
(subdomain-log) {

            
  log {

            
    hostnames {args[0]}

            
    format json

            
    output file /var/log/caddy/{args[0]}.jsonl {

            
      roll_size 100MiB

            
      roll_keep 3

            
      roll_keep_for 720h

            
    }

            
  }

            
}

            


            
orange.myriad-dreamin.com {

            
  import subdomain-log orange.myriad-dreamin.com

            
  reverse_proxy homepage

            
}

            
(subdomain-log) {

            
  log {

            
    hostnames {args[0]}

            
    format json

            
    output file /var/log/caddy/{args[0]}.jsonl {

            
      roll_size 100MiB

            
      roll_keep 3

            
      roll_keep_for 720h

            
    }

            
  }

            
}

            


            
orange.myriad-dreamin.com {

            
  import subdomain-log orange.myriad-dreamin.com

            
  reverse_proxy homepage

            
}

Comments