步骤/目录： 1.准备工作 2.编写compose模板 3.运行并验证 4.gogs使用https 5.gogs备份本文首发于个人博客https://lisper517.top/index.php/archives/24/，转载请注明出处。本文的目的是使用docker、docker-compose在云服务器上运行gogs。本文实验日期为2022年7月28日。本文使用的云服务器为阿里云轻量应用服务器，系统镜像 Docker 19.03 。之前的文章中曾介绍过gogs，详见 树莓派+云服务器，gogs+frp搭建私有git 。简单来说，使用gogs可以搭建一个私有的轻量级的git服务器，一般gogs用于个人或小微企业。在docker官方仓库网页可以搜索到官方gogs镜像即 gogs/gogs ，其下载量为50M+，3天前更新。下面就使用该镜像+mysql镜像搭建gogs服务。1.准备工作和frp的部署一样，笔者准备了一台云服务器，如果只打算在局域网用gogs也可以在本地机器上运行。在服务器上准备目录，拉取镜像：mkdir -p /docker/gogs/data/gogs mkdir /docker/gogs/data/mysql mkdir -p /docker/gogs/conf/mysql docker pull gogs/gogs docker pull mysql:8.0.29这里需要简单说明一下docker-gogs的使用。容器内的22端口用于git命令相关操作，3000用于网页访问；容器内的/data目录包含了仓库、gogs的个性化设置。2.编写compose模板编写compose模板：vim /docker/gogs/docker-compose.yml写入以下内容：version: "3.9" services: gogs: image: gogs/gogs ports: - "50023:22" - "9444:3000" volumes: - /docker/gogs/data/gogs:/data depends_on: - mysql logging: driver: syslog restart: always mysql: image: mysql:8.0.29 environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: gogs MYSQL_USER: gogs MYSQL_PASSWORD: 234567 command: - mysqld - --character-set-server=utf8 - --default-authentication-plugin=mysql_native_password volumes: - /docker/gogs/data/mysql:/var/lib/mysql - /docker/gogs/conf/mysql:/etc/mysql/conf.d logging: driver: syslog restart: always根据 docker-mysql 的介绍，通过 MYSQL_USER 、 MYSQL_PASSWORD 创建的用户，对通过 MYSQL_DATABASE 创建的数据库享有所有权限。3.运行并验证cd /docker/gogs docker-compose config docker-compose up记得在云服务器的设置里打开9444、50023端口。这时可以访问 云服务器ip:9444 ，出现gogs初始页面。这里谈一点gogs的初始设置：数据库选mysql，数据库主机填mysql:3306，用户密码就是gogs@234567。基本设置里的域名，笔者使用云服务器就填服务器ip，应用URL则不影响实际、只是展示用。SMPT设置和typecho的CommentNotifier插件差不多，见 Docker树莓派实践——Typecho及其备份 。如果是私人或小微企业，可以在 禁止用户自主注册 、 启用登录访问限制 里打勾。具体配置细节还是参考 gogs/gogs镜像原址 。另外，若要后台运行docker-frpc，可使用docker-compose up -d命令。4.gogs使用httpsgogs可以使用https，这里笔者使用的是自己域名的证书，也可以用自签证书。mkdir /docker/gogs/ssl_certificate cp /docker/gogs/data/gogs/gogs/conf/app.ini /docker/gogs/data/gogs/gogs/conf/app.ini_backup vim /docker/gogs/data/gogs/gogs/conf/app.ini将自己的证书放到/docker/gogs/ssl_certificate中后，对gogs的配置文件app.ini进行以下修改：[server] ...... ...... #新增4行 PROTOCOL = https ROOT_URL = https://lisper517.top:3000 CERT_FILE = /ssl_certificate/www.lisper517.top.pem KEY_FILE = /ssl_certificate/www.lisper517.top.key修改gogs配置文件主要参考了 这篇文章 ，其中还介绍了使用nginx反向代理。然后修改compose模板，新增一个挂载：- /docker/gogs/ssl_certificate:/ssl_certificate最后删除容器、重新生成：docker stop gogs_mysql_1 docker stop gogs_gogs_1 docker container rm gogs_mysql_1 docker container rm gogs_gogs_1 docker network rm gogs_default docker volume ls docker volume prune #gogs用了一个匿名卷也删掉 cd /docker/gogs docker-compose config docker-compose up最后在浏览器访问 https://lisper517.top:9444 即可看到使用了https的gogs页面。5.gogs备份为了定时备份，同 Docker树莓派实践——Typecho及其备份 中最后一部分一样，在服务器上进行如下操作：mkdir -p /backup/gogs crontab -e加上一行：15 0 * * * /usr/bin/zip -r /backup/gogs/backup.zip /docker/gogszip命令写绝对路径是怕cron运行时的env里PATH不包括/usr/bin，实际上应该尽量都写绝对路径。用whereis zip可查找zip的路径。这样每天0点15分就会备份整个docker-gogs。最后，可以在树莓派上也设置每天1点15分拷贝服务器上的/backup/gogs/backup.zip备份文件，具体操作如下：mkdir -p /backup/gogs mkdir /backup/key #存放密钥 crontab -e加上如下内容：15 1 * * * /usr/bin/scp -i /backup/key/alicloud-backup root@lisper517.top:/backup/gogs/backup.zip /backup/gogs关于打开树莓派的crontab日志功能、设置正确的树莓派系统时间见 Docker树莓派实践——Typecho及其备份 。另外，在gogs后台可以看到有备份仓库的功能，笔者这里为了方便镜像更新后数据迁移，就直接备份文件夹了。
步骤/目录： 1.准备工作 2.运行并测试 3.其他方式本文首发于个人博客https://lisper517.top/index.php/archives/23/，转载请注明出处。本文的目的是使用docker在云服务器上运行portainer，并使得其网页UI只能使用https访问。本文实验日期为2022年7月29日。本文使用的是树莓派4B（内存8G版），系统为Pi OS 64位桌面版（2022年4月4日更新，镜像名 2022-04-04-raspios-bullseye-arm64.img ）。云服务器为阿里云的轻量应用服务器，镜像为Docker 19.03。之前的文章中曾介绍过使用https访问frp的dashboard，详见 Docker树莓派实践——frp（附dashboard开启https、frp双向验证） 。上次生成的server.crt和server.key在本文中也会用到。本文主要参考了 portainer的官方文档 。1.准备工作首先需要生成自己的.crt和.key文件。这里和 Docker树莓派实践——frp（附dashboard开启https、frp双向验证） 中一模一样，同样参考了 gofrp文档 。在树莓派上进行以下操作：mkdir /home/pi/ssl_cert #或者在其他文件夹，只要是WinSCP登录用户能访问的文件夹 cd /home/pi/ssl_cert find / -name "openssl.cnf" #查找openssl.cnfopenssl.cnf是openssl的配置文件，需要拷贝到/home/pi/ssl_cert下。在树莓派上找到了/usr/lib/ssl/openssl.cnf和/etc/ssl/openssl.cnf，但内容与 gofrp文档 有些差异。于是笔者直接复制了 gofrp文档 中的配置：nano /home/pi/ssl_cert/my-openssl.cnf写入以下内容：[ ca ] default_ca = CA_default [ CA_default ] x509_extensions = usr_cert [ req ] default_bits = 2048 default_md = sha256 default_keyfile = privkey.pem distinguished_name = req_distinguished_name attributes = req_attributes x509_extensions = v3_ca string_mask = utf8only [ req_distinguished_name ] [ req_attributes ] [ usr_cert ] basicConstraints = CA:FALSE nsComment = "OpenSSL Generated Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer [ v3_ca ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = CA:true接下来生成默认ca（CA即Certification Authority，颁发证书的机构）：openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -subj "/CN=www.lisper517.top" -days 5000 -out ca.crt其中可以把www.lisper517.top改成自己的域名，或者也可以乱写。然后使用以下命令生成证书：#第一条命令 openssl genrsa -out server.key 2048 #第二条命令 openssl req -new -sha256 -key server.key \ -subj "/C=XX/ST=DEFAULT/L=DEFAULT/O=DEFAULT/CN=server.com" \ -reqexts SAN \ -config <(cat my-openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:localhost,IP:47.98.39.236,DNS:lisper517.top")) \ -out server.csr #第三条命令 openssl x509 -req -days 365 \ -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -extfile <(printf "subjectAltName=DNS:localhost,IP:47.98.39.236,DNS:lisper517.top") \ -out server.crt注意把第二、三条命令中的IP:47.98.39.236,DNS:lisper517.top修改一下，IP改成自己云服务器的ip，DNS改成自己的域名。这些文件里需要的是server.crt和server.key，本文中不需要用到ca.crt，或者如果要关闭浏览器警告也可以留着。可以使用如下命令压缩：zip cert.zip ca.crt server.crt server.key因为用WinSCP把server.key转移到win10机器上时出现Permisson denied，所以先压缩一下才能转移。之后解压一下，还是通过WinSCP放到云服务器上（笔者放到了/docker/portainer/ssl_certificate目录下，ca.crt可以不放到云服务器上）。另外，如果想通过配置使得win10或linux接受自己颁发的证书，还要记得转移一下ca.crt，这个文件用于确认颁发证书的机构。最后记得删除临时文件夹：cd .. rm -r /home/pi/ssl_cert至此就生成并转移了需要的三个文件。2.运行并测试先进行试运行，在服务器上使用如下命令：docker pull portainer/portainer-ce docker volume create portainer_data docker run --name portainer -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data -v /docker/portainer/ssl_certificate:/ssl_certificate -p 9001:9000 -p 9002:9443 --restart always portainer/portainer-ce --sslcert /ssl_certificate/server.crt --sslkey /ssl_certificate/server.key然后在服务器的防火墙设置中打开9001、9002端口（注意不要用ufw或iptables打开），浏览器访问 http://lisper517.top:9001 和 https://lisper517.top:9002 ，两个网页都可正常打开。然后删除试运行的容器，在生产环境下去掉http端口、只用https端口：docker stop portainer docker container rm portainer docker run -d --name portainer -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data -v /docker/portainer/ssl_certificate:/ssl_certificate -p 9443:9443 --restart always portainer/portainer-ce --sslcert /ssl_certificate/server.crt --sslkey /ssl_certificate/server.key然后在服务器防火墙设置中删除9001、9002规则，打开9443端口，那么以后正常访问 https://lisper517.top:9443 即可安全访问portainer了。3.其他方式portainer/portainer已经停止更新了，目前推荐用portainer/portainer-ce。新版的portainer中加入了在网页UI中设置https的功能，在左侧选中 Setting 大标签（注意不要选到子项），有一项 SSL certificate ，在这里可以上传 .crt、.key 文件，所以其实不用挂载 /docker/portainer/ssl_certificate:/ssl_certificate ，而是可以从http访问、在这里上传两个文件、再从https端口访问。另外，上面的 Force HTTPS only 是只打开https端口，在这里笔者生产环境中 docker run 的时候就只打开了https端口，所以无所谓。另外，在 Users 里可以改密码。最后，若要关闭浏览器访问警告，可参考 Docker树莓派实践——frp（附dashboard开启https、frp双向验证） 中的 附录1.（3）访问的注意事项 。
步骤/目录： 1.方法一：独立镜像、network连接 （1）准备工作 （2）运行容器与测试 （3）收尾与总结 2.方法二：docker-compose （1）准备工作 （2）编写文件并运行项目 （3）收尾与总结 3.实战：编写易移植的typecho博客并使用https （1）准备工作 （2）编写nginx配置及compose模板 （3）运行项目并测试 （4）问题及其他 a.博客迁移 b.安装完成后无法登录 c.typecho镜像选择 d.typecho插件及其他 （5）后续 （6）为typecho添加其它功能 a.在文章页面添加最新文章 b.添加侧边栏目录索引 c.添加Sitemap d.添加网站图标 （7）使用其它主题 （8）升级版本本文首发于个人博客https://lisper517.top/index.php/archives/22/，转载请注明出处。本文的目的是使用docker、docker-compose在树莓派上运行Typecho。本文实验日期为2022年7月26日。本文使用的是树莓派4B（内存8G版），系统为Pi OS 64位桌面版（2022年4月4日更新，镜像名 2022-04-04-raspios-bullseye-arm64.img ）。之前的文章中曾介绍过Typecho，详见 树莓派搭建个人博客 。简单来说，Typecho是LNMP环境下运行的个人博客。学习Docker后，发现docker官网有typecho官方维护的镜像，于是试着用docker运行typecho。另外，这个镜像的下载量为100K+，但更新时间为3年前。官方镜像虽然只有50K下载，但最近更新比较勤（据说之前typecho五年没更新，docker镜像就更不会更新了），所以下面还是使用官方镜像。在typecho官方镜像页可以看到后缀不同的镜像，这些镜像的命名格式是：nightly-php版本号-后缀。php的版本主要有7.3、7.4、8.0，后缀主要有 空后缀、cli、fpm、apache、alpine 。本文选用的镜像是nightly-php8.0-apache。1.方法一：独立镜像、network连接主要思路是使用nightly-php8.0-apache镜像（apache和nginx作用一样，也是网页服务器），搭配docker-mysql。两个容器独立运行，使用docker network连接。需要注意typecho设置时如何填写数据库地址。在这种方式中，可以不用Dockerfile或docker-compose。（1）准备工作准备文件夹并拉取镜像：mkdir -p /test/mysql/data mkdir /test/mysql/conf docker network create -d bridge net_test docker pull mysql:8.0.29 docker pull joyqi/typecho:nightly-php8.0-apache拉取typecho镜像时比较慢，需要耐心等待。感觉白天镜像下的快一点。（2）运行容器与测试进行如下操作：docker run -itd --name mysql_test -v /test/mysql/data:/var/lib/mysql -v /test/mysql/conf:/etc/mysql/conf.d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0.29 docker run -d --name typecho_test -p 80:80 joyqi/typecho:nightly-php8.0-apache docker network connect net_test mysql_test docker network connect net_test typecho_test笔者实验时宿主机的80、3306端口没有被占用，有占用的话需自行更改。另外，typecho初始化时使用的数据库需要提前创建。进行如下操作：docker exec -it mysql_test bash mysql -uroot -p #这里连不上库可以参考 Docker树莓派实践——LNMP 中的附录 create database test; show databases; exit exit顺便也可以查看mysql容器是否运行正常。如果不想进入mysql容器，也可在mysql容器启动命令中指定MYSQL_DATABASE：docker run -itd --name mysql_test -v /test/mysql/data:/var/lib/mysql -v /test/mysql/conf:/etc/mysql/conf.d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=test mysql:8.0.29下面进入typecho页面进行第一次访问的初始化。浏览器访问 树莓派ip ，出现typecho初始化页面。跟着引导走即可，需要注意的是数据库地址要填容器名，本例中为mysql_test，容器id、容器ip（使用docker network inspect net_test查看，注意不要带/后面的数字）也可以。另外，数据库名称填刚才创建的test。设置完成后即可正常访问。（3）收尾与总结进行删除等收尾工作：docker stop mysql_test docker stop typecho_test docker container ls -a docker container rm mysql_test docker container rm typecho_test docker container ls -a docker images docker image rm mysql:8.0.29 docker image rm joyqi/typecho:nightly-php8.0-apache docker images docker network rm net_test cd / rm -r /test两个镜像如果后续需要的话，也可以不删。总结一下，这种方法使用了两个独立的镜像，typecho镜像自带了php、apache，另开了一个mysql容器。两个容器通过docker network连接，能够正常工作。这种方法较为简单易行。2.方法二：docker-compose刚才的方法两个容器需要分别配置，移植性不够好，下面使用compose进行配置。这里还是使用nightly-php8.0-apache这个镜像，搭配nginx和mysql，其中nginx用来反向代理。（1）准备工作准备文件夹并拉取镜像：mkdir -p /test/data/mysql mkdir -p /test/conf/mysql mkdir /test/conf/nginx mkdir /test/conf/typecho docker pull mysql:8.0.29 docker pull nginx:1.23.1 docker pull joyqi/typecho:nightly-php8.0-apachetypecho的个性化设置目录在容器/app/usr下，等下准备将/test/conf/typecho挂载到该目录。/app/usr目录下主要是插件、主题等自定义博客的一些配置。（2）编写文件并运行项目编写compose模板：nano /test/docker-compose.yml写入以下内容：version: "3.9" services: nginx: image: nginx:1.23.1 ports: - "443:443/tcp" - "443:443/udp" - "80:80" volumes: - /test/conf/nginx:/etc/nginx/conf.d depends_on: - typecho logging: driver: syslog restart: always mysql: image: mysql:8.0.29 environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: typecho command: - mysqld - --character-set-server=utf8mb4 - --default-authentication-plugin=mysql_native_password volumes: - /test/data/mysql:/var/lib/mysql - /test/conf/mysql:/etc/mysql/conf.d logging: driver: syslog restart: always typecho: image: joyqi/typecho:nightly-php8.0-apache volumes: - /test/conf/typecho:/app/usr logging: driver: syslog restart: always然后编写nginx配置：nano /test/conf/nginx/default.conf写入以下内容：server { listen 80; server_name localhost; location / { proxy_pass http://typecho:80; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }nginx配置参考了这个问题。然后就可以运行项目：cd /test docker-compose config docker-compose up浏览器访问 树莓派ip ，出现typecho初始化页面。同样注意数据库地址填服务容器名或容器全名，本例中为mysql或test_mysql_1，容器id、容器ip应该也可以。另外，数据库名这里填compose模板里指定创建的typecho。设置完成后即可正常访问。（3）收尾与总结进行删除等收尾工作：docker stop test_nginx_1 docker stop test_mysql_1 docker stop test_typecho_1 docker container ls -a docker container rm test_nginx_1 docker container rm test_mysql_1 docker container rm test_typecho_1 docker container ls -a docker images docker image rm nginx:1.23.1 docker image rm mysql:8.0.29 docker image rm joyqi/typecho:nightly-php8.0-apache docker images docker network rm test_default cd / rm -r /test这三个镜像如果后续需要的话，也可以不删。总结一下，这种方法使用了三个镜像并使用docker-compose管理，typecho镜像自带了php、apache，另开了mysql容器和nginx容器。这种方法加以修改，就能很方便地在更换服务器时移植博客。最后，除了上面使用的两种方法，还有很多其他方式搭建，这里就不介绍了，下面进行实战。3.实战：编写易移植的typecho博客并使用https有时候树莓派因更换或其他原因需要从头配置，想移植博客到新的环境中。使用docker-compose可以简化这个过程。下面还是使用nightly-php8.0-apache这个镜像，搭配nginx和mysql，nginx用于反向代理。另外还需要一台云服务器、一个域名、https认证证书，并且在服务器运行frps、在树莓派运行frpc。在树莓派运行frpc详见 Docker树莓派实践——frpc 或 在树莓派上设置frp穿透。在服务器运行frps详见 在树莓派上设置frp穿透 。域名、https认证证书等可自行搜索资料，或参考 树莓派搭建个人博客 （少量提及）。（1）准备工作这里将树莓派系统上所有docker项目相关的文件都放在/docker中：mkdir -p /docker/typecho/data/mysql mkdir -p /docker/typecho/conf/mysql mkdir /docker/typecho/conf/nginx mkdir /docker/typecho/conf/typecho mkdir /docker/typecho/ssl_certificate docker pull mysql:8.0.29 docker pull nginx:1.23.1 docker pull joyqi/typecho:nightly-php8.0-apache/docker/typecho/ssl_certificate用来存放https相关的证书、密钥，可以使用WinSCP等工具上传到树莓派。另外再次提醒，typecho的个性化设置目录在容器/app/usr下，等下会将/docker/typecho/conf/typecho挂载到该目录。/app/usr目录下主要是插件、主题等自定义博客的一些配置。然后修改一下frpc.ini：[common] server_addr = 云服务器ip地址 server_port = 7000 token = 你的密码 login_fail_exit = false [pi4B-blog-http-1] type = tcp local_ip = localhost local_port = 80 remote_port = 80 [pi4B-blog-https-1] type = tcp local_ip = localhost local_port = 443 remote_port = 443（2）编写nginx配置及compose模板nano /docker/typecho/conf/nginx/default.conf配置中写入以下内容（配置参考了这个问题）：server { listen 80; server_name www.lisper517.top; rewrite ^(.*)$ https://$host$1 permanent; } server { listen 443 ssl; server_name www.lisper517.top; ssl_certificate .pem文件路径; ssl_certificate_key .key文件路径; ssl_session_timeout 5m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_prefer_server_ciphers on; location / { proxy_pass http://typecho:80; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-Ip $remote_addr; proxy_set_header X-NginX-Proxy true; proxy_redirect off; } }记得把 www.lisper517.top 换成自己的域名。该配置将http请求改写成https后再处理。或者也可写入：server { listen 80; listen 443 ssl; server_name www.lisper517.top; ssl on; ssl_certificate .pem文件路径; ssl_certificate_key .key文件路径; ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location / { proxy_pass http://typecho:80; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-Ip $remote_addr; proxy_set_header X-NginX-Proxy true; #proxy_redirect off; } error_page 497 https://$host$request_uri; }同样注意把 www.lisper517.top 换成自己的域名。第二种配置的原理是http请求发到https端口后nginx会产生497错误，通过改写错误页面实现http请求返回https协议。两种配置通过不同的方法，使得即使用 http://博客地址 访问typecho博客，也会跳转到https页面。需要注意两种配置中的 proxy_pass http://typecho:80; ，这里同样是容器名。然后是compose模板：nano /docker/typecho/docker-compose.yml写入以下内容：version: "3.9" services: nginx: image: nginx:1.23.1 ports: - "443:443/tcp" - "443:443/udp" - "80:80" volumes: - /docker/typecho/conf/nginx:/etc/nginx/conf.d - /docker/typecho/ssl_certificate:/ssl_certificate depends_on: - typecho logging: driver: syslog restart: always mysql: image: mysql:8.0.29 environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: typecho command: - mysqld - --character-set-server=utf8mb4 - --default-authentication-plugin=mysql_native_password volumes: - /docker/typecho/data/mysql:/var/lib/mysql - /docker/typecho/conf/mysql:/etc/mysql/conf.d logging: driver: syslog restart: always typecho: image: joyqi/typecho:nightly-php8.0-apache volumes: - /docker/typecho/conf/typecho:/app/usr logging: driver: syslog restart: always最后运行项目：cd /docker/typecho docker-compose config docker-compose up（3）运行项目并测试在浏览器访问自己的域名（如果没有绑定域名就访问服务器ip），即可看到typecho初始页面。跟随引导对typecho进行初始化设置，在本例中，数据库地址填服务容器名或容器全名（mysql或typecho_mysql_1），数据库用户名root、密码123456，数据库名typecho（数据库需要提前创建。本例中通过MYSQL_DATABASE: typecho创建了该库）。如果在本过程中遇到问题，可以参考下一条 （4）问题及其他 。（4）问题及其他第三次提醒，typecho的个性化设置（比如主题、插件等）在typecho根目录的/usr下，joyqi/typecho:nightly-php8.0-apache容器中则是在/app/usr（/app就是typecho在容器中的根目录，不同typecho镜像可能有所差别）。另外，typecho的配置在typecho根目录下的config.inc.php中，里面存有数据库用户密码和其他设置。下面整理了一些常见的问题。a.博客迁移方法一是登入后台，在 控制台-备份 中操作，适用于文章较少时。这样生成的.dat文件只是文章、评论、注册的用户等，/app/usr目录、config.inc.php也可以备份一下。方法二是从mysql备份typecho使用的数据库，可用于文章较多时。同样，也可以备份一下/app/usr目录和config.inc.php。使用mysql导出、导入数据见 MySQL 导出数据 、 MySQL 导入数据 。另外，joyqi/typecho镜像支持通过docker-compose里的environment（或Dockerfile的ENV命令）设置一些环境变量，来达到第一次登录typecho时无需手动初始化的效果。支持的环境变量名可参考 镜像详情页 。或者也可以直接备份config.inc.php文件。最后，如果需要，可以写自动备份typecho几个目录的脚本、使用crontab等定时备份。这部分可自行探索，或参考本文最后。b.安装完成后无法登录笔者按照 3.实战：编写易移植的typecho并使用https 中的方法完成安装，可以从 www.lisper517.top 访问到博客，进行初始化后却无法登录，表现为在登录页面输入正确的用户密码后该登录页面会刷新、密码一栏清空，打开的其他页面也还是未登录状态；输入错误的用户密码后页面上方则会显示 用户名或密码错误 ，说明typecho能识别正确的用户密码，但无法跳转到登录后的页面。除了不能正常登录，评论、查看文章的功能都正常。后来笔者修改了nginx配置、去掉了https相关设置，如下：server { listen 80; server_name www.lisper517.top; location / { proxy_pass http://typecho:80; } }重启nginx容器后可以正常登录。然后把nginx配置改回使用https的配置，登录状态保持，但无法退出。这时可以正常使用备份、撰写/修改文章等用户功能。期间参考 该镜像github下的提问 ，在 config.inc.php 最后加上一行 define('__TYPECHO_SECURE__',true); ，没有解决问题。该设置大概是让typecho使用https协议。后来树莓派断电并使用其他Pi OS系统（换sd卡），大概3小时后重新使用该系统，又可以从https登录typecho后台了。用新的树莓派系统按本文配置从头装docker-typecho也没有复现问题，笔者便没有深入研究。估计可能是frps和云服务器的问题。遇到同样的问题或其他难以解决的问题，推荐重启云服务器，或使用docker-LNMP加树莓派本地运行typecho，把typecho根目录挂载到nginx的网页根目录。可参考笔者之前的 Docker树莓派实践——LNMP 和 树莓派搭建个人博客 ，或 这篇文章 。c.typecho镜像选择之前提到过，joyqi/typecho镜像命名规则为nightly-php版本号-后缀，后缀主要有 空后缀、cli、fpm、apache、alpine 。由于笔者并非CS出身，对php、nginx实际上不太了解，只会用apache镜像+nginx反向代理。有会使用fpm或其他镜像的希望在下方评论不吝赐教。d.typecho插件及其他在 树莓派搭建个人博客 中最后提到了添加备案号、修改允许上传的附件大小。这里再推荐两种插件。一是文章浏览次数统计插件，如ViewsCounter、PageViews，其使用可参考 ViewsCounter项目地址 、 PageViews使用参考 。二是可通过邮件提醒评论回复的插件，如Comment2Mail，安装后，如果用户或游客在文章下方的评论有新的回复，会自动发邮件提醒，当然该插件需要游客留下邮箱。但是需要提醒，由于typecho突然诈尸更新了1.2.0，语法大改，目前以前的插件有些用不了，Comment2Mail就是其中之一。如果用的是typecho 1.2.0之前的版本，使用Comment2Mail可参考 Comment2Mail项目地址 或 typecho官网 。若使用typecho 1.2.0，笔者在 typecho官方论坛 找到一款替代插件CommentNotifier，该插件基于Comment2Mail但按照typecho 1.2.0的新语法打造。详见 CommentNotifier项目地址 。使用CommentNotifier，以SMTP、qq邮箱为例， SMTP服务器地址 为 smtp.qq.com ， SMTP登录用户 和 SMTP邮箱地址 都填 自己的qq邮箱 ， SMTP登录密码 则是 qq邮箱SMTP授权码（登录qq邮箱，在 设置-账户 里选择开启IMAP/SMTP服务，经过验证后可得到一个授权码）， SMTP 加密模式 选 SSL加密 ， SMTP服务端口 为 465 。另外CommentNotifier自带的邮件模板比较简陋，可自行编写，或使用以下配置（参考 这篇文章 中的配置稍微修改）： 亲爱的：{Pname} ！ > 您在 《{title}》 下的评论有了新回复呢！ 您的评论： {Ptext} {author} 给您的回复： {commentText} 不战斗便无法存活，不战斗便无法生存！ ——石森章太郎 《假面骑士龙骑》 点击查看完整回复內容 本邮件为系统自动发送，请勿直接回复哦！ Copyright © {siteUrl}. All Rights Reserved. 替换插件根目录里的 theme/reply.html （Comment2Mail）或 template/default/guest.html （CommentNotifier）即可。注意笔者这里修改了一些变量名，该模板只能用于CommentNotifier。有需要的可自行更改，或到 参考文章 中复制原配置。（5）后续docker-typecho的typecho根目录就是/app，只挂载/app/usr的话会为/app创建匿名卷。为了尽量减少匿名卷的生成，把typecho的挂载中 - /docker/typecho/conf/typecho:/app/usr 改成了 - /docker/typecho/conf/typecho/app:/app 。并且为了保证博客24h运行，笔者取消了使用树莓派运行typecho+frp内网穿透，而是直接把typecho运行在了云服务器上。如果以后云服务器涨价，也许会考虑申请动态公网ip+DDNS或其他方式。另外，为了保证博客及时备份（对/docker/typecho文件夹备份），笔者使用crontab定时备份，操作如下：mkdir -p /backup/typecho crontab -e输入如下内容：0 0 * * * /usr/bin/zip -r /backup/typecho/backup.zip /docker/typechozip命令写绝对路径是怕cron运行时的env里PATH不包括/usr/bin，实际上应该尽量都写绝对路径。用whereis zip可查找zip的路径。这样每天0点0分就会备份整个docker-typecho。笔者实验时backup.zip仅16MB，解压后的文件夹则有209MB，还是很节约空间的。最后，可以在树莓派上也设置每天1点拷贝服务器上的/backup/typecho/backup.zip备份文件，具体操作如下：mkdir -p /backup/typecho mkdir /backup/key #存放密钥 crontab -e输入如下内容：0 1 * * * /usr/bin/scp -i /backup/key/alicloud-backup root@lisper517.top:/backup/typecho/backup.zip /backup/typecho另外记得打开树莓派的crontab日志功能：nano /etc/rsyslog.conf将其中的#cron.* /var/log/cron.log前的#去掉，之后重启树莓派。在笔者的云服务器上cron日志则是/var/log/cron，默认打开。最后，笔者使用时发现云服务器上的crontab日志里的时间和本地时间一致，但树莓派上则不一致，比如现在是本地时间8:00，crontab里有8点的定时任务，但查看树莓派的cron.log发现crontab的时间可能还是凌晨2点。可以查看一下树莓派系统用的是哪里的时间：date #输出时间和本地时间不一致 ls -l /etc/localtime #输出结果 ... /etc/localtime -> /usr/share/zoneinfo/Europe/London把树莓派的时区改成本地：rm /etc/localtime ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime date #输出时间和本地时间一致即可。最后重启rsyslog和cron，再看一下cron.log，时间就是本地时间：service restart rsyslog service restart cron tail -f /var/log/cron.log（6）为typecho添加其它功能可以自己修改主题的php，达到美观的功能。如果typecho容器运行时修改php，可能会卡死，重启一下容器就行了。a.在文章页面添加最新文章这个其实是上面提到的 ViewsCounter 的一种用法。如下操作：vim /docker/typecho/conf/typecho/app/usr/themes/default/sidebar.php在合适的地方加入以下内容： <?php _e('热门文章'); ?> <?php foreach (ViewsCounter_Plugin::getMostViewed() as $post): ?> <a href="<?php echo $post['permalink'] ?>"><?php echo $post['title'] ?> <?php endforeach; ?> 即可在侧边栏显示浏览数最多的文章。这个显示的个数也可在插件里设置。b.添加侧边栏目录索引参考了 这篇文章 。vim /docker/typecho/conf/typecho/app/usr/themes/default/functions.php添加3个函数：/* 文章目录开始 */ function createCatalog($obj) { //为文章标题添加锚点 global $catalog; global $catalog_count; $catalog = array(); $catalog_count = 0; $obj = preg_replace_callback('/<h([1-6])(.*?)>(.*?)<\/h\1>/i', function ($obj) { global $catalog; global $catalog_count; $catalog_count++; $catalog[] = array('text' => trim(strip_tags($obj[3])), 'depth' => $obj[1], 'count' => $catalog_count); return '<h' . $obj[1] . $obj[2] . '>' . $obj[3] . '</h' . $obj[1] . '>'; }, $obj); return $obj; } function getCatalog() { //输出文章目录容器 global $catalog; $index = ''; if ($catalog) { $index = ' ' . "\n"; $prev_depth = ''; $to_depth = 0; foreach ($catalog as $catalog_item) { $catalog_depth = $catalog_item['depth']; if ($prev_depth) { if ($catalog_depth == $prev_depth) { $index .= ' ' . "\n"; } elseif ($catalog_depth > $prev_depth) { $to_depth++; $index .= ' ' . "\n"; } else { $to_depth2 = ($to_depth > ($prev_depth - $catalog_depth)) ? ($prev_depth - $catalog_depth) : $to_depth; if ($to_depth2) { for ($i = 0; $i < $to_depth2; $i++) { $index .= ' ' . "\n" . ' ' . "\n"; $to_depth--; } } $index .= ' '; } } $index .= ' ' . $catalog_item['text'] . ''; $prev_depth = $catalog_item['depth']; } for ($i = 0; $i <= $to_depth; $i++) { $index .= ' ' . "\n" . ' ' . "\n"; } $index = ' ' . ' ' . "\n" . $index . ' ' . "\n"; } echo $index; } function themeInit($archive) { if ($archive->is('single')) { $archive->content = createCatalog($archive->content); } } /* 文章目录结束 */然后在页面内添加相应的代码，比如也加在侧边栏：vim /docker/typecho/conf/typecho/app/usr/themes/default/sidebar.php添加一行即可：<? php getCatalog(); ?>这样就实现了在侧边栏悬浮固定的目录索引，还为标题添加了超链，会前端的可以再精修一下。笔者使用的是如下配置： <?php getCatalog();?> 固定索引的大小、位置，添加了滑动条。c.添加Sitemap目的是生成一个 你的博客域名/sitemap.xml ，对博客网页进行归类整理，便于谷歌、bing、百度的爬虫访问。你可以自己写一个爬虫，或者使用typecho插件。笔者找到了一款适用typecho 1.2的插件： Sitemap-For-Typecho ，使用方法如下（参考 插件作者文章一 、 插件作者文章二 ）：首先设置typecho的伪静态，在typecho的后台， 设置-永久链接 里，启用 地址重写功能 。如果出现红字 重写功能检测失败，请检查你的服务器设置 ，则勾选下面的 如果你仍想启用此功能，请勾选这里 。如果还是不行，就放弃从typecho设置伪静态，而是在nginx中添加配置： if (!-e $request_filename) { rewrite ^(.*)$ /index.php$1 last; }如果用apache则添加：RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [L,E=PATH_INFO:$1]设置完伪静态，就可以添加sitemap插件。在 github 的 <> Code - Download ZIP 中下载zip文件，解压后把里面的Sitemap文件夹放到typecho的 app/usr/plugins 目录下，并且用 chmod 777 -R Sitemap 把该插件目录改成777（不改的话，启用插件时会出现一个好像以 fwrite() 开头的错误报告）。重启docker-typecho，在后台管理插件处打开该插件设置，推荐把 自动更新Sitemap 打开（这样会在每次有新文章时更新Sitemap，但是发布新文章时可能会卡一下，因为要先运行这个插件）；也可以在Sitemap更新时自动推送到baidu，但是笔者觉得没有这个必要，因为从后台数据来看，根本没有人从baidu跳转到本博客。如果不打开 自动更新Sitemap ，那么每次发布新文章后都要到该插件后台管理处手动更新Sitemap。刚安装好插件后可以手动更新一下，访问 你的博客域名/sitemap.xml 看看效果。另外可以在robots.txt中收录一下，添加一行： Sitemap: 你的域名/sitemap.xml 。d.添加网站图标根据 官方文档 以及其它网络资料，首先用一些网页工具生成 favicon.ico 文件，上传到下面的文件夹：/usr/themes/你的主题名然后修改下面的文件：/usr/themes/你的主题名/header.php写入三行： 刷新页面即可。（7）使用其它主题typecho自带的主题其实已经足够使用，笔者一向认为内容大于形式，但是看久了总会审美疲劳。很多人为typecho开发了各种主题，本文介绍一下 Joe ，搭建后的效果参考 这个演示站点 ，比较花里胡哨。到 Joe 的项目地址下载zip包，放到 /docker/typecho/conf/typecho/app/usr/themes 目录，然后就可以使用了。需要注意的是，由于采用了Joe的那一套php页面，所以需要再次手动添加目录索引、备案号。（8）升级版本根据 官方文档 和 这篇文章 ，首先备份并删除服务器上的这四个目录与文件：/admin/ /var/ /index.php /install.php然后下载最新版本的typecho（ 官方下载地址 ），把同样的四个目录与文件上传到服务器，最后刷新页面、登录后台即可升级。但升级后有些插件可能需要重新设置。
步骤/目录： 1.准备工作 2.编写frpc.ini和compose模板 3.运行并验证 附录： 1.让frps的dashboard使用https （1）tls介绍 （2）生成证书并运行 （3）访问的注意事项 2.frp双向验证 （1）介绍 （2）准备工作 （3）修改配置并运行本文首发于个人博客https://lisper517.top/index.php/archives/21/，转载请注明出处。本文的目的是使用docker、docker-compose在云服务器上运行frps、在树莓派上运行frpc。本文实验日期为2022年7月27日。本文使用的是树莓派4B（内存8G版），系统为Pi OS 64位桌面版（2022年4月4日更新，镜像名 2022-04-04-raspios-bullseye-arm64.img ）。之前的文章中曾介绍过frp，详见 在树莓派上设置frp穿透 。简单来说，frp的运行需要一台有公网ip的机器和另一台机器（比如没有公网ip的内网机器）；frp的作用是把另一台机器的端口映射到公网机器，使得从公网机器也能访问到另一台机器的端口（即内网穿透）。在docker官方仓库网页可以搜索到很多frp镜像，这里frpc选用的是snowdreamtech/frpc镜像，其下载量为10M+，两个月前更新。frps镜像也是该作者的。1.准备工作首先准备云服务器。笔者这里使用的是阿里云，镜像为Docker 19.03。在云服务器上进行如下操作：docker pull snowdreamtech/frps mkdir -p /docker/frps/conf mkdir /docker/frps/ssl_certificate #存放tls相关证书 #安装docker-compose。使用env命令发现PATH里没有/usr/local/bin，就放在/usr/bin里了 curl -L https://github.com/docker/compose/releases/download/v1.29.2/docker-compose-`uname -s`-`uname -m` > /usr/bin/docker-compose chmod +x /usr/bin/docker-compose docker-compose -v #编辑frps配置 vim /docker/frps/conf/frps.ini笔者使用的初始frps配置如下：[common] bind_port = 7000 #frpc上也要有相同的token值 token = 你的密码 #frps统计板 dashboard_port = 7500 dashboard_user = wuhuqifei dashboard_pwd = wuhuqifei #kcp据说是多消耗15%流量、降低tcp延迟。端口可和bind_port相同 kcp_bind_port = 7000使用初始配置时的运行命令：docker run -d --name frps -v /docker/frps/conf/frps.ini:/etc/frp/frps.ini --network host --restart=always snowdreamtech/frps为了让dashboard使用tls协议加密，笔者将frps.ini修改为：[common] bind_port = 7000 #frpc上也要有相同的token值 token = 你的密码 #frps统计板 dashboard_port = 7500 dashboard_user = wuhuqifei dashboard_pwd = wuhuqifei #kcp据说是多消耗15%流量、降低tcp延迟。端口可和bind_port相同 kcp_bind_port = 7000 #dashboard使用tls的相关配置 dashboard_tls_mode = true dashboard_tls_cert_file = /ssl_certificate/server.crt dashboard_tls_key_file = /ssl_certificate/server.key关于.crt、.key、ca.crt文件的介绍与自己生成的方法见 附录1. ，或者参考 gofrp文档 。然后更改命令，加上挂载证书存放目录的部分：docker run -d --name frps -v /docker/frps/conf/frps.ini:/etc/frp/frps.ini -v /docker/frps/ssl_certificate:/ssl_certificate --network host --restart=always snowdreamtech/frps记得把.crt（这里不是指ca.crt）、.key文件放到云服务器的/docker/frps/ssl_certificate目录，至此云服务器设置完毕。接下来对树莓派进行操作。在树莓派上准备目录，拉取镜像：mkdir -p /docker/frpc/conf docker pull snowdreamtech/frpc2.编写frpc.ini和compose模板nano /docker/frpc/conf/frpc.ini写入以下内容：[common] server_addr = 云服务器ip地址 server_port = 7000 token = 你的密码 login_fail_exit = false [pi4B-ssh-1] type = tcp local_ip = localhost local_port = 22 remote_port = 50022其中pi4B-ssh-1这一项映射是为了之后验证。另外由于这项映射要使用到服务器的50022端口，还需要登录到云服务器主页、在防火墙设置里打开该端口（勿用ufw或iptables打开）。再为树莓派编写compose模板：nano /docker/frpc/docker-compose.yml写入以下内容：version: "3.9" services: frpc: image: snowdreamtech/frpc network_mode: host volumes: - /docker/frpc/conf/frpc.ini:/etc/frp/frpc.ini logging: driver: syslog restart: always3.运行并验证cd /docker/frpc docker-compose config docker-compose up #输出结果 frpc_1 | 2022/07/27 03:23:33 [I] [service.go:301] [50829e63df489f3f] login to server success, get run id [50829e63df489f3f], server udp port [0] frpc_1 | 2022/07/27 03:23:33 [I] [proxy_manager.go:144] [50829e63df489f3f] proxy added: [pi4B-ssh-1] frpc_1 | 2022/07/27 03:23:33 [I] [control.go:180] [50829e63df489f3f] [pi4B-ssh-1] start proxy success这时可以登录到frps的dashboard（云服务器ip:7500/），输入设置的用户名密码，在 Proxies-TCP 中，可看到一项名为 pi4B-ssh-1 的服务，说明成功设置了frp。想进一步实验，可以试着用putty连接到 云服务器ip:50022 ，即通过公网ip连接到树莓派ssh。再次提醒，记得在云服务器的设置里打开50022端口。最后，若要后台运行docker-frpc，可使用docker-compose up -d命令。最后提醒一下，关于ufw的使用。用frp把树莓派端口映射到服务器，或者使用docker把容器端口映射到宿主机树莓派时，即使没有用ufw打开端口，也能从树莓派本地以外访问到这些端口。所以为了安全起见，请谨慎设置端口映射。另外，关于dashboard开启https的进一步内容，可阅读 附录1. 。附录：1.让frps的dashboard使用https在云服务器上开启frps的dashboard是挺简单的，但是由于云服务器有公网ip，暴露dashboard端口其实不太安全。笔者之后了解到，frps的dashboard可以通过一些设置来使用tls协议、走https，于是查找了一些资料后配置成功。（1）tls介绍SSL/TLS是一种密码通信框架，其中SSL出现较早，TLS出现较晚，TLS可以说是SSL的后续版本。在 Docker树莓派实践——Typecho 一文中，nginx配置其实就出现了SSL/TLS：ssl_protocols TLSv1 TLSv1.1 TLSv1.2;实际上，HTTPS、SMTP服务都可由SSL/TLS承载。SSL/TLS的具体原理就不介绍了。从使用上来说，使用TLS，可以身份验证、信息加密，使得信息传输更加安全，这样访问服务器端口、使用frp或其他软件时就更加保险。（2）生成证书并运行这里主要参考了 gofrp文档 。笔者使用的阿里云，虽然为自己的域名申请了一个免费证书，但该证书下载下来后，其apache版本的crt文件分为chain和public，nginx版则只有pem文件，而且都没有ca.crt。实际上，dashboard使用的证书最好还是自己生成。在树莓派上使用如下命令：mkdir ~/ssl_cert cd ~/ssl_cert find / -name "openssl.cnf" #查找openssl.cnfopenssl.cnf是openssl的配置文件，需要拷贝到~/ssl_cert下。在树莓派上找到了/usr/lib/ssl/openssl.cnf和/etc/ssl/openssl.cnf，但内容与 gofrp文档 有些差异。于是笔者直接复制了 gofrp文档 中的配置：nano ~/ssl_cert/my-openssl.cnf写入以下内容：[ ca ] default_ca = CA_default [ CA_default ] x509_extensions = usr_cert [ req ] default_bits = 2048 default_md = sha256 default_keyfile = privkey.pem distinguished_name = req_distinguished_name attributes = req_attributes x509_extensions = v3_ca string_mask = utf8only [ req_distinguished_name ] [ req_attributes ] [ usr_cert ] basicConstraints = CA:FALSE nsComment = "OpenSSL Generated Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer [ v3_ca ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = CA:true接下来生成默认ca（CA即Certification Authority，颁发证书的机构）：openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -subj "/CN=www.lisper517.top" -days 5000 -out ca.crt其中可以把www.lisper517.top改成自己的域名，或者也可以乱写。然后使用以下命令生成证书：#第一条命令 openssl genrsa -out server.key 2048 #第二条命令 openssl req -new -sha256 -key server.key \ -subj "/C=XX/ST=DEFAULT/L=DEFAULT/O=DEFAULT/CN=server.com" \ -reqexts SAN \ -config <(cat my-openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:localhost,IP:47.98.39.236,DNS:lisper517.top")) \ -out server.csr #第三条命令 openssl x509 -req -days 365 \ -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -extfile <(printf "subjectAltName=DNS:localhost,IP:47.98.39.236,DNS:lisper517.top") \ -out server.crt注意把第二、三条命令中的IP:47.98.39.236,DNS:lisper517.top修改一下，IP改成自己云服务器的ip，DNS改成自己的域名。这些文件里需要的是server.crt和server.key，而ca.crt在 附录2. 中需要用到，并且可以用于关闭浏览器警告。可以使用如下命令压缩并转移：zip cert.zip ca.crt server.crt server.key cd .. mv ssl_cert /home/pi #或者其他用户，只要能用WinSCP登录。如果设置了root可登录，也可以不mv因为用WinSCP把server.key转移到win10机器上时出现Permisson denied，所以先压缩一下才能转移。之后解压一下，还是通过WinSCP放到云服务器上（笔者放到了/docker/frps/ssl_certificate目录下，ca.crt可以不放到云服务器上）。另外，如果想通过配置使得win10或linux接受自己颁发的证书，还要记得转移一下ca.crt，这个文件用于确认颁发证书的机构。最后记得删除临时文件夹：rm -r /home/pi/ssl_cert至此就生成并转移了需要的三个文件。然后在浏览器访问 https://域名:7500 ，就会发现dashboard已经使用了https。但是由于证书是自己生成的，浏览器会发出警告。关于警告的解决方法见下一节 （3）访问的注意事项 。（3）访问的注意事项对自己生成的证书，浏览器会认为证书颁发机构不可信任。笔者一般是强行访问，但也可以通过设置来添加到可信任的颁发机构。在linux上，笔者并未实操，但据说将ca.crt放到/usr/local/share/ca-certificates目录下，然后update-ca-certificates即可。在win10上，可按win+r键，输入mmc并运行，在 文件-添加/删除管理单元 中，在左侧单击 证书 ，点击 添加 ，然后可以为本用户或计算机所有用户添加，点击完成、确定，然后点击 证书 - 当前用户 （或 证书 - 计算机用户） 的下拉菜单，点击 受信任的根证书颁发机构-证书 ，在中间会出现许多颁发机构。在中间空白处右键单击，然后选择 所有任务-导入 ，最后把自己的ca.crt导入即可。最后关闭，将设置存入 控制台1.msc 即可。之后重启电脑，就不会出现安全警告了。2.frp双向验证（1）介绍详见 gofrp文档 。简单来说，frp可以使用use_encryption或STCP等功能让传输过程安全，但无法判断对方身份是否合法。而使用tls、双向验证，不仅能加密，还能验证身份。（2）准备工作刚才介绍dashboard使用https时提到过，ca.crt代表了证书颁发机构，如果server.crt和server.key是ca签发或在ca的信任链中，则server.crt和server.key是合法的。也可以理解为，ca.crt为server.crt和server.key提供合法性，只要验证了ca.crt颁发了这两个文件，那么这两个文件就可以信任。frp双向验证的双向，指的就是frpc和frps对彼此的身份都要验证。在前文中笔者演示了自己颁发证书，生成了ca.crt、server.crt和server.key，这里我们称为server_ca.crt、server.crt和server.key；使用类似的方法生成client_ca.crt、client.crt和client.key，那么frpc端上存放server_ca.crt、client.crt和client.key，其中server_ca.crt用来对frps端进行验证，client.crt和client.key则提供给frps端以使frps端对frpc端进行验证；对称地，frps端上存放client_ca.crt、server.crt和server.key，其中client_ca.crt用来对frpc端进行验证，server.crt和server.key则提供给frpc端以使frpc端对frps端进行验证。前文已经生成了server_ca.crt、server.crt和server.key，虽然是给dashboard用的，但也能用于双向验证。接下来只演示生成client_ca.crt、client.crt和client.key。之前的步骤见前文，这里只有最后三条命令有改动：#第一条命令 openssl genrsa -out client.key 2048 #第二条命令 openssl req -new -sha256 -key client.key \ -subj "/C=XX/ST=DEFAULT/L=DEFAULT/O=DEFAULT/CN=client.com" \ -reqexts SAN \ -config <(cat my-openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:lisper517.top,DNS:www.lisper517.top")) \ -out client.csr #第三条命令 openssl x509 -req -days 365 \ -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -extfile <(printf "subjectAltName=DNS:lisper517.top,DNS:www.lisper517.top") \ -out client.crt同样注意把DNS:lisper517.top,DNS:www.lisper517.top改成自己的域名，或者乱写也可以。同样，转移client.key的时候提示Permisson denied，也用zip压缩后转移。另外，转移ca.crt的时候务必分清，最好是像下文中一样改个名字。最后，记得删除临时文件夹。（3）修改配置并运行vim /docker/frps/conf/frps.inifrps配置新增四行：[common] bind_port = 7000 token = 你的密码 dashboard_port = 7500 dashboard_user = wuhuqifei dashboard_pwd = wuhuqifei kcp_bind_port = 7000 dashboard_tls_mode = true dashboard_tls_cert_file = /ssl_certificate/server.crt dashboard_tls_key_file = /ssl_certificate/server.key #新增内容 tls_only = true tls_cert_file = /ssl_certificate/server.crt tls_key_file = /ssl_certificate/server.key tls_trusted_ca_file = /ssl_certificate/client_ca.crt然后把三个文件放到/docker/frps/ssl_certificate文件夹中。注意这里的ca.crt是生成client.crt、client.key时生成的，即client_ca.crt。最后重启docker-frps，服务器端配置完毕：docker restart frps然后对树莓派进行如下修改：mkdir /docker/frpc/ssl_certificate nano /docker/frpc/conf/frpc.ini同样增加四行内容：tls_enable = true tls_cert_file = /ssl_certificate/client.crt tls_key_file = /ssl_certificate/client.key tls_trusted_ca_file = /ssl_certificate/server_ca.crt同样把三个文件放到文件夹中，同样注意这里的ca.crt是生成server.crt、server.key时生成的，即server_ca.crt。然后挂载/docker/frpc/ssl_certificate：nano /docker/frpc/docker-compose.yml在volumes下新增：- /docker/frpc/ssl_certificate:/ssl_certificate由于新增了挂载，需要删除容器、重新生成：docker stop frpc_frpc_1 docker container rm frpc_frpc_1 cd /docker/frpc docker-compose up查看一下日志，是否成功连接。成功连接后使用-d后台运行：docker stop frpc_frpc_1 docker-compose up -d
步骤/目录： 1.下载、烧录Pi OS最新镜像 2.安装docker并调试；备份系统镜像 3.方法一：假docker法 （1）拉取并运行ubuntu镜像 （2）安装vim、nginx （3）安装php并配置nginx （4）安装mariadb （5）收尾及总结 4.方法二：胖容器法 （1）创建环境，运行容器 （2）测试容器 （3）收尾及总结 5.方法三：瘦容器法 （1）创建环境，配置nginx （2）创建网络，开启php、nginx容器并测试 （3）运行mysql容器并测试 （4）准备环境并编写compose模板，运行并测试 （5）收尾及总结 附录 1.docker连接mysql时显示"Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock'" （1）docker run /bin/bash （2）Dockerfile CMD （3）docker run未挂载或映射端口 （4）mysql配置问题 2.使用docker-compose创建mysql容器时的初始化 （1）挂载配置文件和数据库 （2）进入服务容器修改 （3）挂载到/docker-entrypoint-initdb.d （4）Dockerfile COPY到/docker-entrypoint-initdb.d （5）--init-file选项本文首发于个人博客https://lisper517.top/index.php/archives/20/，转载请注明出处。本文的目的是使用docker、docker-compose在树莓派上搭建LNMP环境。本文实验日期为2022年7月25日。本文使用的是树莓派4B（内存8G版），系统为Pi OS 64位桌面版（2022年4月4日更新，镜像名 2022-04-04-raspios-bullseye-arm64.img ），装上系统后仅做了一些基础配置（安装了ufw、watchdog，设置了ssh密钥，apt update了一下）。LNMP环境指Linux、Nginx、Mysql、Php，LNMP常用于提供动态网页服务。一般来说使用docker有三种方式搭建LNMP环境：一是使用linux容器（如centos或ubuntu），docker run -it ubuntu bash，类似不使用docker时进行安装；二是使用Dockerfile，FROM ubuntu开始构建一个镜像，安装NMP；三是单独生成N、M、P的镜像，使用docker-compose或其他方法管理。方法一属于下策，但是最简单易行；方法二可称为fat container，只需要一个镜像、一个容器，缺点是不利于NMP的版本变更，但在一些情况下也可以使用；方法三为thin container，需要三个容器跑一个功能，容器多但是利于版本更迭、调整环境，而且使用docker-compose可以非常方便地管理多个容器。这里三种方法都演示一下。另外，如果需要非docker搭建LNMP，可以参考之前的文章 树莓派搭建个人博客 。1.下载、烧录Pi OS最新镜像这里涉及的操作之前的文章已经介绍过了，详见 树莓派4B初始化 。本次实验使用的是树莓派4B。在系统镜像下载网页下载最新的Pi OS 64位桌面版（下载文件名为2022-04-04-raspios-bullseye-arm64.img.xz，解压为.img文件后大小为3.95GB）。注意2022年4月后Pi OS取消了默认的pi用户，但可以自己创建：在boot下新建userconf文件，写入以下内容：pi:$6$8WzhrUbnvxbJdS5n$p5acHHXaB02qI1eCJrSH4lSUOsXx9WnTvbNm2T9h9d/OmnFuf0qXGTAYF3GK5pqFONn7LiA4Khn4AHXcYG72D/这样就手动创建了pi用户，密码还是raspberry。装上系统后仅进行了apt-get upgrade，安装并设置了ufw、watchdog，调整了ssh设置。2.安装docker并调试；备份系统镜像安装、调试docker和docker-compose的过程在前面已经介绍，这里不再赘述（见 Docker入门（一） ）。记得换docker源。这里使用RaspberryBackup脚本备份系统，详细介绍见 树莓派备份-RaspberryBackup详解 。3.方法一：假docker法再次提醒，这种方法只是学术探讨，实际环境中不管是生成还是测试都不要使用。（1）拉取并运行ubuntu镜像docker pull ubuntu docker run -itd -p 80:80 --name ubuntu_LNMP --restart always ubuntu /bin/bash #未映射宿主机文件夹 #输出容器id为 4a5 docker container ls -a docker exec -it 4a5 /bin/bash进入ubuntu命令行后，查看一下系统信息：cat /proc/version cat /etc/os-release（2）安装vim、nginx安装vim、nginx（未换apt源），开启nginx服务：apt-get update apt-get install -y vim nginx service --status-all #使用systemctl还要安装systemd service nginx start这时访问 树莓派ip ，即可看到nginx初始页。再测试一下：cd /var/www/html vim test.html写入文件内容如下： nginx test wuhu,qifei 浏览器访问 树莓派ip/test.html ，出现对应页面。（3）安装php并配置nginxapt-get install -y php-fpm php-mysql service --status-all #这里安装的是php8.1-fpm这里只配置nginx，php配置不变：vim /etc/nginx/sites-available/default配置文件共修改两处。首先将： index index.html index.htm index.nginx-debian.html;改为： index index.php index.html index.htm index.nginx-debian.html;其次在这个server的{}最后加上： location ~ .*\.php(\/.*)*$ { fastcgi_pass unix:/run/php/php8.1-fpm.sock; #注意自己的php-fpm版本 include fastcgi_params; set $path_info ""; set $real_script_name $fastcgi_script_name; if ($fastcgi_script_name ~ "^(.+?\.php)(/.+)$") { set $real_script_name $1; set $path_info $2; } fastcgi_param SCRIPT_FILENAME $document_root$real_script_name; fastcgi_param SCRIPT_NAME $real_script_name; fastcgi_param PATH_INFO $path_info; }接下来开始测试：nginx -t service nginx restart vim /var/www/html/myweb.php文件内容为：<?php phpinfo(); ?>然后在浏览器输入 树莓派ip/myweb.php ，就会显示经典的php信息页面。（4）安装mariadbmariadb和mysql基本一样。apt-get install -y mariadb-server service --status-all service mariadb start mysql_secure_installation这里进行一些基础设置，设置完root密码后全选y即可，但最好还是自己看一下。之后进入mariadb看一下：mysql -uroot -p show databases; exit后续可以自己建库、表试试。另外，这里就不测试php和mariadb互联了。（5）收尾及总结使用以下命令删除镜像和容器：docker stop ubuntu_LNMP docker container rm ubuntu_LNMP docker image rm ubuntu总结一下，这种方法总体上比较简单，和不用docker时差不多。个人网页也许可以用这种方式，但还是不推荐，毕竟学习了docker还是应该整点狠活。4.方法二：胖容器法此处使用的镜像是这个镜像，这里是该项目的github页面。为了方便，下文直接使用该镜像。感兴趣的可以看看该项目的Dockerfile。如之前所述，这种方法的优点是只有一个镜像，管理方便；但是缺点是镜像内的几个软件更迭版本比较费时，不能做到数秒内切换版本。如果镜像搭建时各软件使用的都是已知比较稳定、交互时bug少的版本，那这种方法可能也可以用于生产环境。（1）创建环境，运行容器mkdir -p /test/www mkdir -p /test/mysql/data docker pull 2233466866/lnmp:1.13 docker images docker run -itd --name=lnmp_test -v /test/www:/www -v /test/mysql/data:/data/mysql --privileged=true -p 80:80 2233466866/lnmp:1.13这里使用的镜像使用了CentOS7 + Nginx + Node.js + MySQL + php-^7.4 + php5.6.40搭建。由于该镜像的平台架构是linux/amd64，而笔者用的树莓派是linux/arm64/v8，所以该镜像实际上是跑不起来的。后面测试容器、收尾部分并未实测。如需要挂载各种配置文件，可去项目原址了解详情。（2）测试容器docker exec -it lnmp_test /bin/bash cat /etc/os-release cat /proc/version nginx -t ps aux | grep nginx ps aux | grep mysql ps aux | grep php7 ps aux | grep php5之后在/test/www下创建一些测试网页，如前文一样，在浏览器访问 树莓派ip 进行测试。（3）收尾及总结使用以下命令删除镜像和容器：rm -r /test docker stop lnmp_test docker container rm lnmp_test docker image rm 2233466866/lnmp:1.135.方法三：瘦容器法如前所述，这种方法是最推荐的，各个镜像换版本很方便，能做到数秒内切换版本；由于镜像较多，管理稍微麻烦一点，但可通过docker compose进行方便快捷的管理。下面的内容，不涉及docker-compose的部分主要参考了这篇文章和这个网页；涉及docker-compose的部分参考了这篇文章。最后需要说明，前三步都是测试环境、配置。赶时间可直接看第四、五步。（1）创建环境，配置nginxmkdir -p /test/nginx/conf.d mkdir /test/www nano /test/nginx/conf.d/default.confdefault.conf就是nginx的配置文件，其中写入以下内容：server { listen 80; server_name localhost; location / { root /var/www/html; index index.php index.html index.htm; } location ~ \.php$ { fastcgi_pass php_test:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /var/www/html/$fastcgi_script_name; include fastcgi_params; } }其中fastcgi_pass php_test:9000;需要特别注意。这里的php_test是容器名，后面跑不起来的话可以试试换成容器id、容器在网络中的ip；9000是php提供服务的端口。（2）创建网络，开启php、nginx容器并测试创建网络，开启php-fpm容器、加入网络：docker network create -d bridge net_test docker pull php:8.1-fpm docker stop portainer #之前这个容器也使用了9000端口，暂时停一下 docker run -d --name php_test -v /test/www:/var/www/html -p 9000:9000 --network net_test php:8.1-fpm如果docker run时忘记加上--network net_test参数，可以使用如下命令补救：docker network connect net_test php_test开启nginx容器：docker pull nginx docker run -d --name nginx_test -v /test/www:/var/www/html -v /test/nginx/conf.d:/etc/nginx/conf.d -p 80:80 -p 443:443 --network net_test nginx443端口用于HTTPS协议，这里也可以不加。进行测试：nano /test/www/index.html nano /test/www/index.php分别写入： nginx test wuhu,qifei 和：<?php phpinfo(); ?>在浏览器输入 树莓派ip/index.html 或 树莓派ip/index.php ，出现对应界面，测试成功。另外，为了让php使用mysql，还需要在php容器中安装pdo、pdo_mysql扩展，php镜像就有提供安装该扩展的快捷操作（这里可先不运行）：docker exec -it php_test bash docker-php-ext-install pdo pdo_mysql exit docker restart php_test（3）运行mysql容器并测试先创建文件夹：mkdir -p /test/mysql/data mkdir /test/mysql/conf然后运行容器并进入：docker pull mysql docker run -itd --name mysql_test -v /test/mysql/data:/var/lib/mysql -v /test/mysql/conf:/etc/mysql/conf.d -p 3306:3306 --network net_test -e MYSQL_ROOT_PASSWORD=123456 mysql这里拉取到的是mysql8.0.29，如果是老版本的话容器放配置、数据、日志的位置可能不一样。另外其中-e MYSQL_ROOT_PASSWORD=123456可设置mysql的root密码。最后关于mysql的配置，本例中，在宿主机的/test/mysql/conf下任何以.cnf结尾的文件都会覆盖原有配置。为了测试mysql，可以在mysql中建一张表。操作如下：nano /test/mysql/test.sql其内容如下：CREATE TABLE student( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', name VARCHAR(20) NOT NULL COMMENT '姓名', age INT NOT NULL COMMENT '年龄' )ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '学生数据表'; INSERT INTO student(name, age) VALUES('alice', 18); INSERT INTO student(name, age) VALUES('bob', 28);下面拷贝/test/mysql/test.sql到容器里，并进入mysql容器进行配置：docker cp /test/mysql/test.sql mysql_test:/tmp/ docker exec -it mysql_test bash mysql -uroot -p #输入密码 CREATE DATABASE test; SHOW DATABASES; USE test; SOURCE /tmp/test.sql; SELECT * FROM student; exit exit成功显示导入的表。这里也不测试php和mysql的互联了。前面三步只是初步测试，下面开始动真格。（4）准备环境并编写compose模板，运行并测试前面运行的容器没有使用docker-compose管理、仅用于测试，现在运行的话php与mysql容器无法互联。下面删除测试容器、镜像与目录，另起炉灶：cd / docker container ls -a docker stop mysql_test docker stop nginx_test docker stop php_test docker container rm mysql_test docker container rm nginx_test docker container rm php_test docker container ls -a docker network ls docker network rm net_test docker network ls docker images docker image rm mysql docker image rm nginx docker image rm php:8.1-fpm docker images rm -r /test然后创建测试目录：mkdir -p /test/docker_compose/LNMP/nginx/conf.d touch /test/docker_compose/LNMP/nginx/conf.d/default.conf mkdir /test/docker_compose/LNMP/www mkdir -p /test/docker_compose/LNMP/mysql/data mkdir /test/docker_compose/LNMP/mysql/conf mkdir /test/docker_compose/LNMP/php下面准备拉取镜像、创建php配置目录（但本次不会挂载）：docker pull nginx:1.23.1 docker pull mysql:8.0.29 docker pull php:8.1-fpm docker images mkdir /test/docker_compose/LNMP/php/confphp镜像需要安装扩展、新增配置，所以第一次运行时不要挂载宿主机目录到php服务容器的配置目录。先运行一次，后面再用docker cp拷贝，并挂载。接下来创建Dockerfile。其中nginx需要修改配置文件，php需要安装pdo、pdo_mysql扩展，mysql需要建表、进行一些基础设置，所以nginx不用Dockerfile、只需修改一下/test/docker_compose/LNMP/nginx/conf.d/default.conf，而给php、mysql写Dockerfile即可：nano /test/docker_compose/LNMP/nginx/conf.d/default.confnginx的配置文件中写入以下内容：server { listen 80; server_name localhost; location / { root /var/www/html; index index.php index.html index.htm; } location ~ \.php$ { fastcgi_pass php_test:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /var/www/html/$fastcgi_script_name; include fastcgi_params; } }注意fastcgi_pass php_test:9000;，这里写的是服务容器名（php_test），写实际容器名（lnmp_php_test_1）也可以，据说还可以写项目内容器的ip或容器短id，笔者没有实验，毕竟这样通用性可能不强。然后写一下php、mysql的Dockerfile：mkdir -p /test/docker_compose/LNMP/dockerfiles/php mkdir /test/docker_compose/LNMP/dockerfiles/mysql nano /test/docker_compose/LNMP/dockerfiles/php/Dockerfile nano /test/docker_compose/LNMP/dockerfiles/mysql/Dockerfilephp的Dockerfile写入以下内容：FROM php:8.1-fpm RUN docker-php-ext-install mysqli pdo pdo_mysql后续的docker-compose up过程中显示3个php扩展都装了（这个版本的php自带PDO）。另外需要提醒一下，如果挂载了本地php配置目录到php_test服务容器，那么一定确保里面有这三个扩展的配置，否则装了和没装一样。严格按照本文流程是没问题的。mysql的Dockerfile写入以下内容：FROM mysql:8.0.29 COPY ./test.sql /docker-entrypoint-initdb.d/ ENTRYPOINT ["docker-entrypoint.sh"] CMD ["mysqld","--character-set-server=utf8mb4","--default-authentication-plugin=mysql_native_password"]docker-mysql的/docker-entrypoint-initdb.d目录是其初始化目录，可放入.sh、.sql和.sql.gz文件来进行一些初始化操作，比如导入一些表。该目录里文件按字母顺序依次执行（.sql.gz压缩文件应该会解压成.sql后执行），若不指定库名，则要导入的库名默认为MYSQL_DATABASE指定的库。关于mysql初始化的内容详见附录2.。再写入mysql构建时需要的test.sql：nano /test/docker_compose/LNMP/dockerfiles/mysql/test.sql文件内容如下：CREATE DATABASE test; USE test; CREATE TABLE student( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL, age INT NOT NULL ); INSERT INTO student(name, age) VALUES('alice', 18); INSERT INTO student(name, age) VALUES('bob', 28);至此就写好了Dockerfile及其需要的文件。下面创建compose模板文件：nano /test/docker_compose/LNMP/docker-compose.ymlcompose模板文件内容如下，其编写参考了这篇文章和这个项目。：version: "3.9" services: nginx_test: image: nginx:1.23.1 ports: - "443:443/tcp" - "443:443/udp" - "80:80" volumes: - /test/docker_compose/LNMP/www:/var/www/html - /test/docker_compose/LNMP/nginx/conf.d:/etc/nginx/conf.d depends_on: - php_test logging: driver: syslog mysql_test: build: context: ./dockerfiles/mysql/ dockerfile: Dockerfile environment: MYSQL_ROOT_PASSWORD: 123456 volumes: - /test/docker_compose/LNMP/mysql/data:/var/lib/mysql - /test/docker_compose/LNMP/mysql/conf:/etc/mysql/conf.d logging: driver: syslog php_test: build: context: ./dockerfiles/php/ dockerfile: Dockerfile volumes: - /test/docker_compose/LNMP/www:/var/www/html #- /test/docker_compose/LNMP/php/conf:/usr/local/etc/php #因为php镜像要安装3个扩展、会新增配置，所以第一次运行先不挂载php配置目录 logging: driver: syslog再次提醒，第一次运行时，就算/test/docker_compose/LNMP/php/conf是空的，也不要挂载到php服务容器。像mysql容器，由于它在容器运行后才会运行初始化脚本、生成数据，所以第一次运行就把宿主机上空的数据目录/test/docker_compose/LNMP/mysql/data挂载了。但php在生成镜像时就会安装扩展、生成配置，如果第一次就挂载空的php配置目录，就会使用默认配置，结果找不到新扩展的相关配置、无法使用这些扩展。然后验证一下格式，运行项目：cd /test/docker_compose/LNMP/ docker-compose config docker-compose up输出结果：………… ………… Creating lnmp_mysql_test_1 ... done Creating lnmp_phpl_test_1 ... done Creating lnmp_nginx_test_1 ... done ………… …………现在另开一个ssh连接，测试一下LNMP项目。首先测试一下nginx和php：nano /test/docker_compose/LNMP/www/index.html nano /test/docker_compose/LNMP/www/index.php分别写入： nginx test wuhu,qifei 和：<?php phpinfo(); ?>浏览器访问 树莓派ip 或 树莓派ip/index.php，显示php信息界面；访问 树莓派ip/index.html ，显示wuhu,qifei。接下来试试php与mysql是否正常协作。先看看mysql容器：docker exec -it lnmp_mysql_test_1 bash mysql -uroot -p show databases; use test; show tables; select * from student; exit exit正确显示初始数据库。为了测试LNMP环境（主要是php和mysql间的通信），新建一张网页LNMP_test.php：nano /test/docker_compose/LNMP/www/LNMP_test.php其内容如下：<?php $dsn = 'mysql:dbname=test;host=mysql_test'; $user = 'root'; $password = '123456'; try { $dbh = new PDO($dsn, $user, $password); $sql = 'SELECT * FROM student WHERE id=?'; $sth = $dbh->prepare($sql); $sth->execute([2]); $result = $sth->fetch(PDO::FETCH_ASSOC); var_dump($result); } catch (PDOException $e) { echo 'Error: ' . $e->getMessage(); } ?>特别注意$dsn = 'mysql:dbname=test;host=mysql_test';中的host，这里写的是服务容器的名字（在compose模板里的名字）。和nginx配置一样，这里也可以写成lnmp_mysql_test_1。然后在浏览器访问 树莓派ip/LNMP_test.php ，网页成功显示student表里的内容。最后，如果想在宿主机保存php配置，可进行如下操作：rmdir /test/docker_compose/LNMP/php/conf docker cp lnmp_php_test_1:/usr/local/etc/php /test/docker_compose/LNMP/php mv /test/docker_compose/LNMP/php/php /test/docker_compose/LNMP/php/conf nano /test/docker_compose/LNMP/docker-compose.yml再将compose模板里注释掉的挂载命令前的 # 去掉即可。然后删掉php_test容器，重新docker-compose up：docker stop lnmp_php_test_1 docker container rm lnmp_php_test_1 cd /test/docker_compose/LNMP/ docker-compose config docker-compose up这里没有改动镜像、只是改了启动时的选项（相当于docker run增加了一个-v参数），所以可以不删镜像。按理来说也可以不删容器。这样就能同步php配置了。此时的镜像已经可以试用于生产环境，但是最好在compose模板里，每个服务容器下面再加上restart: always。（5）收尾及总结使用如下命令彻底清理测试环境：docker stop lnmp_mysql_test_1 docker stop lnmp_nginx_test_1 docker stop lnmp_php_test_1 docker container ls -a docker container rm lnmp_mysql_test_1 docker container rm lnmp_nginx_test_1 docker container rm lnmp_php_test_1 docker container ls -a docker images docker image rm lnmp_mysql_test docker image rm lnmp_php_test_1 docker image rm mysql:8.0.29 docker image rm nginx:1.23.1 docker image rm php:8.1-fpm docker images docker network ls docker network rm lnmp_default docker network ls cd / rm -r /test总结一下，整个流程还是比较复杂的，其中有一些小坑。但是一旦配置完成，使用docker-compose就非常容易管理，分布式、Swarm mode等也手到擒来，可以说难度大、收益大。附录：1.docker连接mysql时显示"Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock'"出现这个错误常常是因为mysql进程在容器里出现问题。（1）docker run /bin/bash在docker run末尾加/bin/bahs或bash，会导致mysql进程不运行。进行如下操作：mkdir -p /test/mysql/data mkdir /test/mysql/conf docker pull mysql docker run -itd --name mysql_test -v /test/mysql/data:/var/lib/mysql -v /test/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456 mysql /bin/bash docker exec -it mysql_test /bin/bash mysql -uroot -p #输入密码后输出结果 ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2) mysql -h localhost -uroot -p #输出结果同上 mysql -h localhost -P 3306 -uroot -p #输出结果同上这是因为在docker run的末尾多加了一个/bin/bash。下面去掉/bin/bash：docker run -itd --name mysql_test -v /test/mysql/data:/var/lib/mysql -v /test/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456 mysql docker exec -it mysql_test /bin/bash mysql -uroot -p #第一次输入密码后输出结果 ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES) mysql -uroot -p第二次输入密码后才成功登录。如果想一次登录成功，需要加上-p参数：docker run -itd --name mysql_test -v /test/mysql/data:/var/lib/mysql -v /test/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 mysql若要一次登录成功，需要同时加上-v、-p参数。详见后面 （3）docker run未挂载或映射端口 。（2）Dockerfile CMD该情况来自于一个问答翻译网站，原问答地址多半在stackoverflow或github。如果要用mysql官方镜像生成自己的镜像，并且在Dockerfile中写CMD tail -f /dev/null这样无关的命令，就会覆盖之前镜像的CMD，而正常镜像的CMD应该负责开启容器进程。比如去看官方的mysql镜像，最后是这样写的：ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 3306 33060 CMD ["mysqld"]所以要写CMD也应写成以下形式：ENTRYPOINT ["docker-entrypoint.sh"] CMD ["mysqld"] #mysql镜像或：CMD ["nginx", "-g", "daemon off;"] #nginx镜像另外，对于mysql镜像的自动导入表功能（/docker-entrypoint-initdb.d目录），据说也要用entrypoint脚本时才行。所以如果希望自己构建的mysql镜像附带初始表，可以这样写Dockerfile：FROM mysql:8.0.29 ENV MYSQL_ROOT_PASSWORD=123456 MYSQL_DATABASE=test COPY ./test.sql /docker-entrypoint-initdb.d/ ENTRYPOINT ["docker-entrypoint.sh"] CMD ["mysqld"]但是就算Dockerfile写的没错，之后docker run也可能出问题，详见下一条。（3）docker run未挂载或映射端口现有这样一个场景：从官方mysql镜像创建自己的镜像，要求该镜像启动时自动挂载一张表。进行如下操作：mkdir -p /test/mysql/data #数据文件夹，暂时先不挂载 mkdir /test/mysql/conf #配置文件夹，也暂时先不挂载 mkdir -p /test/dockerfiles/mysql nano /test/dockerfiles/mysql/test.sql nano /test/dockerfiles/mysql/Dockerfiletest.sql中写入以下内容：CREATE TABLE student( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', name VARCHAR(20) NOT NULL COMMENT '姓名', age INT NOT NULL COMMENT '年龄' )ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '学生数据表'; INSERT INTO student(name, age) VALUES('alice', 18); INSERT INTO student(name, age) VALUES('bob', 28);Dockerfile中写入以下内容：FROM mysql:8.0.29 ENV MYSQL_ROOT_PASSWORD=123456 MYSQL_DATABASE=test COPY ./test.sql /docker-entrypoint-initdb.d/ ENTRYPOINT ["docker-entrypoint.sh"] CMD ["mysqld"]看起来很完美，没有踩任何坑。Dockerfile通过/docker-entrypoint-initdb.d/目录会自动写入test.sql到test数据库中（默认写入MYSQL_DATABASE指定的库中），并且最后确保了这个自制镜像通过entrypoint脚本启动。接下来构建镜像、运行容器并测试一下：cd /test/dockerfiles/mysql docker build -t mysql:my . docker run -itd --name mysql_test mysql:my docker exec -it mysql_test /bin/bash mysql -uroot -p=123456 #输出结果 mysql: [Warning] Using a password on the command line interface can be insecure. ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)又是同样的错误，但这里是由于docker run没有加-v和-p参数。解决方法有二，一是修改mysql登录命令：mysql -h localhost -P 3306 -uroot -p #第一次输入密码后输出结果 ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES) mysql -h localhost -P 3306 -uroot -p #第二次输入密码后才成功登录二是修改docker run（加挂载和端口映射）：docker run -itd --name mysql_test -v /test/mysql/data:/var/lib/mysql -v /test/mysql/conf:/etc/mysql/conf.d -p 3306:3306 mysql:my docker exec -it mysql_test /bin/bash mysql -uroot -p #一次就成功登录在方法二中，如果单独加-v或单独加-p参数，都不能彻底解决问题。如果单独加-v，共使用2次mysql -h localhost -P 3306 -uroot -p才进入mysql命令行；单独加-p时则用了4次。不过这几种情况下，只要成功进入mysql命令行，会发现test库的创建、test.sql的导入都没有问题。（4）mysql配置问题配置问题就比较复杂了，可以先试试mysql -h localhost -P 3306 -uroot -p；或者如果之前运行过mysql的容器没删、这次运行出问题，可以拿以前的容器先顶上；最后如果没办法，可以用docker cp拷贝配置文件到宿主机，逐行比对自己的配置和mysql的默认配置。mysql的登录问题告一段落，下面介绍篇mysql容器的初始化。2.使用docker-compose创建mysql容器时的初始化在使用docker-compose创建容器服务时，有时需要对初始的mysql做一些设置或存入一些初始数据。下面介绍5种方法。（1）挂载配置文件和数据库在服务容器中，一般mysql的数据文件在/var/lib/mysql，配置文件在/etc/mysql/conf.d。注意/etc/mysql/conf.d目录下本来是没有配置文件的，但如果在里面出现了以.cnf结尾的文件，就会覆盖mysql原有的配置文件。mysql原有的配置文件为/etc/my.cnf（linux）。所以可以在docker run时使用-v参数挂载宿主机路径或数据卷到这些服务容器路径，就能轻松转移以前的mysql数据库配置和数据。（2）进入服务容器修改使用docker exec -it 容器名 bash或docker exec -it 容器名 /bin/bash进入容器服务，再登录到mysql命令行，就能用mysql命令行自带的命令对设置或数据进行修改，比如用source导入数据。（3）挂载到/docker-entrypoint-initdb.d如果希望在mysql镜像实例化时自动导入数据，可以试试/docker-entrypoint-initdb.d目录。根据docker-mysql官网的介绍，mysql镜像实例化时会以字母顺序执行/docker-entrypoint-initdb.d目录下的.sh、.sql、.sql.gz文件，其中sql.gz压缩文件应该会解压成.sql后执行。使用该目录有两种方法，其一是挂载宿主机路径到该目录。下面进行实操，先创建测试目录、compose模板文件：mkdir -p /test/mysql/data mkdir /test/mysql/dockerfile mkdir /test/mysql/init.d nano /test/mysql/docker-compose.yml nano /test/mysql/dockerfile/Dockerfile nano /test/mysql/init.d/test.sqlcompose模板文件中写入以下内容：version: "3.9" services: mysql_test: build: context: ./dockerfile/ dockerfile: Dockerfile environment: MYSQL_ROOT_PASSWORD: 123456 volumes: - /test/mysql/data:/var/lib/mysql - /test/mysql/init.d:/docker-entrypoint-initdb.d:ro logging: driver: syslog然后是Dockerfile：FROM mysql:8.0.29 ENTRYPOINT ["docker-entrypoint.sh"] CMD ["mysqld","--character-set-server=utf8mb4","--default-authentication-plugin=mysql_native_password"]接着是test.sql：CREATE DATABASE test; USE test; CREATE TABLE student( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL, age INT NOT NULL ); INSERT INTO student(name, age) VALUES('alice', 18); INSERT INTO student(name, age) VALUES('bob', 28);接下来开启项目，进行测试：cd /test/mysql docker-compose config docker-compose up #另开一个ssh界面 docker exec -it mysql_mysql_test_1 bash mysql -uroot -p use test; select * from student; exit exit成功显示导入的数据。另外提醒一下，本例中在compose模板中mysql容器使用了syslog，正常执行/docker-entrypoint-initdb.d目录下文件时输出的log会在执行命令前后留下数行空行，非常显眼。总结一下，这种方法的关键就是在容器服务的volumes下带上一个挂载：- 宿主机目录:/docker-entrypoint-initdb.d:ro，在宿主机目录中存放初始化文件。测试完毕，进行收尾：docker stop mysql_mysql_test_1 docker container ls -a docker container rm mysql_mysql_test_1 docker container ls -a docker network ls docker network rm mysql_default docker network ls cd / rm -r /test（4）Dockerfile COPY到/docker-entrypoint-initdb.d上面的方法是挂载，第二种方法就是用Dockerfile的COPY指令，在构建镜像的时候就把初始化文件放进去。下面进行实操，同样先创建测试目录、compose模板文件：mkdir -p /test/mysql/data mkdir /test/mysql/dockerfile nano /test/mysql/docker-compose.yml nano /test/mysql/dockerfile/Dockerfile nano /test/mysql/dockerfile/test.sqlcompose模板文件中写入以下内容：version: "3.9" services: mysql_test: build: context: ./dockerfile/ dockerfile: Dockerfile environment: MYSQL_ROOT_PASSWORD: 123456 volumes: - /test/mysql/data:/var/lib/mysql logging: driver: syslog然后是Dockerfile：FROM mysql:8.0.29 COPY ./test.sql /docker-entrypoint-initdb.d/ ENTRYPOINT ["docker-entrypoint.sh"] CMD ["mysqld","--character-set-server=utf8mb4","--default-authentication-plugin=mysql_native_password"]接着是test.sql：CREATE DATABASE test; USE test; CREATE TABLE student( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL, age INT NOT NULL ); INSERT INTO student(name, age) VALUES('alice', 18); INSERT INTO student(name, age) VALUES('bob', 28);接下来开启项目，进行测试：cd /test/mysql docker-compose config docker-compose up #另开一个ssh界面 docker exec -it mysql_mysql_test_1 bash mysql -uroot -p use test; select * from student; exit exit成功显示导入的数据。和上一种方法相比，这种方法的关键是在Dockerfile中COPY ./init.sql /docker-entrypoint-initdb.d/，而不是挂载初始化目录或文件到容器服务。和上一种方法一样，本例中在compose模板中mysql容器使用了syslog，正常执行/docker-entrypoint-initdb.d目录下文件时输出的log会在执行命令前后留下数行空行，非常显眼。测试完毕，进行收尾：docker stop mysql_mysql_test_1 docker container ls -a docker container rm mysql_mysql_test_1 docker container ls -a docker network ls docker network rm mysql_default docker network ls cd / rm -r /test（5）--init-file选项前面两种方法的目标都是把初始化文件放到容器服务的/docker-entrypoint-initdb.d中。本方法则利用mysql数据库启动时，mysqld命令的--init-file达到同样的效果。不需要额外挂载，但同样需要使用Dockerfile的COPY指令。下面进行实操，还是先创建测试目录、compose模板文件：mkdir -p /test/mysql/data mkdir /test/mysql/dockerfile nano /test/mysql/docker-compose.yml nano /test/mysql/dockerfile/Dockerfile nano /test/mysql/dockerfile/test.sqlcompose模板文件中写入以下内容：version: "3.9" services: mysql_test: build: context: ./dockerfile/ dockerfile: Dockerfile environment: MYSQL_ROOT_PASSWORD: 123456 volumes: - /test/mysql/data:/var/lib/mysql logging: driver: syslog然后是Dockerfile：FROM mysql:8.0.29 COPY ./test.sql /init.sql ENTRYPOINT ["docker-entrypoint.sh"] CMD ["mysqld","--character-set-server=utf8mb4","--default-authentication-plugin=mysql_native_password","--init-file=/init.sql"]接着是test.sql：CREATE DATABASE test; USE test; CREATE TABLE student( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL, age INT NOT NULL ); INSERT INTO student(name, age) VALUES('alice', 18); INSERT INTO student(name, age) VALUES('bob', 28);接下来开启项目，进行测试：cd /test/mysql docker-compose config docker-compose up #另开一个ssh界面 docker exec -it mysql_mysql_test_1 bash mysql -uroot -p use test; select * from student; exit exit成功显示导入的数据。和前两种方法相比，这种方法的关键是在Dockerfile中COPY ./init.sql /init.sql，没有使用/docker-entrypoint-initdb.d目录。另外提醒一下，据说--init-file参数提供的.sql文件，书写要求是不能有注释、一行写一条命令（估计意思是不能一行中写多条命令，但可以把一条命令拆成几行写，如果有条件还是尽量一行写一条命令）。测试完毕，进行收尾：docker stop mysql_mysql_test_1 docker container ls -a docker container rm mysql_mysql_test_1 docker container ls -a docker network ls docker network rm mysql_default docker network ls cd / rm -r /test
本文较长，属于系统性总结，可作为使用手册查阅。本文主要介绍了docker-compose的使用。步骤/目录： 1.Docker Compose （1）安装compose与卸载 （2）compose的使用例一 （3）docker-compose命令 a.build b.config c.down d.exec e.images f.kill g.logs h.pause i.port j.ps k.pull l.push m.restart n.rm o.run p.scale q.start r.stop s.top t.unpause u.up v.help w.version （4）docker-compose.yml模板文件 a.build b.cap_add，cap_drop c.command d.configs e.cgroup_parent f.container_name g.deploy h.devices i.depends_on j.dns k.dns_search l.tmpfs m.env_file n.environment o.expose p.external_links q.extra_hosts r.healthcheck s.image t.label u.logging v.network_mode w.networks x.pid y.ports z.secrets a1.security_opt b1.stop_signal c1.sysctls d1.ulimits e1.volumes f1.其他指令 g1.读取变量值 （5）docker-compose实战 2.其他进阶内容 3.使用docker-compose更新容器 4.docker使用心得 （1）docker网络-单机 （2）docker与ufw （3）docker网络-多机本文首发于个人博客https://lisper517.top/index.php/archives/19/，转载请注明出处。本文的目的是对docker-compose相关内容做总结归纳，使用本文从零学习docker将有一定难度。本文主要总结自菜鸟教程和Docker——从入门到实践和一些其他零散资料，有不明白的地方请参考原文。本文写作日期为2022年2月-7月。本文涉及的主要是docker的进阶内容，初学者可以先只看Docker Compose，再使用一段时间docker后，有需要再学习其他的。1.Docker Compose更详细的说明见Docker Compose，这里只做一个快速手册式的总结。在以下compose的讲解中，服务（service）指一个镜像实例化的一个或多个容器（为了便于理解，笔者一般都称服务容器），项目（project）指为了完成某种功能而相互协作的多个容器构成的整体。Docker Compose是一种管理多个容器的工具，只需要配置docker-compose.yml（compose模板）文件就能打包多个容器为一个项目（project）。YAML格式（.yml文件）类似html、json格式，是一种组织内容表达的格式，规则类似json的键值对，形式上不同而已。最后，原来的compose是用python写的，后来的compose v2则是官方用Go重写的。（1）安装compose与卸载compose支持linux、mac、windows平台；可通过pip或二进制包下载两种方式安装。对于mac、windows平台，其docker桌面版自带compose；对于linux平台，x86_64架构的机器，可使用以下方式安装（该命令会自动选择合适的平台）：curl -L https://github.com/docker/compose/releases/download/v1.27.4/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose #国内用户可替换为以下命令加快下载，笔者实验的时候这个网页好像已经失效了 curl -L https://download.fastgit.org/docker/compose/releases/download/v1.27.4/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose #至此就算安装完成对于linux平台，arm架构的机器（如树莓派），可使用pip安装：pip install -U docker-compose另外还可以使用以下命令进行bash命令补全（估计是一个包的命令不全，需要下面这个包补充）：curl -L https://raw.githubusercontent.com/docker/compose/1.27.4/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose chmod +x /etc/bash_completion.d/docker-compose笔者使用该命令时出现curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL错误，于是去掉了域名前的https协议才成功下载：curl -L raw.githubusercontent.com/docker/compose/1.27.4/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose但是，笔者实验的时候这两个网页已经404了（github上的compose项目网页不太安定），于是就通过pip安装了。有需要的可以自行找下载页。安装完成后，可运行docker-compose --version测试是否成功安装。需要卸载的话，如果使用二进制包安装，删除包即可：rm /usr/local/bin/docker-compose rm /etc/bash_completion.d/docker-compose如果通过pip安装，就用pip卸载：pip uninstall docker-compose（2）compose的使用例一最常见需要多个容器协作的场景应该就是web网页了。这里使用python搭建一个镜像，需要达到的目标是每访问一次5000端口，网页显示的访问次数就+1。本项目一共使用了2个容器。进行如下操作：mkdir -p /test/compose_test nano /test/compose_test/app.py写入以下内容：from flask import Flask from redis import Redis app = Flask(__name__) redis = Redis(host='redis', port=6379) @app.route('/') def hello(): count = redis.incr('hits') return 'Hello World! 该页面已被访问 {} 次。\n'.format(count) if __name__ == "__main__": app.run(host="0.0.0.0", debug=True)如果不会python，上面这个文件的内容不算重要。然后编写Dockerfile：nano /test/compose_test/Dockerfile写入以下内容FROM python:3.6-alpine ADD . /code #不知道为什么这里用ADD，和前面的原则冲突 WORKDIR /code RUN pip install redis flask CMD ["python", "app.py"]这里的alpine是指极简版（不仅是python），据说学界对是否应使用alpine版素有争论。最后编写docker-compose.yml文件：nano /test/compose_test/docker-compose.yml写入以下内容：version: '3' services: web: build: . ports: - "5000:5000" redis: image: "redis:alpine"上面的compose模板看不懂，可先继续往下读，后面再回头看这里。最后运行compose项目：cd /test/compose_test docker-compose up运行完后，访问 树莓派ip:5000 ，每次刷新后访问次数都会+1。运行完后进行删除：^C docker container ls -a docker container rm compose_test_web_1 #注意这里容器的命名方式 docker container rm compose_test_redis_1 docker container ls -a docker images docker image rm compose_test_web:latest docker image rm redis:alpine docker image rm python:3.6-alpine docker images docker network ls docker network rm compose_test_default docker network ls rm -r /test从本示例可以看出compose的使用方式。一般来说，compose和使用Dockerfile时相比，只多了编写docker-compose.yml文件这一步，并且把docker build换成了docker-compose up。docker-compose up也是十分强大的docker-compose命令。（3）docker-compose命令原文内容较长，这里也是仅做总结。详情可阅读原网页或官方网页。如同刚才接触的docker-compose up，docker-compose还有许多其他命令。命令的基本格式为：docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]使用docker-compose help [COMMAND]可以查看某个命令的详细介绍。首先介绍格式中的[options]。常见的命令选项如下：-f, --file FILE 指定使用的 Compose 模板文件，默认为 docker-compose.yml，可以多次指定。 -p, --project-name NAME 指定项目名称，默认将使用所在目录名称作为项目名。 --verbose 输出更多调试信息。 -v, --version 打印版本并退出。然后介绍[COMMAND]部分。常用的命令有：a.build格式为docker-compose build [options] [SERVICE...]，用途是构建或重新构建项目里的服务容器。它的选项包括：--force-rm 删除构建过程中的临时容器。 --no-cache 构建镜像过程中不使用 cache（这将加长构建过程）。 --pull 始终尝试通过 pull 来获取更新版本的镜像。b.config验证 Compose 文件格式是否正确，若正确则显示配置，若格式错误显示错误原因。c.down停止up命令所启动的所有服务容器，并移除网络。d.exec进入指定的服务容器。这个命令可用docker exec代替。e.images列出 Compose 文件中包含的镜像。Compose 文件有时可能不包含镜像（image），全用build。f.kill格式为docker-compose kill [options] [SERVICE...]，用途是发送 SIGKILL 信号来强制停止服务容器。支持通过-s参数来指定发送的信号，例如通过如下指令发送 SIGINT 信号：docker-compose kill -s SIGINTg.logs格式为docker-compose logs [options] [SERVICE...]，可查看服务容器的输出。该命令在调试问题的时候十分有用。默认情况下，docker-compose 将对不同的服务输出使用不同的颜色来区分。可以通过 --no-color 来关闭颜色。h.pause格式为docker-compose pause [SERVICE...]，用途是暂停一个服务容器。相对的是unpause。i.port格式为docker-compose port [options] SERVICE PRIVATE_PORT，可打印某个服务容器端口所映射的公共端口。选项：--protocol=proto 指定端口协议，tcp（默认值）或者 udp。 --index=index 如果同一服务存在多个容器，指定命令对象容器的序号（默认为 1）。j.ps格式为docker-compose ps [options] [SERVICE...]，可列出项目中目前的所有容器。选项：-q 只打印容器的 ID 信息。k.pull格式为docker-compose pull [options] [SERVICE...]，可拉取服务依赖的镜像。选项：--ignore-pull-failures 忽略拉取镜像过程中的错误。l.push推送服务依赖的镜像到 Docker 镜像仓库。m.restart格式为docker-compose restart [options] [SERVICE...]，可重启项目中的服务。选项：-t, --timeout TIMEOUT 指定重启前停止容器的超时（默认为 10 秒）。n.rm格式为docker-compose rm [options] [SERVICE...]，用途是删除所有（停止状态的）服务容器。推荐先执行 docker-compose stop 命令来停止容器。选项：-f, --force 强制直接删除，包括非停止状态的容器。不推荐使用该选项。 -v 同时删除容器所挂载的数据卷。o.run格式为docker-compose run [options] [-p PORT...] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]，用途是在指定服务上执行一个命令，例如：docker-compose run ubuntu ping docker.com将会启动一个 ubuntu 服务容器，并执行 ping docker.com 命令。默认情况下，如果存在关联，则所有关联的服务将会自动被启动，除非这些服务已在运行中。该命令类似启动容器后运行指定的命令，相关卷、链接等等都将会按照配置自动创建。两个不同点：给定命令将会覆盖原有的自动运行命令；不会自动创建端口，以避免冲突。如果不希望自动启动关联的容器，可以使用 --no-deps 选项，例如：docker-compose run --no-deps web python manage.py shell将不会启动 web 容器所关联的其它容器。选项：-d 后台运行容器。 --name NAME 为容器指定一个名字。 --entrypoint CMD 覆盖默认的容器启动指令。 -e KEY=VAL 设置环境变量值，可多次使用选项来设置多个环境变量。 -u, --user="" 指定运行容器的用户名或者 uid。 --no-deps 不自动启动关联的服务容器。 --rm 运行命令后自动删除容器，d 模式下将忽略。 -p, --publish=[] 映射容器端口到本地主机。 --service-ports 配置服务端口并映射到本地主机。 -T 不分配伪 tty，意味着依赖 tty 的指令将无法运行。p.scale格式为docker-compose scale [options] [SERVICE=NUM...]，用于设置指定服务运行的容器个数。通过 service=num 的参数来设置数量。例如：docker-compose scale web=3 db=2将启动 3 个容器运行 web 服务，2 个容器运行 db 服务。一般的，当指定数目多于该服务当前实际运行容器，将新创建并启动容器；反之，将停止容器。选项：-t, --timeout TIMEOUT 停止容器时候的超时（默认为 10 秒）。q.start格式为docker-compose start [SERVICE...]，可启动已经存在的服务容器。r.stop格式为docker-compose stop [options] [SERVICE...]，可停止已经处于运行状态的容器，但不删除它。通过docker-compose start可以再次启动这些容器。选项：-t, --timeout TIMEOUT 停止容器时候的超时（默认为 10 秒）。s.top查看各个服务容器内运行的进程。t.unpause格式为docker-compose unpause [SERVICE...]，可恢复处于暂停状态中的服务。相对的是pause。u.up格式为docker-compose up [options] [SERVICE...]。该命令十分强大，它将尝试自动完成包括构建镜像，（重新）创建服务，启动服务，并关联服务相关容器的一系列操作。可以说，大部分时候都可以直接通过该命令来启动一个项目。链接的服务都将会被自动启动，除非已经处于运行状态。默认情况，docker-compose up启动的容器都在前台，控制台将会同时打印所有容器的输出信息，方便进行调试。前台模式下，通过Ctrl-C停止命令时，所有容器将会停止。如果使用docker-compose up -d，将会在后台启动并运行所有的容器。一般推荐生产环境下使用该选项。默认情况，如果服务容器已经存在，docker-compose up将会尝试停止容器，然后重新创建（保持使用 volumes 挂载的卷），以保证新启动的服务匹配 docker-compose.yml 文件的最新内容。如果用户不希望容器被停止并重新创建，可以使用docker-compose up --no-recreate。这样将只会启动处于停止状态的容器，而忽略已经运行的服务。如果用户只想重新部署某个服务，可以使用docker-compose up --no-deps -d <SERVICE_NAME>来重新创建服务并后台停止旧服务、启动新服务，这种方式不会影响到其所依赖的服务。选项：-d 在后台运行服务容器。 --no-color 不使用颜色来区分不同的服务的控制台输出。 --no-deps 不启动服务所链接的容器。 --force-recreate 强制重新创建容器，不能与 --no-recreate 同时使用。 --no-recreate 如果容器已经存在了，则不重新创建，不能与 --force-recreate 同时使用。 --no-build 不自动构建缺失的服务镜像。 -t, --timeout TIMEOUT 停止容器时候的超时（默认为 10 秒）。v.help格式为docker-compose help [COMMAND]，可输出命令的详细信息。w.version格式为docker-compose version，可打印版本信息。（4）docker-compose.yml模板文件模板文件是docker-compose的核心，其使用的YAML格式有些类似json的键值对套娃。下面是一份模板：version: "3" services: webapp: image: examples/web ports: - "80:80" volumes: - "/data"每个服务容器都必须通过image: 指定镜像，或用build指定构建方式。另外，version: "3"指定compose模板文件使用的docker-compose版本，不同的版本对应不同版本的docker引擎，可向下兼容，所以一般可以写高点。下面介绍各指令的用法。a.build用于指定 Dockerfile 所在文件夹的路径（可以是绝对路径，或者相对 docker-compose.yml 文件的路径，为了部署方便一般都写相对路径）。Compose 将会利用它自动构建这个镜像，然后使用这个镜像。示例：version: '3' services: webapp: build: ./dirbuild下可使用context指定Dockerfile的路径，使用dockerfile指令指定 Dockerfile 文件名，使用arg指令指定构建镜像时的变量：version: '3' services: webapp: build: context: ./dir dockerfile: Dockerfile-alternate args: buildno: 1environment则类似ENV，但是放在build外面，后面也有介绍。还可使用cache_from指定构建镜像的缓存：build: context: . cache_from: - alpine:latest - corp/web_app:3.14b.cap_add，cap_drop指定容器的内核能力（capacity）分配，比如让容器拥有所有能力可以指定为：cap_add: - ALL去掉 NET_ADMIN 能力可以指定为：cap_drop: - NET_ADMINc.command覆盖容器启动后默认执行的命令（覆盖镜像的CMD），例如：command: echo "hello world"d.configs仅用于 Swarm mode（虫群模式）。关于Swarm mode可查看此网页（新手可以不看）。e.cgroup_parent指定父 cgroup 组，意味着将继承该组的资源限制。例如创建一个 cgroup 组名称为 cgroups_1：cgroup_parent: cgroups_1f.container_name指定容器名称。不指定时默认将会使用项目名称_服务容器名称_序号这样的格式。示例：container_name: docker-web-container注意，指定容器名称后，该服务将无法进行扩展（scale），因为 Docker 不允许多个容器具有相同的名称。g.deploy也是仅用于 Swarm mode（虫群模式）。关于Swarm mode可查看此网页（新手可以不看）。h.devices指定设备映射关系。示例：devices: - "/dev/ttyUSB1:/dev/ttyUSB0"i.depends_on解决容器的依赖、启动先后的问题。以下例子中会先启动 redis db 再启动 web：version: '3' services: web: build: . depends_on: - db - redis redis: image: redis db: image: postgres注意，上例中 web 服务不会等待 redis db 「完全启动」之后才启动。j.dns自定义 DNS 服务器。可以是一个值或一个列表：dns: 8.8.8.8或：dns: - 8.8.8.8 - 114.114.114.114k.dns_search配置 DNS 搜索域。同样可以是一个值或一个列表：dns_search: example.com或：dns_search: - domain1.example.com - domain2.example.coml.tmpfs挂载一个 tmpfs 文件系统到容器。示例：tmpfs: /run或：tmpfs: - /run - /tmpm.env_file从文件中获取环境变量，可以为单独的文件路径或列表。如果通过docker-compose -f FILE方式来指定 Compose 模板文件，则env_file中变量的路径会基于模板文件路径。如果有变量名称与environment指令冲突，则按照惯例，以位置靠后的为准。格式示例：env_file: .env或：env_file: - ./common.env - ./apps/web.env - /opt/secrets.env环境变量文件中每一行必须符合格式，支持 # 开头的注释行，如：# common.env: Set development environment PROG_ENV=developmentn.environment设置环境变量，可以使用数组或字典两种格式。只给定名称的变量会自动获取运行 Compose 主机上对应变量的值，可以用来防止泄露不必要的数据。示例：environment: RACK_ENV: development SESSION_SECRET:或：environment: - RACK_ENV=development - SESSION_SECRET如果变量名称或者值中用到 true|false，yes|no 等表达 布尔 含义的词汇，最好放到引号里，避免 YAML 自动解析某些内容为对应的布尔语义。这些特定词汇，包括：y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF或者也可以全部用引号。如果运行时又指定（docker run -e），那么应该是docker run时指定的会覆盖environment指定的（未实验）。o.expose暴露端口，但不映射到宿主机、只被连接的服务访问，仅可以指定内部端口为参数。示例：expose: - "3000" - "8000"p.external_links注意：不建议使用该指令。链接到 docker-compose.yml 外部的容器，甚至并非 Compose 管理的外部容器。示例：external_links: - redis_1 - project_db_1:mysql - project_db_1:postgresqlq.extra_hosts类似docker run中的--add-host参数，指定额外的 host 名称映射信息。示例：extra_hosts: - "googledns:8.8.8.8" - "dockerhub:52.1.157.61"会在启动后的服务容器中 /etc/hosts 文件中添加如下两条条目：8.8.8.8 googledns 52.1.157.61 dockerhubr.healthcheck通过命令检查容器是否健康运行。示例：healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] interval: 1m30s timeout: 10s retries: 3s.image指定为镜像名称或镜像 ID。如果镜像在本地不存在，Compose 将会尝试拉取这个镜像。示例：image: ubuntu image: orchardup/postgresql image: a4bc65fdt.label为容器添加 Docker 元数据（metadata）信息，比如可以为容器添加辅助说明信息。示例：labels: com.startupteam.description: "webapp for a startup team" com.startupteam.department: "devops department" com.startupteam.release: "rc3 for v1.0"u.logging配置日志选项。示例：logging: driver: syslog options: syslog-address: "tcp://192.168.0.42:123"目前支持三种日志驱动类型：driver: "json-file" driver: "syslog" driver: "none"options 配置日志驱动的相关参数：options: max-size: "200k" max-file: "10"v.network_mode设置网络模式，使用和docker run的--network参数一样的值。示例：network_mode: "bridge" network_mode: "host" network_mode: "none" network_mode: "service:[service name]" network_mode: "container:[container name/id]"w.networks配置容器连接的网络。示例：version: "3" services: some-service: networks: - some-network - other-network networks: some-network: other-network:在服务容器里写成数组形式，在服务容器外总结时写成字典形式。后面也有一些指令需要在服务容器内外都写一次。x.pid跟主机系统共享进程命名空间。打开该选项的容器之间，以及容器和宿主机系统之间可以通过进程 ID 来相互访问和操作。示例：pid: "host"y.ports暴露端口信息。使用宿主端口：容器端口 (HOST:CONTAINER) 格式，或者仅仅指定容器的端口（宿主将会随机选择端口）都可以。示例：ports: - "3000" - "8000:8000" - "49100:22" - "127.0.0.1:8001:8001"注意：当使用 HOST:CONTAINER 格式来映射端口时，如果你使用的容器端口小于 60 并且没放到引号里，可能会得到错误结果，因为 YAML 会自动解析 xx:yy 这种数字格式为 60 进制。为避免出现这种问题，建议数字串都采用引号包括起来的字符串格式。z.secrets存储敏感数据，例如一些密码。示例：version: "3.1" services: mysql: image: mysql environment: MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password secrets: - db_root_password - my_other_secret secrets: my_secret: file: ./my_secret.txt my_other_secret: external: true服务容器内外各写一次。a1.security_opt指定容器模板标签（label）机制的默认属性（用户、角色、类型、级别等），比如配置标签的用户名和角色名。示例：security_opt: - label:user:USER - label:role:ROLEb1.stop_signal设置另一个信号来停止容器，不指定时默认使用的是SIGTERM停止容器。示例：stop_signal: SIGUSR1c1.sysctls配置容器内核参数。示例：sysctls: net.core.somaxconn: 1024 net.ipv4.tcp_syncookies: 0或：sysctls: - net.core.somaxconn=1024 - net.ipv4.tcp_syncookies=0d1.ulimits指定容器的 ulimits 限制值。例如，指定最大进程数为 65535，指定文件句柄数为 20000（软限制，应用可以随时修改，不能超过硬限制） 和 40000（系统硬限制，只能 root 用户提高）： ulimits: nproc: 65535 nofile: soft: 20000 hard: 40000e1.volumes设置数据卷挂载路径。可以设置为宿主机路径(HOST:CONTAINER)或者数据卷名称(VOLUME:CONTAINER)，并且可以设置访问模式 （HOST:CONTAINER:ro）。该指令中路径支持相对路径。示例：volumes: - /var/lib/mysql - cache/:/tmp/cache - ~/configs:/etc/configs/:ro如果路径为数据卷名称，必须在文件中配置数据卷（就是服务容器内外各写一次）：version: "3" services: my_src: image: mysql:8.0 volumes: - mysql_data:/var/lib/mysql volumes: mysql_data: f1.其他指令此外，还有包括domainname, entrypoint, hostname, ipc, mac_address, privileged, read_only, shm_size, restart, stdin_open, tty, user, working_dir 等指令，基本跟docker run中对应参数的功能一致。指定服务容器启动后执行的入口文件：entrypoint: /code/entrypoint.sh指定容器中运行应用的用户名：user: nginx指定容器中工作目录：working_dir: /code指定容器中搜索域名、主机名、mac 地址等：domainname: your_website.com hostname: test mac_address: 08-00-27-00-0C-0A允许容器中运行一些特权命令：privileged: true指定容器退出后的重启策略为始终重启。该命令对保持服务始终运行十分有效，在生产环境中推荐配置为always或者unless-stopped：restart: always以只读模式挂载容器的 root 文件系统，意味着不能对容器内容进行修改：read_only: true打开标准输入，可以接受外部输入：stdin_open: true模拟一个伪终端：tty: trueg1.读取变量值Compose 模板文件支持动态读取主机的系统环境变量和当前目录下的 .env 文件中的变量。例如，下面的 Compose 文件将从运行它的环境中读取变量 ${MONGO_VERSION} 的值，并写入执行的指令中：version: "3" services: db: image: "mongo:${MONGO_VERSION}"如果执行MONGO_VERSION=3.2 docker-compose up则会启动一个 mongo:3.2 镜像的容器；如果执行MONGO_VERSION=2.8 docker-compose up则会启动一个 mongo:2.8 镜像的容器。若当前目录存在 .env 文件，执行docker-compose命令时将从该文件中读取变量，比如在当前目录新建 .env 文件并写入以下内容：# 支持 # 号注释 MONGO_VERSION=3.6执行docker-compose up则会启动一个 mongo:3.6 镜像的容器。h1.其他参考网页官方文档awesome-compose（5）docker-compose实战https://yeasy.gitbook.io/上提供了一些项目实战：实战 Django实战 Rails实战 WordPress实战 LNMP笔者在 Docker树莓派实践——LNMP 中使用三种方法搭建了LNMP环境，值得参考。2.其他进阶内容docker buildx：新一代构建镜像工具，详见Docker Buildx。swarm mode：集群管理容器，详见Swarm mode。以上内容菜鸟教程也有相应介绍。3.使用docker-compose更新容器只需要3步。进入docker-compose.yml所在的文件夹，进行如下操作：docker-compose pull docker-compose up -d --remove-orphans更新完所有容器后，删除旧的镜像：docker image prune4.docker使用心得（1）docker网络-单机如果涉及到多个容器组（stacks）的互相协作，可能新手不太容易搞清互相怎么写ip。笔者经过一些实验，稍微搞清楚了。a.单个容器自己联系自己，写localhost即可；b.容器组a内部，或者容器组a、容器组b都加入了同一个docker网络，那么写容器名就可以（或者用inspect查看内部的ip，但是这样移植性不好）；c.容器组a没有使用 network_mode:host ，容器组b则用了 network_mode:host ，或者宿主机上其它的服务c，这时a要联络到b、c，需要使用 ip addr show docker0 获得宿主机上docker0的ip，填写之，且此时需要注意b、c的端口冲突。从定义域上说这个ip范围是 容器外 ∩ 宿主机内 。d.在一些特殊情况下，可能镜像有bug，docker0的ip不行，就需要 ifconfig 找到eth0的inet的ip（网卡0，默认网卡），填写之。比如0.53.2版本的frps的webServer.addr（旧称dashboard）。（2）docker与ufwdocker与ufw等防火墙有一些冲突，这主要是因为docker和ufw都是通过操纵底层的iptables来实现防火墙功能的，而docker修改iptables后无法通过ufw查看。a.端口自动开放参考 这个问题 。使用某些容器时（比如一些数据库），用docker把容器的端口映射后，即使没有用ufw把该端口打开，也能从非localhost访问端口。解决方法是在yml中写 127.0.0.1:8080:8080 ，即在宿主机端口前添加 127.0.0.1: ，限制只能从本机访问端口。需要注意的是，用frp这种反向代理时来源ip也会是localhost。笔者实践时没有遇到这种问题，可能后来版本的docker已经解决了这个问题。b.端口必须开放在后面的文章中，笔者使用NPM时，遇到了这样的情况：NPM映射部分端口，加入docker网络（容器a）；其它一些docker容器使用 network_mode:host （容器b。docker网络和host模式应该是冲突的，以host模式优先）；宿主机上一些没有使用docker的其它服务（服务c）。这时容器a要联络到容器b、服务c，要通过docker0的ip（ ip addr show docker0 ），同时需在ufw中打开该端口。如果你的宿主机不对公网开放，这没什么问题，但是对公网开放时这些端口经常被人扫。笔者目前的解决方法是在ufw中指定来源ip，比如：ufw allow from 192.168.1.0/24 to any port 22允许 192.168.1.* 来源的ip访问22端口（ 24 是掩码，一种特定的写法）。这样端口虽然开放了，但外网无法访问。更多ufw的使用方法，请自行搜索，比如参考 这个问题 。（3）docker网络-多机设想以下的场景：机器A上运行容器a，机器B需要访问容器a。一般的场景下将容器a端口映射或者用host模式即可，但在某些情况下（比如端口冲突且不能更改时），可以选择将容器a虚拟化、分配一个独立的局域网ip，相当于一台机器C。macvlan、overlay技术都可以达到该效果，其中macvlan技术的示例见 自建DNS与广告过滤-adguardhome与pihole 中的附录。
本文较长，属于系统性总结，可作为使用手册查阅。本文主要介绍了docker常识与基础。步骤/目录： 一、初识docker与安装docker 二、docker网页GUI与docker源 三、docker命令行基本操作 1.docker pull拉取镜像 2.docker run运行容器 3.docker image ls查看镜像 4.docker image rm删除镜像 5.docker commit保存容器 6.docker cp及其他命令 四、编写Dockerfile定制镜像 1.FROM和RUN命令 2.Dockerfile的其他指令 （1）COPY （2）ADD （3）CMD （4）ENTRYPOINT （5）ENV （6）ARG （7）VOLUME （8）EXPOSE （9）WORKDIR （10）USER （11）HEALTHCHECK （12）ONBUILD （13）LABEL （14）SHELL 3.拉取时自动对应系统 4.其他方式制作镜像 五、操作容器 1.启动容器 2.后台运行容器 3.终止/重启容器 4.进入容器 5.导入/导出容器 6.删除容器 六、仓库 1.官方仓库 2.私有仓库 3.使用Nexus帮助管理私有仓库 七、docker数据卷 1.数据卷的常用操作 2.挂载宿主机目录作为数据卷 八、容器与网络 1.端口映射 2.docker网络 3.配置DNS 4.高级网络配置本文首发于个人博客https://lisper517.top/index.php/archives/18/，转载请注明出处。本文的目的是对docker基础内容做总结归纳，使用本文从零学习docker将有一定难度。本文主要总结自菜鸟教程和Docker——从入门到实践和一些其他零散资料，有不明白的地方请参考原文。本文写作日期为2022年2月-7月。本文使用的是树莓派4B（内存8G版），系统为Pi OS 64位桌面版（2022年4月4日更新，镜像名 2022-04-04-raspios-bullseye-arm64.img ），装上系统后仅做了一些基础配置（安装了ufw、watchdog，设置了ssh密钥，apt update了一下）。一、初识docker与安装dockerDocker是基于Go语言的开源应用容器引擎，可以将应用和依赖打包成容器、在其他linux/windows/mac机器运行，还支持虚拟化。docker的优点主要有三：标准化、一致化；可移植、响应式部署；轻量级、硬件开销小。最后附上docker官网和Github社区版docker。另外，Docker对比传统虚拟机有很大优势（硬件开销小）。最后介绍一下docker架构：包括镜像，容器和仓库。面向对象编程中，类实例化后就创建了对象，对于docker来说，镜像=类，容器=对象，仓库则用来保存镜像。另外，可通过 仓库名:标签 指定镜像版本，不给出时默认为latest。安装docker推荐使用官方脚本自动安装，树莓派可运行如下命令：cd /tmp curl https://get.docker.com/ > ./docker_install.sh chmod +x docker_install.sh sh docker_install.sh –mirror Aliyun #换源安装 rm docker_install.sh没有配置源时不要直接apt install安装Docker，使用apt命令安装可参考树莓派实验室和Docker——从入门到实践。安装完后测试docker，输入：docker run hello-world #输出结果 Unable to find image 'hello-world:latest' locally #默认为latest，并先在本地查找 latest: Pulling from library/hello-world #从公共仓库pull，这里是默认的Docker Hub 9b157615502d: Pull complete Digest: sha256:97a379f4f88575512824f3b352bc03cd75e239179eea0fecc38e597b2209f49a Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (arm32v7) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://hub.docker.com/ For more examples and ideas, visit: https://docs.docker.com/get-started/接下来进行如下操作：systemctl enable docker systemctl start docker usermod -aG docker pi #这行是把pi用户添加到docker组，就是给个权限。没有pi用户的可以改成其他用户。另外，这里提一下如何在windows机器上安装docker。首先，推荐运行windows的WSL服务（微软推出的，一种在windows上运行linux的功能），安装方式为：（1）确定你的windows版本，win10的话需要2004版本及之后；或者win11。（2）用管理员模式打开cmd，输入 wsl --install 即可（默认安装的是最新的 WSL 1，也有可能是2）。简单介绍一下wsl操作。使用 wsl --help 查看帮助， wsl --list --online （或简写 wsl -l -o ）查看目前支持的linux发行版（2022年9月，支持Ubuntu，Debian，kali-linux，openSUSE-42，SLES-12，Ubuntu-16.04、18.04、20.04）， wsl --install -d 发行版名称 来下载安装一个发行版。这里以Ubuntu-20.04为例。使用 wsl --update 更新wsl的内核版本，使用 wsl --set-version Ubuntu-20.04 2 来指定下载的发行版使用WLS 2。另外， wsl -d 发行版名 可运行发行版， wsl -shutdown 是关闭所有发行版并退出wsl， wsl -t 发行版名 终止一个发行版， wsl -s 发行版 将发行版设定为默认， wsl --unregister 发行版 可注销、删除发行版实例。然后到 docker-desktop下载网页 ，或者直接点击 这个链接 下载windows版的docker-desktop，安装、重启，打开docker-desktop，检查一下设置里面的 Use the WSL 2 based engine 是否勾选上了。针对VS Code，可以在扩展里找到 Remote - WSL ，然后你就可以在windows、虚拟机里同时运行两个VS Code，方便你在虚拟机中修改代码。如果你的windows不支持WSL，就跳过安装WSL的步骤，只安装docker-desktop。二、docker网页GUI与docker源docker的portainer镜像提供了网页GUI，操作更加方便。下载并运行portainer（使用9000端口。这里的ce是community edition社区版，原来的portainer/portainer已停止更新）：docker pull portainer/portainer-ce docker volume create portainer_data docker run -d -p 9000:9000 --name portainer --restart always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce #不用在ufw打开端口这时浏览器即可查看 树莓派ip:9000 进入portainer。初次进入先设置用户和密码，然后选择Local即可。不过初次接触docker最好还是从shell开始，下面介绍一些docker命令。另外，若拉取镜像时较慢，可以配置一下镜像加速。对树莓派，新建/etc/docker/daemon.json并写入：{ "registry-mirrors": [ "https://hub-mirror.c.163.com", "https://mirror.baidubce.com" ] }如果有阿里云还可查看自己账号的加速器地址，详见Docker——从入门到实践。另外，后面将用到 docker pull 拉取镜像。有些镜像较大，下载可能中断，重新下载又要从头开始。要让镜像下载时支持断点续传，可在 daemon.json 中添加一个容器快照功能：{ "registry-mirrors": [ "https://hub-mirror.c.163.com", "https://mirror.baidubce.com" ], "features": {"containerd-snapshotter": true} }然后重启docker，并检查加速器：systemctl daemon-reload systemctl restart docker docker info #输出结果 ...... ...... Registry Mirrors: https://hub-mirror.c.163.com/ https://mirror.baidubce.com/三、docker命令行基本操作1.docker pull拉取镜像官方仓库Docker Hub上有大量镜像，获取镜像使用docker pull。输入docker pull --help可发现该命令的格式：docker pull [选项] [Docker仓库地址[:端口号]/]仓库名[:标签]其中，Docker仓库地址的格式为<域名/ip地址>，有时后面可加:端口号。若不给出则默认为docker.io（官方的Docker Hub）；仓库名一般是两段：<用户名>/<软件名>。若使用Docker Hub且不给出用户名，则用户名为默认的library；:标签指定软件版本，若不给出则默认为latest。接下来在树莓派上使用如下命令：docker pull ubuntu输出如下：Using default tag: latest latest: Pulling from library/ubuntu latest: Pulling from library/ubuntu 8795d4da4abd: Pull complete Digest: sha256:626ffe58f6e7566e00254b638eb7e0f3b11d4da9675088f4781a50ae288f3322 Status: Downloaded newer image for ubuntu:latest docker.io/library/ubuntu:latest2.docker run运行容器docker run [OPTIONS] IMAGE [COMMAND] [ARG...]可实例化镜像为容器。使用docker run时，Docker会先在本地主机查找镜像，若不存在则会从默认的官方仓库Docker Hub下载。docker run -it ubuntu bash-i允许对容器内的标准输入进行交互，-t在容器内指定终端。上面的命令输入后，就进入了ubuntu:latest容器的bash终端，可以输入一些命令交互，比如：cat /proc/version #输出结果 Linux version 5.10.92-v7l+ (dom@buildbot) (arm-linux-gnueabihf-gcc-8 (Ubuntu/Linaro 8.4.0-3ubuntu1) 8.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1514 SMP Mon Jan 17 17:38:03 GMT 2022 cat /etc/os-release #输出结果 NAME="Ubuntu" VERSION="20.04.3 LTS (Focal Fossa)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 20.04.3 LTS" VERSION_ID="20.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=focal UBUNTU_CODENAME=focal测试完毕后，输入exit退出。另外，如果输入的是docker run -it --rm ubuntu bash，则退出后会自动删除容器。也可以通过刚才的portainer提供的网页UI中删除，或后续介绍的docker xxx rm命令。3.docker image ls查看镜像docker image ls或docker images列出已下载的镜像，其中IMAGE ID是镜像的唯一标识。如果使用docker image ls -a则还可以看到中间层镜像（docker是一层层搭建的，中间层差不多相当于存档点，后续有介绍）；使用docker image ls 镜像名[:标签]还可以筛选镜像。另外，docker image ls支持-f参数（过滤器），比如列出建立时间在某镜像之后的可使用docker image ls -f since=镜像名（before同理）。最后，docker image ls -q可以输出镜像ID（常用于对镜像批量操作）。docker system df查看镜像、容器、数据占用的空间。dangling image（虚悬镜像）：有时用docker image ls时会出现<none>镜像，这是因为之前下载过某版本的镜像，结果后来官方的同名镜像更新了又下过一次，旧的镜像名给了新镜像。这样的镜像是可以随意删除的，用命令docker image prune。docker image prune和docker container prune可以用于删除当前不使用的镜像或容器。4.docker image rm删除镜像docker image rm [OPTIONS] IMAGE [IMAGE...]可删除一个或多个镜像，其中IMAGE可以是镜像的短ID、长ID、名字或摘要。其中长ID就是docker image ls列出的，短ID则是长ID的前几位、足够与其他镜像区分，一般取前3位。删除镜像时，会先untag镜像，即取消符合要求的镜像的标签；然后从镜像的上层向基础层开始删除，若有其他镜像依赖于某一层，则可能暂时不会删该层。另外，如果有活动的容器正在使用镜像，这时删除镜像要先停止容器（docker stop 容器短id或名字）、删除容器（docker container rm 容器短id或名字），再删镜像（docker image rm 镜像短id或名字）。最后，docker image rm可以配合docker image ls -q批量删除镜像，如删除指定名字的镜像：docker image rm $(docker image ls -q 镜像名)或删除指定 镜像:标签 创建前的镜像：docker image rm $(docker image ls -q -f before=镜像名:标签)5.docker commit保存容器镜像构建时是一层层的，每一层是在前一层基础上，并且每层只包含需要的东西。有些镜像间可以复用某些层。容器运行时需要存储，所以容器就是在镜像的基础上加了一个存储层。docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]可以保存存储层、构成新的镜像，以后运行时就是改动过文件变化的容器。注意，不要用docker commit构建镜像。下面的命令将容器保存为镜像，并附加了一些信息：docker commit \ --author "Tao Wang <twang2218@gmail.com>" \ --message "修改了xxxx" \ 容器名 \ 镜像名:自定义标签名之后docker image ls就能看到标签为自定义标签名的新镜像。docker history 镜像名:标签名则可以查看镜像的历史记录。最后，docker commit只有一些特殊应用（如被入侵后保存现场），不应该频繁使用（如当作系统备份），因为改动的层多、镜像会越来越臃肿，而且时间久了会记不清该镜像进行过的操作。6.docker cp及其他命令下面介绍一下docker cp命令。其用途为容器与主机之间的数据拷贝，格式为：#宿主机拷贝到容器 docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|- #容器拷贝到宿主机 docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH可用的选项为-L，保持源目标中的链接（Always follow symbol link in SRC_PATH）。补充一点docker cp的使用细节，如果是参数是两个目录，那么不论哪一方、末尾加不加/，效果都是把源目录放到目标目录下：docker cp /www/dir_1 container_1:/www docker cp /www/dir_1 container_1:/www/ docker cp /www/dir_1/ container_1:/www docker cp /www/dir_1/ container_1:/www/以上4条命令效果都是一样的。其他docker命令可参考runoob的 Docker命令大全 。四、编写Dockerfile定制镜像1.FROM和RUN命令Dockerfile是一个脚本文件，其中包含了一条条指令，每一条指令构建一层。一般来说，应该用Dockerfile而不是docker commit构建镜像。以nginx镜像为例，进行如下操作：mkdir /tmp/mynginx cd /tmp/mynginx nano Dockerfile写入内容：FROM nginx RUN echo ' Hello, Docker! ' > /usr/share/nginx/html/index.htmlFROM是指定基础镜像，该命令一般是Dockerfile的第一条指令（有时前面可出现ARG）。另外，scratch表示一个空白镜像。RUN是用来执行bash命令的，其格式有两种：一是 RUN bash命令 ，二是 RUN ["可执行文件", "参数1", "参数2"...]。因为Dockerfile一条指令构建一层，所以有多条bash命令时不应连续使用RUN，而是应该写成如下示例：FROM debian:stretch RUN set -x; buildDeps='gcc libc6-dev make wget' \ && apt-get update \ && apt-get install -y $buildDeps \ && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \ && mkdir -p /usr/src/redis \ && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \ && make -C /usr/src/redis \ && make -C /usr/src/redis install \ && rm -rf /var/lib/apt/lists/* \ && rm redis.tar.gz \ && rm -r /usr/src/redis \ && apt-get purge -y --auto-remove $buildDeps另外，构建每一层时都尽量只包含必要的东西，以避免臃肿；上面的RUN在最后删除了编译构建用的软件，是非常重要的。现在回到刚才创建的基于nginx的Dockerfile，执行：docker build -t nginx:v3 . #最后的 . 是上下文路径，不要遗漏输出如下（之前未docker pull nginx）：Sending build context to Docker daemon 2.048kB Step 1/2 : FROM nginx latest: Pulling from library/nginx aaef1f1162ec: Pull complete 158fdbcad3be: Pull complete e91a8f81054f: Pull complete 5dac8ed1dcde: Pull complete f4ad1f3b674e: Pull complete 83c0cd4ca302: Pull complete Digest: sha256:2834dc507516af02784808c5f48b7cbe38b8ed5d0f4837f16e78d00deb7e7767 Status: Downloaded newer image for nginx:latest ---> d12b7a1d6c8a Step 2/2 : RUN echo ' Hello, Docker! ' > /usr/share/nginx/html/index.html ---> Running in c79b0e883836 Removing intermediate container c79b0e883836 ---> d749600f530b Successfully built d749600f530b Successfully tagged nginx:v3build命令的格式为docker build [选项] <上下文路径/URL/->，本例中上下文路径就是 . 。要注意上下文路径并不是指保存Dockerfile的路径。docker在运行时分为docker引擎和客户端工具，虽然这里一直是在本地运行，但其实全程都通过API实现引擎和客户端交互，属于 Client/Server 的模式。为了让服务器端获取本地文件， docker build 会将上下文路径里的所有内容打包上传到服务器。所以在构建镜像时需要用到的东西都保存到上下文路径，Dockerfile里写 COPY 时也要写相对于上下文路径的相对路径（COPY ./xxx/xxx 容器内路径）。刚才执行docker build -t nginx:v3 .时显示结果第一行Sending build context to Docker daemon 2.048kB就是在打包上下文路径到引擎服务器端。2.Dockerfile的其他指令这里介绍的比较简略，附带使用例的详细介绍可参考Dockerfile 指令详解或Docker 命令大全。（1）COPYCOPY命令的格式为COPY [--chown=<user>:<group>] <源路径>... <目标路径>或COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]用途是将上下文路径的东西复制到本层镜像内的目标路径位置。源路径可以有多个（目标路径只有一个），也可以写成通配符，通配符要满足Go语言的filepath.Match规则。目标路径可以是绝对路径或相对于工作目录的相对路径（用WORKDIR可指定工作目录），目标路径不存在时会自动创建。另外，COPY可以保留源文件的rwx、变更时间等元数据，也可用--chown=<user>:<group>来改变所属用户和组。最后，由于目标路径只有一个，所以一般只写一次COPY，之后再通过RUN mv等方式移动文件和目录。（2）ADDADD和COPY的格式、性质差不多，但ADD增加了一些功能。但是由于其有些功能不明确，所以一般情况下都使用COPY。ADD的源路径可以是URL，引擎会自动下载文件放到目标路径，并且文件权限为600。但这个功能不推荐使用，因为可以用RUN替代，一般都是用wget、curl下载后，再进行改权限、解压、清理等操作。当源路径为tar文件、压缩格式为gzip、bzip2、xz时，ADD会自动解压该文件到目标路径。目前ADD应仅用于这种情形，其他情况用COPY和RUN代替。（3）CMDCMD用于指定容器运行的主进程。容器仅为主进程而存在，当主进程结束后容器也会停止。CMD和RUN的格式类似，也是两种：shell 格式：CMD <命令> exec 格式：CMD ["可执行文件", "参数1", "参数2"...]在exec格式中，一定注意要使用双引号。另外还有一种格式：参数列表格式：CMD ["参数1", "参数2"...]，是在指定了 ENTRYPOINT 指令后，用 CMD 指定具体的参数。CMD命令在执行时会包装成sh -c形式，比如：CMD echo $HOME包装为CMD [ "sh", "-c", "echo $HOME" ]。另外要注意docker容器中的应用都应前台执行，因为容器就是为了主进程存在的，主进程退出容器也会退出。比如nginx启动不应写成CMD service nginx start或CMD systemctl start nginx，而应是CMD ["nginx", "-g", "daemon off;"]，即一般都是直接运行二进制文件。多个CMD时，仅最后一个生效（比如从官方镜像生成自己的镜像，就可以写CMD来覆盖官方镜像中的CMD）。（4）ENTRYPOINTENTRYPOINT的格式和RUN、CMD一样，分为exec格式和shell格式。ENTRYPOINT的用途和CMD一样，也是指定容器启动程序和参数。当指定了ENTRYPOINT后，CMD就不再直接运行命令，而是将CMD的内容作为参数传给ENTRYPOINT。比如Dockerfile里写：ENTRYPOINT [ "xxx", "xxx" ] CMD ["参数1", "参数2"...]那么使用该镜像时就可以docker run 镜像名 参数形式来传递参数。ENTRYPOINT 还可用于应用运行前的准备工作。和CMD一样，多个ENTRYPOINT时仅最后一个生效。（5）ENVENV的格式有两种：ENV <key> <value> ENV <key1>=<value1> <key2>=<value2>...ENV的作用是设置环境变量，方便后续调用，和docker run -e作用一样。比如对于mysql镜像，可使用以下方式设置root密码：ENV MYSQL_ROOT_PASSWORD=123456或者用docker run -e MYSQL_ROOT_PASSWORD=123456也是一样的。最后注意，一般docker run参数指定的东西优先级更高，会覆盖其他地方的设置，其他命令也是这样。（6）ARGARG的格式为：ARG <参数名>[=<默认值>]ARG的用途和ENV一样，但和ENV不同在于，ARG设置的环境变量在容器运行时不存在（不能传递给容器）。另外ARG有生效范围，在FROM之前用的话ARG只能用于FROM中。（7）VOLUMEVOLUME的格式为：VOLUME ["<路径1>", "<路径2>"...] VOLUME <路径>VOLUME用于定义匿名卷。容器运行时应尽量保持存储层不写入，对数据库等，其数据库文件应保存在卷中。为防止用户运行容器时忘记挂载卷（docker run忘记-v参数），就可以用VOLUME指定匿名卷。（8）EXPOSEEXPOSE的格式为：EXPOSE <端口1> [<端口2>...]用途是声明端口。和docker run -p的区别在于EXPOSE只起声明作用，没有映射端口的功能。（9）WORKDIR格式为：WORKDIR <工作目录路径>作用为指定容器内的当前目录（不存在时自动创建）。在Dockerfile中一个RUN就是一层，两个RUN执行的环境不同（比如使用cd时）。所以要不就写绝对路径，要不就指定工作目录。Dockerfile中连续多次使用WORKDIR不是切换当前目录，而是类似于cd，进入子目录。（10）USER格式为：USER <用户名>[:<用户组>]作用是指定当前用户。如果执行脚本时希望切换到其他用户，不应用su或sudo等，而应使用gosu。（11）HEALTHCHECK格式：HEALTHCHECK [选项] CMD <命令>：设置检查容器健康状况的命令HEALTHCHECK NONE：如果基础镜像有健康检查指令，使用这行可以屏蔽掉其健康检查指令用途是告诉docker如何判断容器的状态是否正常。加了HEALTHCHECK的镜像，运行容器时初始状态为starting，然后通过检查变为healthy，连续数次失败则为unhealthy。HEALTHCHECK的选项：--interval=<间隔>：两次健康检查的间隔，默认为 30 秒；--timeout=<时长>：健康检查命令运行超时时间，如果超过这个时间，本次健康检查就被视为失败，默认 30 秒；--retries=<次数>：当连续失败指定次数后，则将容器状态视为 unhealthy，默认 3 次。HEALTHCHECK只应出现一次。多次出现时，仅最后一个生效。（12）ONBUILD格式为：ONBUILD <其它指令>ONBUILD比较特殊，它后面跟的是其它指令，比如RUN, COPY等，且这些指令在当前镜像构建时并不会被执行，只有当以当前镜像为基础镜像去构建下一级镜像的时候才会被执行。（13）LABEL格式为：LABEL <key>=<value> <key>=<value> <key>=<value> ...用途是为镜像添加元数据。（14）SHELL格式为：SHELL ["executable", "parameters"]用于指定RUN、ENTRYPOINT、CMD的shell。Dockerfile中使用的指令介绍告一段落，下面介绍点了解内容。3.拉取时自动对应系统使用某种架构的系统docker pull时一般应该拉取对应架构专用的镜像。使用 manifest 列表就能不指定架构、自动拉取对应架构的镜像。详见官方博客。4.其他方式制作镜像除了Dockerfile，还可从rootfs压缩包或docker save、docker load制作镜像。详见其他制作镜像的方式。五、操作容器1.启动容器启动容器分两种，一种是新建并启动容器，另一种是重启已经exited的容器。docker run用于新建并启动容器，docker container start用于启动已经终止的容器。但是由于docker的轻量级，一般容器都是随用随删的。2.后台运行容器docker run -d可以后台运行容器（STDOUT不输出到宿主机），之后会返回一个容器id，也可以通过docker container ls查看。要查看容器的STDOUT，可使用docker container logs。3.终止/重启容器docker container stop可终止运行中的容器，或者主进程结束后容器也会自动终止。已经终止的容器可通过docker container ls -a看到。docker container restart可重启容器。4.进入容器使用docker run -d时容器会在后台进行。若要进入容器操作，可使用docker attach或docker exec，其中推荐使用docker exec。比如运行一个ubuntu容器时，docker attach 容器id和docker exec -it 容器id bash效果是一样的，但在docker attach中使用exit会导致容器终止，docker exec则不会。5.导入/导出容器docker export 容器id > 123.tar可导出容器快照到本地；docker import可导入容器快照为镜像。import和load的区别在于import导入时丢弃了快照的历史记录、元数据，可以重新指定这些元数据，import生成的快照文件也因此要小一点。6.删除容器docker container rm可删除终止状态的容器。加-f参数可删除运行中的容器（不推荐，一般还是停止容器再删除）。docker container prune可删除所有终止状态的容器。六、仓库1.官方仓库官方仓库为Docker Hub，大部分镜像都可找到。可在docker官方网站注册一个docker账号，并使用docker login、docker logout登入、登出。docker search 关键词可查找镜像，--filter=stars=N可仅显示收藏在N以上的镜像。找到心仪的镜像后，使用docker pull下载镜像。登录后还可以通过docker push推送自己的镜像（制造代码垃圾，屎山又变多了）。2.私有仓库可以通过官方提供的docker-registry构建私有镜像仓库。首先安装运行docker-registry：docker run -d -p 5000:5000 --restart=always --name registry registry默认仓库创建在/var/lib/registry下，可通过-v参数指定镜像存放在本地的路径。然后使用docker tag标记一个镜像，并推送到私有仓库（以之前下载的ubuntu镜像为例）：docker tag ubuntu:latest 127.0.0.1:5000/ubuntu:latest docker push 127.0.0.1:5000/ubuntu:latest这时可以用curl查看私有仓库中的镜像：curl 127.0.0.1:5000/v2/_catalog显示结果：{"repositories":["ubuntu"]}如果在公司中，可能希望局域网的主机都能推送镜像，但docker默认不允许通过非HTTPS方式推送镜像。这时可以配置docker取消该限制，或配置通过HTTPS访问的仓库。后者比较复杂，但有新机器入网时不用添加配置，详见私有仓库高级配置。要配置取消docker的HTTPS限制，可在/etc/docker/daemon.jason中写入：{ "registry-mirror": [ "https://hub-mirror.c.163.com", "https://mirror.baidubce.com" ], "insecure-registries": [ "192.168.xxx.xxx:5000" ] }其中registry-mirror是前面提到的镜像下载加速，insecure-registries则是对局域网内的ip192.168.xxx.xxx取消了限制。如果使用windows、mac的桌面版docker，在docker engine中编辑即可。3.使用Nexus帮助管理私有仓库详见Nexus 3，这里就不展开了。七、docker数据卷数据卷的特征：数据卷可以在容器之间共享和重用；对数据卷的修改会立马生效；对数据卷的更新不会影响镜像；数据卷默认会一直存在，即使容器被删除。可以说数据是灵魂，容器是皮囊。数据卷使用时类似mount，镜像中被指定为挂载点的目录中的文件会复制到数据卷中（仅数据卷为空时会复制，若数据卷非空则数据是从数据卷向容器“同步”）。1.数据卷的常用操作docker volume create my-vol docker volume ls docker volume inspect my-vol docker volume rm my-vol以上命令分别为创建数据卷、查看数据卷、查看指定数据卷详情、删除数据卷。docker run命令可使用--mount source=my-vol,target=/xxx/xxx来指定数据卷，并加载数据卷到容器的/xxx/xxx目录；或者直接docker run -v 数据卷:容器内路径。在挂载数据卷后，使用docker inspect 容器id，在Mounts键后可看到数据卷的相关信息。在删除容器时使用docker container rm -v可同时删除数据卷；使用docker volume prune可删除所有无主的数据卷。2.挂载宿主机目录作为数据卷--mount可以挂载主机上存在的目录作为数据卷：$ docker run -d -P \ --name web \ # -v /src/webapp:/usr/share/nginx/html \ --mount type=bind,source=/src/webapp,target=/usr/share/nginx/html,readonly \ nginx:alpine主机上的目录必须存在，且必须是绝对路径。如果不添加readonly，则默认容器可以读写该目录（见下面对-v模式的补充）。还可以挂载本地文件作为数据卷，比如要记录容器bash输入过的命令，可以这样写：--mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history这里补充一点-v选项的模式（以下内容笔者没有实验过）。docker run -v /www:/www这种默认模式下，宿主机和容器对目录内文件的增删改会同步；若挂载文件而非目录，那么宿主机和容器都可修改文件，但不会同步。docker run -v /www:/www:ro可以指定数据卷只读，则挂载目录或文件时容器都只能读取不能修改。docker run -v /www:/www:rw挂载目录时，双方对目录内文件的增删改和默认模式一样会同步；但挂载文件时不同，容器和宿主机的修改会互相同步，不同在于容器内不可删除该文件，且宿主机删除该文件时容器内不会同步。最后，如果你是在windows上使用 WSL + docker-desktop ，注意，在wsl的发行版里把windows系统盘挂载到了/mnt下（/mnt/c，/mnt/d这种形式），但是你指定-v参数时写windows目录，比如不是用 -v /mnt/d/... ，而是写 -v D:\... 这样，因为docker-desktop在后台调用WSL，会帮你转换路径。八、容器与网络1.端口映射使用-P或-p可指定端口映射。-P是随机映射外部端口到容器开放的网络端口；-p则可以指定端口，其格式为：ip:hostPort:containerPort、ip::containerPort或hostPort:containerPort。docker port 容器id 容器端口可查看绑定的地址。2.docker网络创建新的网络：docker network create -d bridge my-net其中-d指定网络类型。运行容器时加入网络使用--network指定网络：docker run -it --rm --name busybox1 --network my-net busybox sh （打开一个新终端） docker run -it --rm --name busybox2 --network my-net busybox sh再打开一个新终端，使用ping busybox1或ping busybox2可以验证容器互联。docker network connect 网络名 容器短id可以将已存在的容器加入网络，disconnect退网。如果多个容器需要互相配合，推荐使用docker compose。学会docker compose，就算是摆脱docker新手阶段了。3.配置DNS更改容器里的/etc/resolv.conf文件可更新DNS配置。在/etc/docker/daemon.json中添加以下内容也可设置容器DNS：{ "dns" : [ "114.114.114.114", "14.14.14.14" ] }4.高级网络配置这一节属于进阶内容。详见高级网络配置。
步骤/目录： 1.NAS释义及其用途 （1）局域网共享 （2）私人云存储 （3）离线下载 （4）数据备份/数据安全 （5）多人数据共享 （6）存储主机 2.NAS的实现方法 （1）商品NAS （2）DIY a.购买部件 b.NAS端软件下载 c.客户端软件与使用本文首发于个人博客https://lisper517.top/index.php/archives/17/，转载请注明出处。本文的目的是对NAS的大致框架进行简单介绍。本文写作日期为2022年7月6日。1.NAS释义及其用途NAS全称Network Attached Storage，即网络附属存储，通俗地说，基础的NAS就是一台小电脑+硬盘。相比于单纯的U盘、硬盘，NAS多出来的小主机可以实现很多额外的功能。NAS的用途可以大致分为以下几方面：（1）局域网共享比如在家庭场景下，可能包含windows电脑、linux电脑、MAC OS电脑、android或苹果手机、多种平板、电视机、投影仪等多种设备，有时需要在各个设备间共享文件，使用NAS的话就不用拿U盘拷来拷去。这里更偏向于多设备共享数据。（2）私人云存储如果使用一些手段使得外网也能访问NAS，那么就等同于拥有了私人云存储服务，实现云办公等。相较于百度网盘、阿里云盘等服务，NAS私人云存储的好处主要有：数据更偏向于在自己手里，泄露风险少；文件不会莫名其妙违规，一般也不会被网盘审查；不用买VIP，访问NAS的速度更快；敏感文件不会被网盘企业备份。（3）离线下载配合aria2、qBittorrent等软件，可以实现“离线”下载（台式机或笔记本离线）。相比于用台式机挂机下载，优势是耗电少、占用资源少、风扇噪音小。（4）数据备份/数据安全对一些重要数据可以用NAS进行云备份，常用的实现方法比如RAID等，可以通过重要数据存两份或纠错码等其他方式保全数据。（5）多人数据共享在公司等需要多人协作的场景下，可以实现内部数据共享。这里更偏向于多人、多设备、同时共享数据。（6）存储主机以后购买的电脑无需额外购置硬盘，把NAS作为存储即可，类似网吧。2.NAS的实现方法（1）商品NAS主要有群晖、威联通等品牌，其中群晖目前（2022年7月）仍占据相当一部分国内市场。相对于DIY，商品NAS的优势是简单易上手、配套服务（内网穿透等）成熟，缺点是定制度不够、价格较贵。（2）DIY这一部分只是简单介绍一下，具体细节可参考 树莓派+Samba+frp搭建NAS（网络云存储） 。a.购买部件如上所述，NAS主要包括一个小电脑和硬盘（以及硬盘读取设备），但是为了增加安全性还可以买一个UPS（不间断电源），减少或防止突然断电造成的数据丢失（有的UPS在停电时还可发出关机信号，使NAS主机自动关机）。小电脑有很多选择，比如：专门的NAS主板、树莓派/其他派（见 树莓派型号、参数与作用 ）、路由器（常用的是带USB接口的）、改造过的机顶盒/投影仪、老旧笔记本/手机等。这部分可能花费200-1000。如果小电脑没有硬盘接口或者硬盘比较多时，需要购买硬盘读取设备（输出接口为SATA或USB3.0）。常见的有专门的NAS硬盘盒、普通的硬盘盒，也大概花费200-1000。硬盘一般是花费大头。目前的西部数据、希捷都有NAS专用硬盘，一张8T的大概1500左右。单纯给NAS用的小输出功率UPS可能在300-500。但是如果预算充足，可以顺便给台式机等其他设备买多设备用的高功率UPS，1000左右。如果有空闲的杂物房，也可以买一个退役的存储服务器，缺点是噪音稍大（60分贝甚至更高），优点是设计成熟（散热风道，稳定性）。目前来说笔者较推荐Dell的存储服务器，比如R730XD（见 b站视频 ），它使用双路主板，可用E5的CPU（E5神教）。b.NAS端软件下载有一些用于NAS的系统（TrueNas/FreeNas，OpenMediaVault），集成了分享、离线下载等各种NAS端功能，还推出了多种系统的客户端。如果不想用，也可以用下面提到的软件自己搭建。对于windows或linux主机，都可通过samba、ftp等方式实现硬盘共享；使用aria2、qBittorrent等软件可实现离线下载；使用RAID等方式实现NAS数据安全（比如Ubuntu+mdadm）。使用frp或其他软件实现外网访问（还需要一个公网ip），更简单的方式则是 ipv6+DDNS 。使用plex、emby、jellyfin等软件管理影音文件。如果怕某地天灾（地震、火灾、洪灾等），可以异地+多个NAS，并使用Rsync进行异地备份（建议搭配ipv6。见 b站视频 ）。c.客户端软件与使用对samba、ftp实现的NAS，windows、linux机器上使用资源管理器或浏览器即可连接到NAS，android、投影仪等则可使用 ES文件浏览器 等软件。
步骤/目录： 0.Samba的陷阱 1.树莓派下载安装samba 2.配置samba 3.为samba绑定系统用户 4.其他客户端连接到共享文件夹 （1）linux （2）Windows （3）安卓手机 （4）投影仪、电视 附：ES文件浏览器 连接Samba 5.frp+samba 6.加快读写速度 （0）分析瓶颈、测试硬盘 （1）网线直连 （2）换路由器 （3）修改smb.conf （4）其他 （5）后续 （6）总结 7.自动挂载硬盘 8.使用qBittorrent离线下载 附录：小米手机开启FTP服务器本文首发于个人博客https://lisper517.top/index.php/archives/16/，转载请注明出处。本文的目的是使用树莓派搭建NAS，可以远程访问云硬盘上的文件，并可进行离线下载。本文实验日期为2022年2月19日。使用的是树莓派4B（内存8G版），系统为Pi OS 32位桌面版（2022年1月28日更新，镜像名 2022-01-28-raspios-bullseye-armhf.img ）。frps运行在阿里云服务器（系统镜像Ubuntu 20.04）。本文使用Samba搭建NAS，而且并没有涉及UPS电源、RAID等，属于入门级NAS，感兴趣的可自行深造。Samba可以替换为FTP，或者懒得自建NAS也可以购买群晖等现成NAS。0.Samba的陷阱当你在windows中打开 我的电脑 ，此时打开的东西叫文件管理器（Windows Explorer）。从文件管理器向其它地方拷贝东西，WE会要求目标硬盘先创建一个同等大小的空镜像（内容全部为0的镜像），再填充数据；Samba会调用 ftruncate() 函数来创建，而该函数在FAT文件系统上运行奇慢，需要花很长时间才能创建好空白镜像，WE以为Samba卡死了，最终会返回0x8007003B错误（ 参考回答 ）。FAT文件系统创建空镜像这么慢，归根结底是不支持sparse file（稀疏文件），所以它创建空镜像时真的会在硬盘上挨个写入0；而支持sparse file的，直接指定空镜像文件的头尾区块、存在metadata中即可，不用管这些区块上原本是1还是0。在Samba的设置中有一项隐藏的默认值 strict allocate = no ，如果改成yes，当单个文件大小超过500MB，Samba自己就会要求先生成空镜像再填充数据（参考 wiki-Samba 的 SMB/SMB2 write activities 一节）。综上所述，不需要改Samba的配置，只要保证Samba挂载的不是FAT文件系统（exFAT、FAT32）而是ext4、NTFS就行了（ 参考回答 ）。先创建空镜像再写入，本意是好的，只是FAT执行坏了。1.树莓派下载安装samba输入apt install samba即可。一共安装了16个包（如果没有samba-common-bin则需手动下载）：attr libcephfs2 liburing1 python3-dnspython python3-gpg python3-markdown python3-requests-toolbelt python3-samba python3-tdb python3-yaml samba-common samba-common-bin samba-dsdb-modules samba-vfs-modules tdb-tools2.配置sambasamba的配置文件为/etc/samba/smb.conf。在其中底部添加：[shared] path = /home/samba_user/shared #共享文件夹目录 valid users = samba_user writable = yes read only = no create mask = 0755 #创建文件时，权限为755（测试时发现不管改成什么，创建的文件都是755） directory mask = 0755 #创建目录权限为755（同上） #browseable = yes #无权限的人可看到资源名称另外在[global]下添加：security = user #设定安全等级保存退出，使用testparm检测samba设置有无语法错误。关于samba配置更多信息可参考这篇文章。安全方面，还可用hosts allow设置允许访问的主机、write list设置仅哪些用户可进行写操作。3.为samba绑定系统用户创建用户的命令可以参考 提升树莓派安全性 。这里新建一个用户samba_user，并创建共享文件夹：mkdir -p /home/samba_user/shared useradd -d /home/samba_user samba_user passwd samba_user chown -R samba_user:samba_user /home/samba_user然后为samba指定该用户及设置该用户的SMB密码，激活该用户，并重启服务：smbpasswd -a samba_user #后面更改SMB密码也用这个命令 smbpasswd -e samba_user #-d可disable指定SMB用户 service smbd restart #服务名也可能不叫smbd。service --show-all查看所有服务注意SMB密码和用户的系统密码可能不同，连接到samba共享文件夹只需要SMB密码。但是添加其他SMB用户时，还需要先创建一个同名的系统用户（否则 smbpasswd 时会提示 Failed to add entry for user），即需要添加SMB用户时，要先添加一个系统用户，然后添加同名SMB用户，设置SMB密码，连接时用SMB密码。增加用户的范例：useradd -d /dev/null -s /sbin/nologin samba_user2如上设置的samba_user2系统用户，没有家目录，也无法登录，只是为了添加SMB用户而创建，安全性好一点。然后用 smbpasswd 添加SMB用户即可。最后使用ufw打开samba使用的445端口：ufw allow 445。4.其他客户端连接到共享文件夹（1）linux以树莓派为例（这里顺便测试一下刚才的Samba设置），在资源管理器的地址栏输入 smb://树莓派ip ，按回车键后出现两个文件夹：print$与shared，其中shared就是刚才设置的共享文件夹，print$则是samba设置文件自带的。双击shared文件夹后提示需要输入密码，选择已注册用户，用户名为samba_user，密码为SMB密码，下面可以选是否记住密码、记住多久，然后即可进入共享文件夹。接下来插上u盘模拟硬盘，将u盘挂载到shared文件夹下：df -h #输出结果 #/dev/sda1 ...... /media/pi/xxxxx umount /media/pi/xxxxx cd /home/samba_user/shared mkdir usb_1 mount -o uid=samba_user,gid=samba_user /dev/sda1 usb_1 #指定挂载后文件夹所属用户及组用root身份时会默认挂载到root:root下，这里指定挂载到samba_user下才可对共享文件夹有rwx的权限（默认755）。然后在资源管理器中查看，可以成功看到u盘里的文件，也可修改、删除、新增文件。最后这里说明一下samba的权限问题：用smbpasswd -a 用户名可添加一个SMB用户，该用户必须是系统用户。读写权限有两个地方受到限制，一是登录SMB的系统用户对共享文件夹有无rwx权限，二是在smb.conf里设置共享文件夹的读写权限（writable = yes、read only = no都是可写入）。所以挂载硬盘时要挂载到SMB用户同名的系统用户下（要不然就在挂载时设置权限为xx7），且[shared]里面要指定可以读写共享文件夹。如果在命令行下，也可以挂载，但是要安装一些samba客户端或扩展。以CentOS为例：yum install -y cifs-utils mkdir /mnt/shared mount -t cifs -o username="用户名",password="密码" //树莓派ip/共享名 /mnt/shared #如 //192.168.1.190/shared cd /mnt/shared ls -al可以看到挂载成功。还可以设置开机自动挂载，参考 这篇文章 ，在linux上使用fstab自动挂载SMB网络存储，命令如下：vim /etc/fstab写入一行：//树莓派ip/共享名 /mnt/shared cifs defaults,username=用户名,password=密码,iocharset=utf8,nofail,auto 0 0然后可以试验一下：ls -al /mnt/shared umount /mnt/shared mount -a ls -al /mnt/shared没有报错，能正常挂载。（2）Windows在资源管理器的 此电脑 界面（就是显示C盘和其他盘符的界面），左上角点击 计算机-映射网络驱动器-映射网络驱动器 ，驱动器随便选，文件夹 写\\树莓派局域网ip\shared，点击连接，需要输入凭证。输入samba_user和对应的SMB密码后成功登录，也可增、删、改文件。如果windows上无法连接，可试试打开SMB功能：在设置中搜 启用或关闭Windows功能 ，找到 SMB 1.0/CIFS 文件共享支持 ，勾选上后确定，等组件下载完（带客户端、服务器端、自动删除），重启。但是这个可能是1.0的Samba协议，不知道对读写速度有无影响。笔者测试时没有开启此功能也可连接到共享文件夹。测试速度：树莓派4B无线连接，接口和u盘都支持usb3.0，百兆路由器，写入速度5-6MB/s（不含创建镜像的时间），读取速度5-6MB/s。这里使用的是exFAT格式的u盘，写入大文件时会花较长的时间生成镜像，再写入数据；写入10GB的大文件时，由于镜像迟迟未创建完毕，windows上还出现了0x8007003B错误。下一节中尝试加快读写速度。（3）安卓手机可用 ES文件浏览器 。但是据说其可能使用老的Samba协议，导致传输速度低。其他应用也有能连samba的。另外，有的手机自带ftp服务器，也可以方便地与电脑传输文件，详见附录。（4）投影仪、电视在投影仪、电视自带的应用市场一般也能找到 ES文件浏览器，下载即可。另外，据说SMB在播放高码率视频时，不时卡顿，不如NLDA，笔者没有进行比较。附：ES文件浏览器 连接Samba以安卓手机为例，选择 网络-局域网-新建， 服务器 填 树莓派ip:445，用户名、密码填SMB用户名、密码即可。5.frp+sambafrps和frpc的初始搭建参考 frp+云服务器搭建能从外网访问的redis、mysql。这里默认已经配置好了frps、frpc，仅在frpc.ini增加一项映射。samba默认使用445端口，在树莓派的frpc.ini中添加：[pi4B-samba-server-1] type = tcp local_ip = localhost local_port = 445 remote_port = 44550之后树莓派上重启frpc：service frpc restart，然后到frp服务器的防火墙打开44550端口（云服务器不要用ufw打开）。在树莓派上，资源管理器输入 smb://frp服务器ip:44550 ，即可看到print$和shared两个文件夹；但点开shared时显示 挂载windows共享失败 : 连接超时，可能是由于云服务器的带宽只有5Mbps（640KB/s）。在windows上，映射网络驱动器 不能写\\云服务器ip:44550\shared，只能用默认的445端口。不过可以使用netsh命令将云服务器的44550端口接到本地的端口445（参考文章）。打开cmd，输入：netsh interface portproxy add v4tov4 listenaddress=127.0.0.1 listenport=445 connectaddress=云服务器ip connectport=44550 netsh interface portproxy show all #查看一下 #最后使用netsh interface portproxy delete v4tov4 listenaddress=127.0.0.1 listenport=445即可删除这条端口转接设置确认无误后，在 映射网络驱动器 的 文件夹 一栏直接填\\127.0.0.1\shared即可。可能由于云服务器带宽问题，测试时一直卡在尝试连接界面。注意，以上流程稍微有些问题，具体参考新的文章：Samba, ftp与webdav 。6.加快读写速度（0）分析瓶颈、测试硬盘首先测试一下瓶颈可能在哪里。从windows机器到树莓派，数据需要经过：硬盘读写->树莓派内存->树莓派网卡->有线时通过网线，无线通过无线网卡->路由器->有线或无线->客户端（暂不考虑客户端变化）。可以控制的地方主要有：硬盘或u盘读写速度；硬盘或u盘是否通过usb3.0连到树莓派；树莓派内存是否够用（本文中是8G版，测试时内存消耗不大）；网线传输速度/树莓派网卡速度；路由器网口是百兆还是千兆。首先测试一下u盘。安装hdparm（apt install hdparm），输入下面的命令：hdparm -Tt /dev/sda1 #输出结果 Timing cached reads: 1688 MB in 2.00 seconds = 844.54 MB/sec Timing buffered disk reads: 342 MB in 3.01 seconds = 113.62 MB/sec可以看出u盘的读取还是很快的（hdparm更详细的教程见runoob-hdparm）。然后使用dd命令测试u盘写入速度：cd /home/samba_user/shared/usb_1 dd if=/dev/zero of=./testwritefile bs=4KB count=131072 #写入5GB的文件，测试前要确保有这么大的空间 # 输出结果 ... 5242880000字节（5 GB，4.9 GiB）已复制，178.29 s，29.4 MB/s #rm testwritefile写入速度为30MB/s左右。更多关于dd命令测试硬盘的内容可参考这篇文章。（1）网线直连用买路由器送的免费网线直连树莓派到路由器，写入速度提升到11.5MB/s，已经达到路由器百兆上限（12.5MB/s），提升明显。（2）换路由器测试时的路由器只有百兆网口，换成千兆网口后估计速度可进一步提升；换成千兆网口后理论上支持125MB/s读写，但u盘的读写只有113 / 30 MB/s，将成为新的瓶颈。（3）修改smb.conf在[global]下添加： read raw = yes write raw = yes aio read size = 0 #关闭异步读取 aio write size = 0 #关闭异步写入 large readwrite = yes由于测试时瓶颈是路由器，修改samba配置后读写速度还是11.5MB/s。（4）其他挂载的硬盘最好不用NTFS格式，若用的是NTFS注意使用ntfs-3g挂载（据说树莓派默认即ntfs-3g）。树莓派上格式化设备为指定格式：mkfs -t 格式 /dev/sdxx，也可以用其他工具。windows上可试试DiskGenius。据说换网线也可提升速度。当然，前提是瓶颈在网线；若路由器只支持百兆，换网线应该也没用。另外，有些设备可能用的还是比较老的samba协议，若要添加对老samba协议的支持，可以在smb.conf中的[global]下添加：min protocol = NT1NT1为Samba v1，SMB2、SMB3分别是v2、v3。最后，从windows10机器上向共享文件夹拷贝文件时会先生成等大的镜像，再写入数据；写入10GB的大文件时，由于镜像迟迟未创建完毕，windows上还出现了0x8007003B错误。所以目前还不能从windows机器上写入大文件。（5）后续使用希捷硬盘（exFAT格式），通过带USB3.0的硬盘盒连接到树莓派的USB3.0接口，树莓派和windows电脑都通过网线直连到路由器的1000M网口，smb.conf的[global]下添加相应内容。测试读写速度为80-100MB/s，写入单个文件、高峰时可达113MB/s，已经达到硬盘写入速度极限。（6）总结使用samba共享树莓派挂载的硬盘，可以实现Pi OS、windows间共享文件（android未测试），进行一些设置后甚至可达硬盘读写速度上限。但是从windows向硬盘写入10G以上的大文件时会报错（因为linux系统下写入一般是先创建空白镜像，而windows系统在这段时间内无法得到反馈，误以为写入操作卡死），算是一个缺陷。7.自动挂载硬盘修改/etc/fstab文件，添加一行：设备名 挂载点 硬盘格式 设置 是否备份 是否检测及检测顺序 /dev/sdxx /home/samba_user/shared/1 exfat defaults,uid=1003,gid=1004,nofail,auto,noatime 0 0（这里的挂载点为/home/samba_user/shared/1，而smb.conf中设置的是path = /home/samba_user/shared，两处不一致会导致windows下显示共享盘的大小为派的sd卡大小而不是硬盘大小，但是对实际使用几乎没问题。如果只有一张NAS硬盘就可以把两处改一样）其中uid和gid可通过id samba_user查看；设备名推荐换成UUID=xxxxxx，不会挂错地方。硬盘的UUID可插上硬盘后通过 blkid 或 ls -l /dev/disk/by-uuid 查看；关于nofail，auto，noatime的解析参考浅析 fstab 与移动硬盘挂载方法。最后，使用raspbian带桌面的系统，会发现系统识别到存储介质时会自动挂载，一般是在/media/pi/目录下，所属用户和组都是pi。若要关闭这个功能，需要以pi用户的身份登录桌面，在文件管理器中，左上角 编辑-偏好设置-卷管理 ，第一个选项是开机自动挂载，第二个选项是插入移动存储时自动挂载。8.使用qBittorrent离线下载也可以用aria2代替，但是据说aria2没有公网ip时下载较慢。以下参考了利用树莓派进行挂机下载。apt install qbittorrent qbittorrent-nox（下载qBittorrent-Enhanced-Edition可自动封禁迅雷吸血鬼ip，见上述参考文章）下载完成后，使用qbittorrent-nox即可开启qBittorrent的Web UI，在 树莓派ip:8080 可访问到网页qBittorrent，十分方便。默认用户密码为admin和adminadmin，在 设置-Web UI 中修改。接下来把qbittorrent-nox注册到开机自启：nano /etc/systemd/system/qbittorrent-nox.service服务文件中写入以下内容：[Unit] Description=qBittorrent-nox_WebUI After=network.target [Service] User=root Type=simple RemainAfterExit=yes ExecStart=/usr/bin/qbittorrent-nox -d ExecStop=/bin/kill $MAINPID Restart=on-failure RestartSec=5 StartLimitInterval=0 [Install] WantedBy=multi-user.target允许自启，并开启qBittorrent-nox：systemctl enable qbittorrent-nox systemctl start qbittorrent-nox附录：小米手机开启FTP服务器在 文件管理器-分类-远程管理 中可开启FTP服务器，设置里还可以修改连接的用户名和密码。开启后手机显示 ftp://手机ip:2121 ，在windows的资源管理器输入 ftp://手机ip:2121 、用户名密码 即可查看手机文件。测试了一下，写入速度1.9-2.5MB/s，读取速度3MB/s，勉强能用。后续：更换为千兆路由器后，写入速度在16-40MB/s，提升明显。