NAS 上的 Docker

前言

因为笔者 NAS 之前运行的 Windows Server 2019 授权过期,又恰巧 Ubuntu 20.04 LTS 发布,就把 NAS 的各种功能迁移到了 Ubuntu 中,并用 Docker 替代原有的 Hyper-V 虚拟机,一次性解决运行环境的依赖问题,体验了肉眼可见的性能提升,以下即是迁(zhe)移(teng)过程的要点笔记。

Docker

在 2020 年,Docker 已经不是新事物了,许多开源项目都在 Docker Hub 上发布了开箱即用的容器镜像,本文也是直接使用了现成的官方镜像运行 NAS 上的容器,并且用 Docker Compose 管理、运行这些容器。

Docker Compose 是一个能把多个容器组合成为一套应用的管理工具,它采用 YAML 格式的配置文件docker-compose.yml,代替 Docker 容器所需的冗长命令行参数。

安装与配置

首先来安装 Docker Compose。Ubuntu 下的docker包是图形界面托盘wmdocker的过渡包,与容器无关。docker.io才是本尊。

$ sudo apt install docker.io docker-compose

把当前用户添加到docker组中,就可以免去sudo直接调用dockerdocker-compose了。

$ sudo gpasswd -a $USER docker

确认安装是否成功以及安装的版本。

$ docker -v
Docker version 19.03.8, build afacb8b7f0
$ docker-compose -v
docker-compose version 1.25.0, build unknown

若要把 Docker 数据放在默认/var/lib/docker以外的位置,可以编辑/etc/docker/daemon.json加入以下内容。

{
  "data-root": "/data0/dockerlib"
}

daemon.json中几乎可以配置所有的dockerd选项,其中的一个例外就是代理。但由于众所周知的网络环境问题,在国内从Docker Hub下载镜像的速度难以忍受,不得不给dockerd配置代理。

Ubuntu 中的dockerdsystemd管理,那么编辑docker.service配置。

$ sudo systemctl edit docker

根据你的代理具体配置,在其中写入下面的内容。

[Service]
Environment="HTTPS_PROXY=http://host:port"
Environment="HTTP_PROXY=http://host:port"
Environment="NO_PROXY=localhost,127.0.0.0/8,192.168.0.0/16"

保存后,重新加载systemd配置文件,检查代理配置。

$ sudo systemctl daemon-reload
$ sudo systemctl show --property Environment docker
Environment=HTTPS_PROXY=http://host:port HTTP_PROXY=http://host:port NO_PROXY...

重启dockerd让代理配置生效。

$ sudo systemctl restart docker

Hello World

运行hello-world镜像,确认 Docker 环境配置无误。因为本地没有hello-world的镜像,Docker 会自动从 Docker Hub 中拉取。若 Docker 能正常拉取镜像并运行容器,则会输出类似下面的内容。

$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:6a65f928fb91fcfbc963f7aa6d57c8eeb426ad9a20c7ee045538ef34847f44f1
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.
...

docker ps命令即可看到我们用hello-world镜像创建的容器,在输出内容后处于正常退出(Exited)状态。

$ docker ps --all | grep hello-world
25063973ebe3        hello-world            "/hello"                 28 seconds ago      Exited (0) 27 seconds ago

至此,Docker 的安装与配置就完成了,只是还没试过 Docker Compose,接下来将会给各个应用编写相应的docker-compose.yml

NAS 应用

笔者为不同应用绑定了不同的域名,因此需要复用 HTTP/HTTPS 端口以及同一个 nginx 实例作为反向代理。如果在这个 nginx 实例的配置中通过主机名访问其他应用的容器,则会出现其他应用未启动容器时,无法解析容器的主机名而无法启动。为了解除这种奇怪的依赖,笔者把各个应用都映射到主机的端口,然后在主机网络(host network)运行这一 nginx 实例,反向代理各个应用的端口。

网络结构

NextCloud

笔者根据 NextCloud 官方提供的样例做了一些调整。docker-compose.yml和涉及的额外文件目录结构如下。

.
├── db.env
├── docker-compose.yml
└── web
    ├── Dockerfile
    └── nginx.conf

