个人服务器自动化部署

之前自己做了一个小项目,用于记单词。因为目前的背单词app主要还是记拼写,而我比较欠缺的是记读音,很多单词看见认识,但是听到读音的不知道是哪个单词。所以想着做个程序来记读音,可以先听读音再选是否记住。

大概背景就是这样。做这个主要也是为了体验一下完整前端后端开发,以及部署的整个流程。毕竟工作中主要是后端,而且其实只做后端的一部分。做个小项目可以体验一下全流程。这次我用了fastapi作为后端,了解一下这个很火的新框架,前端暂时是vue,不过前端部分不会的太多,目标先是能用。

开发的事暂时按下不表,先来说说是如何部署的。在以前我玩wordpress时,还都是用个lnmp的脚本,然后把php程序上传到服务器,然后启动。之后在人人车,也是差不多,用git拉下来更新,重启服务,用supervisor自动重启挂掉的服务。而到了头条,一下子就先进了,有TCE系统,点一下就发布新镜像部署了。现在shopee的话,介于两者之间,也算是有自动化的部署流程,只不过系统还没整合到一起。对我自己来说,还真没有实践过容器部署和运维,所以也借这个项目实践一下。

我只买了一台丐中丐级别的vps,buyvm的512mb。一年只要20刀。不过没有新加坡节点,延时大一些。k8s/k3s暂时先不体验了,不想一步迈的太大,配置也不够,咋看起来复杂度也很高。

我主要参考的是这个文章,用的docker swarm加上portainer作为ui来管理,只不过我用了图形化nginx管理工具。这个项目比较简单,数据库用的postgresql(也是我不熟悉的,pgadmin不好用啊🤣),会有一个contianer,然后python后端起一个容器。前端是一个nginx的容器。我用到了nginx proxy manager来管理http/s,这个工具可以设置自动更新ssl证书,另外有ui界面,将subdomain分给portainer,前端,后端。这部分可以用traefik做,不过我还没有研究怎么写配置。

NPM配置

version: "3"

networks:
  nginx-net:
    external: true

volumes:
  nginx-data:
    external:
      name: nginx-data
  letsencrypt:
    external:
      name: letsencrypt

services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    ports:
      # HTTP port
      - '80:80'
      # HTTPS Port:
      - '443:443'
      # Admin UI
      # - '81:81'
    environment:
      DB_SQLITE_FILE: "/data/npm.sqlite"
    volumes:
      - nginx-data:/data
      - letsencrypt:/etc/letsencrypt
    networks:
      - nginx-net

npm没有使用官方的mysql配置,用了sqlite数据库,另外设置了两个volumes,用于存配置和证书。在https转发没配置好之前,81端口是对外开发的,在配置好后,注释掉,只保留80和443

有了这些,只是能让服务跑起来,还没有达到学习目的。为了自动化部署,还有些额外的工作。首先是镜像仓库,鉴于docker/github的私有镜像库免费额度不太够,所以我和那个教程介绍的一样,部署了自己的registry。和教程不一样的,我用了github action,用github action来触发工作流,当在github里发布之后,会触发工作流打包镜像,然后推到自己的registry,然后触发portainer的webhook,更新docker service。这里注意我在生成registry的密码时,用的参考文章的命令有问题,又查了下docker的文档

docker run --entrypoint htpasswd httpd:2 -Bbn testuser testpassword > auth/htpasswd

github actions

name: Build and Publish Docker image
on: 
  release:
    types: [published]
jobs:
  push_to_registries:
    name: Build and Push Docker image to multiple registries
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: read
    steps:
      - id: get_version
        uses: battila7/get-version-action@v2
      - name: Check out the repo
        uses: actions/checkout@v2
      - name: Log in to Docker Hub
        uses: docker/login-action@v1
        with:
          registry: ${{ secrets.DOCKER_REGISTRY }}
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      - name: Build BE image
        uses: docker/build-push-action@v2
        with:
          push: true
          context: server
          tags: |
            ${{ secrets.DOCKER_REGISTRY }}/xxx-be:latest
            ${{ secrets.DOCKER_REGISTRY }}/xxx-be:${{ steps.get_version.outputs.version-without-v }}
      - name: Invoke BE deploy webhook
        uses: joelwmale/webhook-action@master
        with:
          url: ${{ secrets.BE_WEBHOOK_URL }}
      - name: Build FE image
        uses: docker/build-push-action@v2
        with:
          push: true
          context: web_fe
          build-args: VUE_APP_BASE_URL=${{ secrets.VUE_APP_BASE_URL }}
          tags: |
            ${{ secrets.DOCKER_REGISTRY }}/xxx-fe:latest
            ${{ secrets.DOCKER_REGISTRY }}/xxx-fe:${{ steps.get_version.outputs.version-without-v }}
      - name: Invoke FE deploy webhook
        uses: joelwmale/webhook-action@master
        with:
          url: ${{ secrets.FE_WEBHOOK_URL }}

github actions这里是前端后端同时部署,可以分开两个action。我用到了取release version的,这样在push到registry时可以附带上version,我们就可以回滚服务了。我这里触发条件是release,所以在merge到main(是的,现在默认变成main分支了)时并不触发部署,只有在release页面打tag后才会。

有几个坑 - 为了安全,会把portainer的web端口不暴露在外,由nginx转发,所以在调整nginx容器时一旦挂了,portainer ui也会挂掉。所以一般可以先把9001 ui端口对外打开,设置好nginx之后,再更新portainer的设置。 - 同理,nginx proxy manager 也有个81端口,在配置好https转发之后,应该更新docker配置,不要让81端口对外。 - 如果有多台机器组网,对于portainer和db,应该使用配置固定到一台实体机上docker-swarm deploy placement constraints,因为volume只在一台上,实际上有容器部署db也不算是好的实践啦。 - 镜像如果有版本tag,registry会越来越多,如果空间小的话,要注意清理,前端的镜像应该比较小,我的python的镜像好像是150多mb。对于总共10g的vps来说,还挺占地方的。 - 在portainer新建network时,主要加上attachable,不然其他容器后期无法加入网络。