将 Ghost 博客迁移到家里的 Raspberry PI

我这个博客最开始的时候使用的是 DigitalOcean 的最便宜的服务,后来 AWS 推出了更便宜每月 3 .5$ 的 LightSail ,我变把博客迁移到了 LightSail。由于博客本身使用的是 Docker 部署,博客使用的是 Ghost 的 self-hosted,只需使用 scp 将所有的数据从一台服务器拷贝到另外一台服务器即可。

scp [OPTIONS] [[user@]src_host:]file1 [[user@]dest_host:]file2

前几年我买了一块树莓派 4b, 心想着可以做一些好玩的事情,也可以把博客从 LightSail 迁移过来,每个月还可以节省一些钱,虽然很少但聊胜于无,遗憾的是一直没有行动。

刚这次假期的业余时间也很充沛,就刚好可以行动起来。迁移之前,我也研究了一下大概选择什么样的方案,我希望这个方案不单单可以用来部署我的博客也可以用来部署其他的一些应用。这时候 Coolify 进入了我眼前,这是一个类似于 Netlify 的自建服务,可以方便创建各种应用还支持各种数据库,功能强大。但是对于我来说现在的数据都是存储在本地的 SQLite 数据库的文件中,需要花一些时间将博客升级到最新版然后升级数据库才行。而且我还希望使用 Cloudflare Argo Tunnel 做网络出口,毕竟这是一台在家里的树莓派,需要通过 DDNS 才能使得机器联网,而 Cloudflare Argo Tunel 相对更安全。

后来我发现了另外一个服务 k3s ,是一个精简版的 Kubernetes 的软件,移除了很多不必要的功能使得可以在树莓派中也可以运行的非常高效。

安装 K3s

curl -sfL https://get.k3s.io | sh -

如果你在多台机器上安装,需要在配置完一台服务器后使用下面的命令:

curl -sfL https://get.k3s.io | K3S_URL=https://myserver:6443 K3S_TOKEN=mynodetoken sh -

如果你使用的树莓派你需要检查一下是否开启了cgrop,查看/boot/cmdline.txt 是否含有cgroup_memory=1 cgroup_enable=memory,如果没有将这段文字添加到最后然后重启服务器。

创建 Storage

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: xeodou-me-vol-claim
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 1Gi

创建 Pod

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-blog
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test-blog
  template:
    metadata:
      labels:
        app: test-blog
    spec:
      containers:
      - name: test-blog
        image: ghost:5-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 2368
        env:
        - name: url
          value: https://your_domain.com
        volumeMounts:
        - mountPath: /var/lib/ghost/content
          name: test-blog-vol
      volumes:
        - name: test-blog-vol
          persistentVolumeClaim:
            claimName: test-blog-vol-claim

这个配置会在默认的 namespace default 里面创建一个名为test-blog 的 Deployment 和 Pod, 创建的一个存储卷 test-blog-vol-claim。这个 Deployment 使用的是 ghost:5-alpine docker 镜像,然后我们会将我们创建好的存储卷映射到 Docker 服务的 /var/lib/ghost/content 的路径下面,服务启动后我们可以通过 2368 端口访问。

创建 Service

我们希望我们的博客可以被其他服务访问,比如 Cloudflare Argo Tunnel 的代理访问,这时候我们通过创建一个 service 将我们的博客暴露给其他服务。

apiVersion: v1
kind: Service
metadata:
  name: test-me-blog
spec:
  selector:
    app: test-me-blog
  ports:
  - protocol: TCP
    port: 2368
    targetPort: 2368
  type: ClusterIP

这时候我们在 Kubernetes 的内部就可以通过 http://test-me-blog:2368 来访问了。但是我们在访问我们的博客的是会发现博客会一直将 http 链接跳转到 https,然后我们会因为证书问题无法继续访问。

这是因为:
Ghost 博客集成了 Stripe 的支付服务,在生产环境也就是NODE_ENV=production的时候会强制检测是否使用了 HTTPS,或者在一台开启了 HTTPS的反向代理服务后也就是请求头中是否携带 X-Forwarded-Proto: https
https://ghost.org/docs/faq/proxying-https-infinite-loops/

配置 Ingress

因为我并没有想通过自建 k3s 暴露给公网的想法,只能通过类似于实现 Nginx 中的反向代理来修改请求头。这时候我们需要创建一个 Ingress 然后通过I ngress 的plugin 来实现。

apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: test-me-blog-x-forwarded-proto-https
spec:
  headers:
    customRequestHeaders:
      X-Forwarded-Proto: https
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-me-blog
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: web
    traefik.ingress.kubernetes.io/router.middlewares: default-test-me-blog-x-forwarded-proto-https@kubernetescrd
spec:
 rules:
  - host: your_blog_domain.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: test-me-blog
            port:
              number: 2368

这个配置会创建一个将所有请求your_blog_domain.com 的流量都指向我们的 test-me-blog service,然后我们通过创建的一个插件 test-me-blog-x-forwarded-proto-https 在所有请求的头都加上 X-Forwarded-Proto: https

使用 kubectl apply -f ingress.yml 后我们就可以通过以下的命令访问我们的博客了。

curl -H 'Host: your_blog_domain.com' localhost

数据迁移

也是同样使用 scp 命令将我存储在 LightSail 的数据拷贝到我们创建的 storage 里面就可以了。

成本

我用的是树莓派 4b,功率大概是 4.5w 满载大概6w。一年的功耗大概在功率*365*24 = 40 kw左右,东京的电费是大概 18 日元每度电,一年的花费大概是 5.5$ 左右。