1 directory, 4 files

Compose

首先来看docker-compose.yml中定义的服务、网络、卷。

version: '3.7'

services:
  db:
    image: mariadb
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    restart: always
    networks:
      - nextcloud
    volumes:
      - db:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=<root_password>
    env_file:
      - db.env

  redis:
    image: redis:alpine
    restart: always
    networks:
      - nextcloud
    volumes:
      - redis:/data

  app:
    image: nextcloud:fpm-alpine
    restart: always
    networks:
      - nextcloud
    volumes:
      - app:/var/www/html
      - data:/var/www/html/data
    environment:
      - MYSQL_HOST=db
      - REDIS_HOST=redis
    env_file:
      - db.env
    depends_on:
      - db
      - redis

  cron:
    image: nextcloud:fpm-alpine
    restart: always
    networks:
      - nextcloud
    volumes:
      - app:/var/www/html
      - data:/var/www/html/data
    entrypoint: /cron.sh
    depends_on:
      - db
      - redis

  web:
    image: nginx:nextcloud
    build: ./web
    restart: always
    networks:
      - nextcloud
    ports:
      - 10080:80
    volumes:
      - app:/var/www/html:ro

volumes:
  db:
  redis:
  app:
  data:
    driver_opts:
      type: "none"
      o: "bind"
      device: "/host/folder"

networks:
  nextcloud:

关键配置如下。

  • 服务db:MariaDB 实例。它使用命名卷db作为 MariaDB 的默认存储位置,从db.env文件加载环境变量,并额外添加环境变量设置 MariaDB 的 root 用户密码。
  • 服务redis:Alpine 中的 Redis 实例,作为 NextCloud 的缓存。它使用命名卷redis作为 redis 的默认存储位置。
  • 服务app:Alpine 中的 PHP-FPM 运行的 NextCloud 实例,用环境变量指定了 MariaDB 和 Redis 的主机名。它使用命名卷app作为 NextCloud 源码根目录,命名卷data作为 NextCloud 的用户数据存储。
  • 服务cron:Alpine 中的 NextCloud Cron 脚本实例。它与app复用命名卷appdata
  • 服务web:从web文件夹中的DockerFile构建自定义镜像nginx:nextcloud。以只读权限复用命名卷app,作为 PHP-FPM 的反向代理,并映射 80 端口到 Docker 主机的 10080 端口。
  • 命名卷data:其参数的效果等同于mount --bind <data_volume_path> /host/folder,即把 Docker 主机中的目录作为命名卷。
  • 网络nextcloud:把所有服务置于该网络中。

额外文件

db.env中是提供数据库连接信息的环境变量。这 3 个环境变量是 MariaDB 和 NextCloud 共用的。

MYSQL_PASSWORD=<password>
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud

web文件夹中的Dockerfile 中是构建镜像的指令。

FROM nginx:stable-alpine

COPY nginx.conf /etc/nginx/nginx.conf

此处是覆盖nginx:stable-alpine中的 nginx.conf。移除样例中的两处 HTTP 301 跳转。

-        location = /.well-known/carddav {
-            return 301 $scheme://$host:$server_port/remote.php/dav;
-        }
-        location = /.well-known/caldav {
-            return 301 $scheme://$host:$server_port/remote.php/dav;
-        }

然后把这两处 HTTP 301 跳转写入主机网络反向代理 nginx 的配置文件中。

Gitea

Gitea 的配置就比较简单,除 Gitea 本身之外只需再配置一个 MariaDB 实例。

version: "3.7"

services:
  app:
    image: gitea/gitea:latest
    environment:
      - DB_TYPE=mysql
      - DB_HOST=db
      - DB_NAME=gitea
      - DB_USER=gitea
      - DB_PASSWD=<password>
      - DOMAIN=git.example.com
      - SSH_DOMAIN=git.example.com
    restart: always
    networks:
      - gitea
    volumes:
      - app:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
       - 10030:3000
       - 22:22
    depends_on:
      - db

  db:
    image: mariadb
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=<root_password>
      - MYSQL_USER=gitea
      - MYSQL_PASSWORD=<password>
      - MYSQL_DATABASE=gitea
    networks:
      - gitea
    volumes:
      - db:/var/lib/mysql

volumes:
  db:
  app:

networks:
  gitea:

为了直接把 Docker 主机的 SSH 端口(22)映射给 Gitea 使用,需要修改 Docker 主机的sshd配置,防止端口冲突。

以 10022 端口为例,修改/etc/ssh/sshd_config,注释掉默认的 22 端口,添加 10022 端口。如果启用了防火墙,还需要添加相应规则放行 10022 端口。

#Port 22
Port 10022

之后重启sshd

$ systemctl restart sshd

nginx 反向代理

这个 nginx 实例反向代理其他应用,提供 HTTPS 加密。

.
├── docker-compose.yml
└── proxy
    ├── certs
       ├── 1_git.example.com_bundle.crt
       ├── 1_nextcloud.example.com_bundle.crt
       ├── 2_git.example.com.key
       └── 2_nextcloud.example.com.key
    ├── Dockerfile
    └── nginx.conf

2 directories, 7 files

这里通过构建自定义镜像,把相应域名的 SSL 证书和nginx.conf打包到镜像中。

FROM nginx:stable-alpine

COPY nginx.conf /etc/nginx/nginx.conf
COPY certs/1_nextcloud.example.com_bundle.crt /etc/nginx/nextcloud_bundle.crt
COPY certs/2_nextcloud.example.com.key /etc/nginx/nextcloud_private.key
COPY certs/1_git.example.com.crt /etc/nginx/git_bundle.crt
COPY certs/2_git.example.com.key /etc/nginx/git_private.key

此处的docker-compose.yaml 只定义了服务,且直接使用了 Docker 主机(host)网络。

version: '3.7'

services:
  nginx:
    image: nginx:kaguya
    build: ./proxy
    restart: always
    network_mode: host
    ports:
      - 80:80
      - 443:443

在默认的nginx.conf文件http块中为每个应用添加类似下面的server块。下面的配置以 NextCloud 为例。

server {
    listen 80;
    server_name nextcloud.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name nextcloud.example.com;
    ssl_certificate /etc/nginx/nextcloud_bundle.crt;
    ssl_certificate_key /etc/nginx/nextcloud_private.key;
    ssl_session_cache shared:SSL:20m;
    ssl_session_timeout 10m;
    ssl_protocols TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';

    client_max_body_size 10G;

    location = /.well-known/carddav {
        return 301 $scheme://$host:$server_port/remote.php/dav;
    }

    location = /.well-known/caldav {
        return 301 $scheme://$host:$server_port/remote.php/dav;
    }

    location / {
        proxy_pass http://127.0.0.1:10080;
    }
}

关键配置如下:

  • 把前面移除的几行 HTTP 301 跳转加入到server块中。
  • 设置client_max_body_size,使其与 NextCloud 应用中的web配置一致。

管理应用状态

分别进入各个应用docker-compose.yml所在的目录创建并启动相应的容器。下面以 NextCloud 为例。

$ cd nextcloud
$ docker-compose up

或者也可以手动指定docker-compose.yml

$ docker-compose -f /data0/apps/nginx/docker-compose.yml up

之后可以用ps查看容器运行状态。

$ docker-compose ps
      Name                     Command               State           Ports
----------------------------------------------------------------------------------
nextcloud_app_1     /entrypoint.sh php-fpm           Up      9000/tcp
nextcloud_cron_1    /cron.sh                         Up      9000/tcp
nextcloud_db_1      docker-entrypoint.sh --tra ...   Up      3306/tcp
nextcloud_redis_1   docker-entrypoint.sh redis ...   Up      6379/tcp
nextcloud_web_1     nginx -g daemon off;             Up      0.0.0.0:10080->80/tcp

除了up以外,还有许多其他的命令。

  • down:与up相对,停止并移除容器和网络。
  • build:重新构建指定服务的镜像。
  • start:启动指定服务。
  • stop:停止指定服务。
  • rm:移除已停止服务的容器。

执行docker-compose helpdocker-compose help <command>可以查看更多命令以及更多额外参数的用法。

至此,大功告成!

标签
作者
评论