步骤/目录： 0.需求介绍与准备 1.延时摄影 （1）安装摄像头 （2）开启摄像头权限 （3）32位Pi OS （4）64位Pi OS （5）一些优化 （6）crontab设置 2.其它本文首发于个人博客https://lisper517.top/index.php/archives/83/，转载请注明出处。本篇文章的目的是使用树莓派进行延时摄影。本文实验日期为2023年5月1日。使用的是树莓派4B（内存8G版），系统为Pi OS 64位桌面版（2022年4月4日更新，镜像名 2022-04-04-raspios-bullseye-arm64.img ）。本文主要参考了 这篇文章 和 这篇文章 。0.需求介绍与准备需要对种植的植物进行延时摄影，观察生长情况。购买树莓派官方摄像头，大概是pi camera 2，购买摄像头支架。另外树莓派最好使用UPS供电，连接到wifi。1.延时摄影（1）安装摄像头在树莓派断电状态下将摄像头排线接到树莓派。（2）开启摄像头权限在raspi-config中找到Interface Options -> Legacy Camera（Legacy Camera也可能只是Camera），允许打开摄像头。重启后，可以找到/dev/video0这个设备，说明摄像头已准备就绪。（3）32位Pi OS在32位版本的Pi OS里，自带了raspistill命令用于拍照、raspivid用于视频，不过发布64位的Pi OS以后官方已开始慢慢舍弃这2个命令（也可能是64位系统还未加入这些非核心功能？）。树莓派使用摄像头的方法主要有3种：a.raspistill、raspivid命令，32位系统b.python，picamera库c.python，opencv库如果你使用的是32位的Pi OS并安装了python，可以如下使用picamera库来调用摄像头（笔者未实践）：pip install picamera mkdir ~/mybash/python nano ~/mybash/python/time_lapse_photo.py写入如下内容：#!/usr/bin/python from picamera import PiCamera if __name__=="__main__": camera=PiCamera() camera.capture("/temp/test.jpg")如果运行正常，应该可在/temp下找到test.jpg，之后使用crontab定时即可。（4）64位Pi OS笔者使用的是64位系统，from picamera import PiCamera时出现如下报错：OSError: libmmal.so: cannot open shared object file: No such file or directory据说这是因为这个.so文件没有64位的版本。不过也可以使用opencv库来操作摄像头：apt install python3-opencv mkdir ~/mybash/python nano ~/mybash/python/time_lapse_photo.py写入如下内容：#!/usr/bin/python import cv2 as cv # 读取设备 cap = cv.VideoCapture('/dev/video0', cv.CAP_V4L) # 设置分辨率 cap.set(cv.CAP_PROP_FRAME_WIDTH, 1920) cap.set(cv.CAP_PROP_FRAME_HEIGHT, 1080) # 截图 ret, frame = cap.read() if ret: cv.imwrite('/temp/test.jpg', frame) # 必须释放摄像头 cap.release()此时可以在/temp目录下找到test.jpg，说明已经可以使用摄像头了。（5）优化及定时运行延时摄影在后期处理时需要使用图片生成视频。在参考文章中提到摄像头在晚上拍摄的图片太黑看不清，笔者修改后的python脚本如下（需要pip install numpy）：#!/usr/bin/python import cv2 as cv import numpy as np import time, os, shutil RESO = (1920, 1080) #图片和视频分辨率 IMG_DIR = r'/temp/test_photoes' #图片存放目录 IMG_DIR_NAS = r'/temp/photoes' #NAS上图片存放目录 IMG_DIR_ORDERED = r'/temp/ordered' #排序后图片存放目录 VIDEO_DIR = r'/temp/test_videoes' #视频存放目录 NAME_FORMAT = '%Y-%m-%d-%H-%M-%S' #命名时采用的时间格式 #以上的全局参数自行修改 class camera(): """摄像头类，实现了拍照、视频等功能。""" def __init__(self) -> None: """读取设备，设置分辨率""" cap = cv.VideoCapture('/dev/video0', cv.CAP_V4L) cap.set(cv.CAP_PROP_FRAME_WIDTH, RESO[0]) cap.set(cv.CAP_PROP_FRAME_HEIGHT, RESO[1]) self.cap = cap @staticmethod def light_balance(img): """对图片进行光照处理，主要是提高夜间能见度""" img = cv.normalize(img, dst=None, alpha=255, beta=30, norm_type=cv.NORM_MINMAX) return img def take_a_pic(self, img_name: str = '', light_balance: bool = False) -> str: """拍一张照片并存储。 :param img_name: 图片名称 :param light_balance: 是否进行光照处理 :return :图片绝对路径""" cap = self.cap ret, frame = cap.read() if img_name == '': img_name = '{}.jpg'.format(time.strftime(NAME_FORMAT)) img_fullpath = os.path.join(IMG_DIR, img_name) if ret: if light_balance == True: frame = camera().light_balance(frame) cv.imwrite(img_fullpath, frame) return img_fullpath def take_a_video(self, video_name: str = '', frames: int = 150, light_balance: bool = False) -> str: """录制一段视频并储存，可指定帧数。 :param video_name: 视频名称 :param frames: 视频总帧数 :param light_balance: 是否进行光照处理 :return : 视频绝对路径""" cap = self.cap fps = cap.get(cv.CAP_PROP_FPS) #设置摄像头的帧率。笔者的摄像头帧率为30 #如果要通过秒数来指定视频长度，可用秒数*帧率 if video_name == '': video_name = '{}.avi'.format(time.strftime(NAME_FORMAT)) video_fullpath = os.path.join(VIDEO_DIR, video_name) video = cv.VideoWriter(video_fullpath, cv.VideoWriter_fourcc('I', '4', '2', '0'), fps, RESO) #编解码器I420可输出avi格式视频 for i in range(frames): ret, frame = cap.read() if ret: if light_balance == True: frame = camera().light_balance(frame) video.write(frame) return video_fullpath @staticmethod def copy_delete_order_photoes() -> None: """将本机的文件夹上传到NAS后清空，然后对NAS上的图片进行排序、重命名。 """ dir_1 = os.path.dirname(IMG_DIR) + os.path.sep + '*' dir_2 = os.path.dirname(IMG_DIR_NAS) os.system('cp -r {} {}'.format(dir_1, dir_2)) dir_1 = IMG_DIR + os.path.sep + '*' dir_2 = VIDEO_DIR + os.path.sep + '*' os.system('rm {}'.format(dir_1)) os.system('rm {}'.format(dir_2)) for dirpath, dirnames, filenames in os.walk(IMG_DIR_NAS): if dirpath != IMG_DIR_NAS: #仅对第一层排序 break filenames.sort() for filename in filenames: src_abs_path = os.path.join(dirpath, filename) dst_abs_path = os.path.join( IMG_DIR_ORDERED, str(filenames.index(filename) + 1) + '.jpg') shutil.copy(src_abs_path, dst_abs_path) def __del__(self) -> None: self.cap.release() #释放摄像头 if __name__ == '__main__': cam = camera() print(cam.cap.get(cv.CAP_PROP_FPS)) #打印帧数 cam.take_a_pic(light_balance=True) start_time = time.time() cam.take_a_video(frames=150, light_balance=True) print('{:.2f}'.format(time.time() - start_time)) #观察视频写入所需时间 需要注意的是，在1920*1080的分辨率下，图片或视频都比较大，笔者实验时一张图片520-600KB，一段150帧（5秒）的视频445MB，而笔者树莓派4B使用的TF卡读取、写入速度分别为100MB/s、10MB/s。之前笔者组建过家庭内的NAS，如果视频不在树莓派上储存而是通过wifi传到NAS需要55s左右。笔者实际使用时没有用到视频功能，如果需要使用可以适当降低分辨率。在实际环境中，把存储目录换一下，修改主程序如下：if __name__ == '__main__': cam = camera() hour = time.localtime().tm_hour minute = time.localtime().tm_min if hour >= 5 and hour <= 21: cam.take_a_pic(light_balance=False) elif hour == 0 and minute == 20: cam.take_a_pic(light_balance=True) camera.copy_delete_order_photoes() else: cam.take_a_pic(light_balance=True)最后，使用crontab定时执行python脚本：crontab -e写入新的一行：*/20 * * * * python3 /root/mybash/python/time_lapse_photo.py每隔20min照一张照片。并且在每天的0:20，将树莓派上存储的图片、视频上传到NAS上，删除树莓派上的文件，并对NAS上的图片进行排序。排序是为了便于做延时摄影的视频。2.其它（1）将图片拼接为视频首先对图片进行排序，如上文所示。然后使用ffmpeg或者pr将图片转换为视频。以ffmpeg为例（ffmpeg的安装在 Python，爬虫与深度学习（17）——电影的信息爬取、下载与处理（二） 中稍提到过）：cd /temp/ordered ffmpeg -r 24 -f image2 -i %d.jpg test.mp4-r 24 指定视频帧率为24，也就是24张图片1s； -f image2 指定当前使用图像生成视频； -i %d.jpg 指定图片的名称，如果学习过C就会知道%d是整型的占位符。这样生成的视频比较朴素，可以使用各种视频剪辑软件加上BGM或者特效等。（2）将树莓派改造为监控摄像头使用树莓派的摄像头，还可以将树莓派改造为监控摄像头，可通过网页查看，也可用于直播等。这里给出一些参考文章：树莓派+Flask实现视频流媒体WEB服务器工业树莓派结合USB摄像头实现远程网络监控在树莓派上安装 ZoneMinder 实现摄像头监控基于树莓派 CribSense 的婴儿监视器用树莓派做 RTMP 流直播服务器，可推送至斗鱼直播树莓派制造日视/夜视串流直播摄像头
步骤/目录： 1.常用DNS 2.更换DNS 3.添加解析记录本文首发于个人博客https://lisper517.top/index.php/archives/37/，转载请注明出处。本文的目的是为树莓派更换DNS。本文实验日期为2022年8月27日。本文使用的树莓派为 4B 8GB 版，系统镜像是Pi OS的64位桌面版，名为 2022-04-04-raspios-bullseye-arm64.img 。1.常用DNSDNS，即 Domain Name Server（域名服务器），是可以将域名与IP进行转换的服务器。ip地址是xxx.xxx.xxx.xxx的格式、由数字组成，而我们访问网页时的域名显然更好记一些，但最终域名必须转换成ip，网络请求才能找到目的地，这就是DNS发挥作用的地方。DNS也需要通过ip访问，下面是一些常用的DNS（排名表示推荐程度）。国外：（1）Cloudflare DNS 1.1.1.1 1.0.0.1 2606:4700:4700::1111 #IPv6的DNS 2606:4700:4700::1001 （2）谷歌 8.8.8.8 8.8.4.4 2001:4860:4860::8888 2001:4860:4860::8844 （3）微软 4.2.2.1 4.2.2.2 （4）Norton ConnectSafe 199.85.126.10 199.85.126.20 199.85.126.30 199.85.127.10 199.85.127.20 199.85.126.30 （5）OpenDNS 208.67.222.222 208.67.220.220 （6）DNS Watch 84.200.69.80 84.200.70.40 （7）Comodo安全DNS 8.26.56.26 8.20.247.20 （8）威瑞信 64.6.64.6 64.6.65.6 （9）OpenNIC 192.95.54.1 192.95.54.3 （10）GreenTeamDNS 81.218.119.11 209.88.198.133国内：（1）114DNS（http://www.114dns.com/） 114.114.114.114 114.114.115.115 （2）腾讯（https://www.dnspod.cn/Products/Public.DNS） 119.29.29.29 182.254.118.118 （3）阿里（http://alidns.com/） 223.5.5.5 223.6.6.6 （4）百度（http://dudns.baidu.com/intro/publicdns/） 180.76.76.76 （5）CNNIC（http://www.sdns.cn/） 1.2.4.8 210.2.4.82.更换DNSDNS其实起到一个指路的作用，当你访问域名时指向对应的ip。但是有时候可能因为一些原因，DNS故意或无意间指错路了，就被称为 DNS污染 、 DNS劫持 ，这会让我们无法访问到正确的服务器，非常的危险。更换DNS为一些靠谱的DNS可以一定程度上规避这个问题。下面就以google的 8.8.8.8 和 8.8.4.4 为例，展示如何更换为这两个DNS。（0）路由器更换DNS登录路由器后台，一般都可以手动设置DNS，这样最方便。（1）windows系统在 设置 - 网络和Internet - 以太网 - （右上角）更改适配器选项 ，这里可以看到自己的网络连接，可能有无线、有线连接，看一下自己目前用的哪个网络，左下角没有红色叉的是正在使用的网络。对正在用的网络单击鼠标右键，点击 属性 ，点击下面的框里的 Internet协议版本 4 (TCP/IPv4) ，点击右下的 属性 - 常规 - 使用下面的DNS服务器地址 ，填入谷歌DNS。这实际上是设置了单个网络连接的DNS。如果同时使用有线、无线连接，最好把两个网络连接的DNS都设置一下；如果到新的地方、连接到新的网络，则也需要同样更改新网络连接的DNS。（2）linux系统（以树莓派为例）linux系统里一般都可以通过更改 /etc/resolv.conf 这个文件来更改DNS，具体的方法是在这个文件末尾加上：nameserver 8.8.8.8 nameserver 8.8.4.4并把原来的 nameserver 192.168.1.1 等之类的行删掉，或者也可以不删。但是这只是临时更改DNS，因为 /etc/resolv.conf 这个文件在每次开机后里面的内容都会由系统根据系统设置重写，所以开机后原来的设置就会丢失。不过一般这个文件前面的注释里都会提示如何永久更改DNS，像树莓派的这个文件，注释只有一行：# Generated by resolvconfresolvconf 是一个DNS相关的命令，用 whereis resolvconf 可以看到它的可执行文件位置是 /usr/sbin/resolvconf ，另外还可以看到有一个 /etc/resolvconf.conf 配置文件，实际上这个配置文件可以更改 resolvconf 这个命令的设置。进行以下操作：nano /etc/resolvconf.conf根据注释的提示，把其中：#name_servers=这一行改为：name_servers="8.8.8.8 8.8.4.4"然后用 reboot 重启树莓派，再次查看 /etc/resolv.conf ，发现前面增加了谷歌的两个nameserver，最后则是 192.168.1.1 （也可能是其他地址），即路由器的局域网地址。其他linux发行版也是大同小异， /etc/resolv.conf 这个文件的注释一般都会有提示，比如在 Docker实践——用于小诊所的开源医疗记录系统 这篇文章中就用到了OpenSUSE发行版，提示把 /etc/sysconfig/network/config 这个文件中的 NETCONFIG_DNS_STATIC_SERVERS="" 改成了 NETCONFIG_DNS_STATIC_SERVERS="8.8.8.8 8.8.4.4" 。实在不会的，可以自行用bing搜索。3.添加解析记录上面的方式可以更换DNS。有时想通过域名访问局域网的机器（比如想把 mycomputer.com 绑定到 192.168.1.10 ），也是可以做到的。（0）路由器设置解析记录有少部分路由器支持。如果刷了openwrt，也可以设置，这里不讲解了。（1）电脑设置如果路由器无法设置，可以找到hosts文件，它的路径：Windows下，在 C:\Windows\System32\drivers\etc\hosts Linux下，在 /etc/hosts按照里面的例子，添加一条解析记录即可。这样，在该电脑上，就能通过域名访问内网机器。
步骤/目录： 0.Pi Dashboard简介 1.LNP部署Pi Dashboard本文首发于个人博客https://lisper517.top/index.php/archives/36/，转载请注明出处。本文的目的是使用docker、docker-compose在树莓派上运行Pi Dashboard。本文实验日期为2022年8月27日。本文使用的是树莓派4B（内存8G版），系统为Pi OS 64位桌面版（2022年4月4日更新，镜像名 2022-04-04-raspios-bullseye-arm64.img ）。0.Pi Dashboard简介在windows系统中，可以用任务管理器便捷地查看电脑的CPU、显卡等使用情况；对于树莓派，这样的信息收集起来比较麻烦，好在有大神开发了Pi Dashboard，这是一个网页查看树莓派状态的工具，详细介绍在 https://make.quwj.com/project/10 。简单来说Pi Dashboard的部署和Typecho类似，也是放在nginx的文件夹里、搭配php使用，不同点是不需要mysql。1.LNP部署Pi Dashboard这里用docker-compose运行nginx、php服务，最后把Pi Dashboard放在nginx的网页根目录下即可。在树莓派上进行如下操作：mkdir -p /docker/pi_dashboard/conf/nginx mkdir /docker/pi_dashboard/www mkdir /docker/pi_dashboard/ssl_cert docker pull nginx:1.23.1 docker pull php:8.1-fpm nano /docker/pi_dashboard/conf/nginx/default.conf写入以下内容：server { listen 50443 ssl; server_name www.lisper517.top; ssl on; ssl_certificate /ssl_cert/.pem文件名; ssl_certificate_key /ssl_cert/.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 / { index index.html index.htm index.php default.html default.htm default.php; root /var/www/html; } location ~\.php$ { fastcgi_pass php:9000; fastcgi_param SCRIPT_FILENAME /var/www/html/$fastcgi_script_name; include fastcgi_params; } error_page 497 https://$host:50443$request_uri; }这是使用https的配置，注意更改 www.lisper517.top 和ssl证书的两个文件名。如果只使用http，则写入如下内容：server { listen 50443; location / { index index.html index.htm index.php default.html default.htm default.php; root /var/www/html; } location ~\.php$ { fastcgi_pass php:9000; fastcgi_param SCRIPT_FILENAME /var/www/html/$fastcgi_script_name; include fastcgi_params; } }但是要注意，无论使用http还是https，都尽量不要把这个页面开放到公网。然后编写compose模板：nano /docker/pi_dashboard/docker-compose.yml写入如下内容：version: "3.9" services: nginx: image: nginx:1.23.1 ports: - "50443:50443" volumes: - /docker/pi_dashboard/conf/nginx:/etc/nginx/conf.d - /docker/pi_dashboard/ssl_cert:/ssl_cert - /docker/pi_dashboard/www:/var/www/html logging: driver: syslog restart: always php: image: php:8.1-fpm volumes: - /docker/pi_dashboard/www:/var/www/html logging: driver: syslog restart: always无论http还是https都可以用这个compose模板。然后开启服务：cd /docker/pi_dashboard docker-compose config docker-compose up看一下日志是否正常。如果用https记得用WinSCP把证书放到正确的位置；然后到 Pi Dashboard 的github页 把压缩包下载下来（Code里面，Download ZIP），解压后用WinSCP放到树莓派的/docker/pi_dashboard/www目录下，最好不要放整个文件夹，而是把里面的文件放到该目录下；如果放整个文件夹的话，把这个文件夹改名为 pi-dashboard 。最后，浏览器访问 树莓派ip:50443 即可看到 Pi Dashboard ；如果放整个文件夹，则访问 树莓派ip:50443/pi-dashboard 。
Docker实践——用于小诊所的开源医疗记录系统
步骤/目录： 0.准备工作 （1）购买并设置树莓派 （2）购买并设置服务器 （3）准备软件 1.方法一：docker + openmrs 2.方法二：gnuhealth 的树莓派系统 3.方法三：gnuhealth 直接安装（无法使用） 4.了解：新致“开源”医疗 附录：通过公网访问EMR或HIS本文首发于个人博客https://lisper517.top/index.php/archives/29/，转载请注明出处。本文的目的是搭建一个可用于小诊所的电子病历系统，省去一笔开支费用。有些方法中会用到docker、docker-compose。本文实验日期为2022年8月7日。本文使用的云服务器为阿里云轻量应用服务器（杭州），系统镜像为 Docker 19.03；树莓派为 4B 8GB 版，系统镜像是Pi OS的64位桌面版，名为 2022-04-04-raspios-bullseye-arm64.img 。笔者有医学背景，最近需要一个电子病历系统。医疗方面的电子化，可分EMR（electronic medical record）和HIS（hospital information system），EMR侧重于电子病历，而HIS侧重于使用计算机技术对整个医院进行管理，HIS包括EMR，HIS也难得多。但是如果只是想拥有一个私人的电子病历系统，或者开小诊所想使用电子病历，那么可以考虑使用一些开源的EMR或HIS（界面都是全英文的，但是一直用就熟练了，可以输入中文）。当然，这里的电子病历比较简陋，只是起到一个记录、方便查阅（相较于纸质病历）的作用，一般不应起法律效力。如果是单纯的医生、并不懂计算机技术，可以只看这篇文章，遇到中间的注释不懂的，如果没有实际影响可以跳过不看。如果对计算机技术感兴趣的，可从笔者的第一篇文章看起，这里用到的所有技术都能通过之前的文章学会。最后需要说明，下文中的所有方法，是让EMR或HIS在树莓派（一种微型电脑）上运行，服务器只提供一个公网ip、方便公网访问，如果只是在局域网中用（比如说自己开诊所，把树莓派放到诊所里面运行）就可以不买服务器；如果是上班时间摸鱼，想把病人记在自己的私人病历上，而且上班地方的电脑能连接外网，这种情况就可以买一个服务器。另外，以下的这些方法仅供娱乐，如果真的自己开诊所，建议找一些免费的病历系统用，付费的其实也不贵，1000￥/年差不多了。0.准备工作准备工作中，需要购买并设置树莓派；购买服务器，对服务器进行一些初始设置；在自己的电脑上准备相关软件。下文中出现的一个或多个X，一般指代不同的变化的数字。（1）购买并设置树莓派树莓派的详细介绍可参考 树莓派型号、参数与作用 （笔者之前的文章，赶时间的可以不看），粗略来说树莓派是一种便宜的微型电脑，可以用来运行一些任务。购买渠道上，建议到淘宝搜索 树莓派4B ，购买内存4GB或8GB版的，8GB版的目前（2022年8月）淘宝最便宜的是900￥左右，另外需要购买一个树莓派外壳（挑一个外观好看的）、树莓派官方电源、2张64GB的sd卡（2张以备不时之需）、1个读卡器（用于电脑读取sd卡），大概总成本在1100￥（8GB版）或900￥（4GB版）。另外还应该有一台自己的笔记本或台式机（这里需要是装windows系统，因为有些软件就是windows下运行的）。收到东西后，组装一下，把树莓派装进外壳，插上 type C 电源线，此时先不插sd卡。现在要准备一些软件。到 这个网址 下载树莓派的官方刻录工具（用于把树莓派的操作系统写入sd卡），下载的文件名为 imager-1.X.X.exe ，其中 1.X.X 是版本号，选最新的版本，下载完成后安装；点击 这个网址 下载树莓派的操作系统，下载的文件名为 20XX-XX-XX-raspios-bullseye-arm64.img.xz ，然后用7z或rar等软件把它解压成 .img 文件。sd卡插上读卡器、连接到自己的电脑，打开官方刻录工具，操作系统选择 使用自定义镜像 ，找到刚才下载的镜像；sd卡选择插上的sd卡（如果同时连了其他U盘，注意不要选到这些其他的设备），点击右下角的齿轮图标（设置），勾选 开启SSH服务 ，在下面的 Set username and password 里设置一个自己喜欢的用户名及密码（后续还要设置很多的密码，笔者建议所有的地方都用同一个密码，这个密码要8个字符以上，同时包含大小写字母和数字），勾选 配置WiFi ，输入自己的wifi名和密码（如果用网线直连树莓派，则可以不设置wifi。推荐用网线直连），点击保存，点击烧录，等待一会儿。之后弹出sd卡（烧录工具会自动弹出sd卡）并拔下读卡器、sd卡，把sd卡插到树莓派上（方向不要插反了，有金手指的一面朝向树莓派板），连上电源即开机（树莓派没有开机键，连上电开机，如果断电就gg）。接下来使用putty连接到树莓派。首先查找一下树莓派的局域网ip，比如你家里的wifi是路由器开的，那么浏览器登录到路由器的管理页面（详见自己路由器的说明），应该有一个叫 raspberrypi 的设备，记下它的ip地址，一般是 192.168.1.XXX 。然后下载putty，到 putty官网下载页 下载名为 putty-64bit-0.XX-installer.msi 的文件，安装到自己的电脑上。安装好后，打开文件夹内的 putty.exe ，在 Host Name (or IP address) 下面的框中输入树莓派的ip（192.168.1.XXX），右下角点击 Open ，输入烧录镜像时设置的用户名和密码（密码不会显示）即可连接到树莓派上，显示输入命令的界面。以后每次都是这样连接的。下面对树莓派进行一些初始设置。在设置之前，需要进行一些说明。下文中的命令，应当从浏览器中复制，左键点击putty界面，单击鼠标右键即可粘贴（不要自己手动输入），每次只粘贴一行命令，然后按 enter键 执行这行命令。确认自己的网速没问题，在putty界面输入以下命令：sudo su root passwd root raspi-config第二个命令是设置root用户的密码，需要输入。自己设置一个好记的密码，可以和刚才烧录镜像时设置的密码一样，然后会让你再输入确认一次。设置好root的密码后，以后用 sudo su root 可能需要输入root的密码。执行第三个命令时，会进入一个设置页面。按上下移动，到 Advanced Options ，按 enter ，选 Expand Filesystem 。回到一开始的界面，按左右移动，选择右下角的 Finish ，会询问是否现在重启，选择 Yes ，这时putty界面会断开连接，关掉即可。等待一会儿后，重新用putty连接，进行如下操作：sudo su root apt-get update apt-get -y install vim apt-get -y upgrade如果提示中看到了 packagekitd ，则等上10min后再执行上面的命令。第四个命令可能要执行一会儿，可以先看后面（（2）购买并设置服务器）。执行完后，再输入以下的命令：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 curl -L https://github.com/docker/compose/releases/download/v2.10.0/docker-compose-`uname -s`-`uname -m` > /usr/bin/docker-compose chmod +x /usr/bin/docker-compose docker-compose -v rebootreboot 命令也是重启。putty会断开连接，关掉，等待1min左右，打开一个新的putty、重新连接。最后，需要设置一下root的登录（后面用WinSCP需要）：sudo su root vim /etc/ssh/sshd_config这里对vim命令进行说明。使用vim命令一般是要对文件内容进行更改，以这里为例，执行 vim /etc/ssh/sshd_config 后会进入展示文件内容的页面（这里是修改一个已存在的文件，所以会显示该文件的原有内容。后面涉及到新建文件，则会显示空的内容），键盘输入 i ，进入编辑模式，可以按方向键上下左右进行移动；然后对文件内容进行修改（这里的修改是复制内容到文件中，所以需要复制内容，点击putty界面，按 shift+鼠标右键 进行粘贴），修改完毕后，按 ESC ，再按 shift+: （这里的 + 表示按住shift键不放，然后按 : ，最后一起松开，后面同理），输入 wq （页面底部会显示 :wq），最后按 enter 即可保存文件并退出。在这个文件的第一行加上：PermitRootLogin yes按上面的方法保存，然后执行命令：service sshd reload rm /etc/localtime ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime date最后的 date 可输出当前时间，这里设置的是上海时间。另外，这里的第三条命令输入的路径较长，像这种路径在输入的时候可以不输入完，而是输入前几个字母后按 tab键 进行匹配，如果前几个字母没有重名的就会自动补全路径。至此，树莓派设置完毕。如果要关机，使用 shutdown now 命令，开机就是连上电源。最后，有时觉得命令行界面输出的内容太多了，可使用 clear 命令清屏。（2）购买并设置服务器购买服务器的主要目的是提供一个公网ip，这样就能从公网访问到自己的EMR或HIS了。如果确定自己只在局域网中用EMR或HIS，也可以只买树莓派。比较大的提供服务器的厂商有阿里云、腾讯云、百度云、华为云等，其中阿里云起步最早、技术最成熟、市场份额最大，价格要贵一些（又称套路云，因为第一年价格便宜、续费价格贵）；腾讯云起步晚一些、技术也不错，价格稍便宜；其他的厂商就不推荐了。总结一下，不差钱就买阿里云，否则就用腾讯云，但实际上两者相差不大。这里以阿里云为例，登录 阿里云官网 ，用支付宝、淘宝等阿里系（qq、微信就是腾讯系）的app扫码登录，在上方的 产品 中找到 热门产品 -> 轻量应用服务器 ，这里选择内地服务器（香港或海外服务器有时网络会有波动），选择一个配置的服务器，先买一个月的低配置服务器，以后再续费和升级。这里的服务器只是负责提供一个公网ip，所以服务器的配置主要看月流量、峰值带宽，一般来说小诊所，1T、5Mbps（原价100￥/月）应该够用了，仅限个人使用的话，400GB、3Mbps（原价60￥/月）也可以。购买之前还可以比比阿里云和腾讯云的价格，有时候赶上优惠活动还能便宜点。购买好服务器后，服务器的镜像选择 Docker ，确认后等待服务器初始化完成。在上方进入 控制台 -> 轻量应用服务器 ，这里是服务器的后台管理页面（或者直接访问 swas.console.aliyun.com ）。这里可以看到服务器的公网ip，就是下面的 47.xx.xx.xx （可能前两个数字会有不同），这个ip需要用到。点击这个服务器、进入详情页面，点击右上角的 远程连接 即可连接到服务器。连接到服务器后，需要进行一些初始设置。需要提前说明一些基础操作：如果远程连接后弹出来的是一整个黑色页面，则按 ctrl+C 、ctrl+V 可以正常进行复制粘贴；若弹出来一个小的黑色框，它和putty是一样的，即可以在自己的电脑上用 ctrl+C 复制文字，在黑色框中单击鼠标右键粘贴。另外，有时某些命令输入后需要确认（弹出一行英文，末尾一般是 [y/n] ），那么就输入 y ，然后按 enter ；有时也可能提示输入 yes ，那么就输入 yes 后 enter ；有些时候可能要输入密码、用户名之类的，也是输入后按enter，但注意这里输入密码时不会显示。下面对服务器初始设置，进行如下操作（有具体操作说明）：sudo su root passwd root第二条命令是设置root用户的密码，输入一个好记的密码，再输入一次确认密码。尽量确保这个密码不要丢失。然后进行如下操作：curl -L https://github.com/docker/compose/releases/download/v2.10.0/docker-compose-`uname -s`-`uname -m` > /usr/bin/docker-compose chmod +x /usr/bin/docker-compose docker-compose -v这一步的目的是安装docker-compose，成功的话会输出版本信息。最后需要开放服务器的8081端口。在云服务器的控制后台点进刚才购买的服务器，在左边的 安全 里有一个 防火墙 ，点击右上角的 添加规则 ，端口范围填 8081 ，然后确定即可。至此，服务器设置完成。最后，有时觉得命令行界面输出的内容太多了，可使用 clear 命令清屏。（3）准备软件这里准备的软件是安装在windows笔记本或台式机上的（用MAC的自求多福）。需要的软件有WinSCP、7z、SD Card Formatter。有时候需要在自己的电脑和服务器之间拷贝文件，对于大多数的windows系统用户，可以选择WinSCP。到 WinSCP的下载页 ，点击 DOWNLOAD WINSCP X.XX.X (XX MB) 下载并安装。在WinSCP的使用上，比如要连到自己的服务器，文件协议选 SCP ，主机名填 自己服务器的公网ip ，用户名为 root ，密码为刚才设置的root密码，点击登录即可；树莓派也选SCP，填其局域网ip，用户名也是root，进行登录。WinSCP连接后的默认界面，左边是自己电脑上的东西，右边是服务器上的东西，互相可以拖动文件拷贝；或者打开 我的电脑 也可往服务器上拷。7z则是一个解压或压缩文件的软件，下载地址在 https://www.7-zip.org/ ，选 Download .exe 32-bit x86 1.2 MB 这个即可，下载完后安装。SD Card Formatter是格式化sd卡的工具，在这篇教程中不会直接用到，但在日常使用中可能会用到。比如一张sd卡以前写入过操作系统，现在不需要了（可能是系统有问题），就用SD Card Formatter把sd卡格式化、写入其他操作系统。SD Card Formatter的下载地址在 https://www.sdcardformatter.com/download/ ，下载windows版的，安装即可。使用时选 quick format 。1.方法一：docker + openmrs这个方法应该要用树莓派。笔者实验时，试图把openmrs运行在云服务器上，但是2核服务器性能不够、跑不了openmrs，表现为卡在 Create OpenMRS tables 中的某一步；4核的服务器说不定可以，但是太贵了。openmrs是一个应用比较广泛的EMR了。openmrs提供了一个演示网页，访问 演示网页 即可试用openmrs。登录用户名密码为admin和Admin123，选择 Outpatient Clinic （门诊），注册病人（Register a patient）、点进病人详情后写一个 Visit Note ，即可体验写简单的门诊病历。下面开始在自己的树莓派上布置一个openmrs。另外，为了节约时间，建议从笔者这里下载后面会用到的东西，下载地址为 https://lisper517.top/resources/OpenMRS/all.zip ，将其下载后解压，里面文件的用途见下文。在 openmrs的下载页 ，提供了四种下载选择，其中 Reference Application - 2.12 - Standalone Edition 和 Platform 2.5.0 - Standalone Edition 是傻瓜包，安装即用（安装在windows系统上的），安装好后要自己安装扩展，不推荐用于实际环境； Reference Application 2.12 - Advanced Installation 和 Platform 2.5.0 - WAR (for Enterprise) 是推荐用于实际环境的，但是需要自己另外搭建数据库（mysql）、Java应用容器（指Apache Tomcat，其一般也可简称为Tomcat），搭建完成后也要自己安装扩展，不得不说openmrs搭建比较复杂。这里笔者使用的就是 Platform 2.5.0 - WAR (for Enterprise) 这个包，其下载速度非常慢，笔者提供的文件里的 openmrs.war 就是下载好的包，其大小为122MB。之后开始搭建，这里使用docker搭建运行openmrs。在树莓派上运行：mkdir -p /docker/openmrs/data/mysql mkdir -p /docker/openmrs/conf/mysql docker pull tomcat:7.0.109 docker pull mysql:8.0.29笔者试用两个Standalone Edition时发现都用的是tomcat:7.0.50，但是这个版本docker官方仓库没有，就用的tomcat:7里最新的7.0.109。不要用8~10版本的tomcat，可能有些小问题。另外需要说明，tomcat的/usr/local/tomcat目录大概可以理解为nginx的/var/www/html，之后将在tomcat容器的/usr/local/tomcat/webapps里放上刚才下载的war包。第一次运行时需要开一次容器复制一下/usr/local/tomcat里的文件，进行如下操作：docker run -itd --name tomcat --rm tomcat:7.0.109 docker cp tomcat:/usr/local/tomcat /docker/openmrs docker container stop tomcat mv /docker/openmrs/tomcat /docker/openmrs/www rmdir /docker/openmrs/www/webapps mv /docker/openmrs/www/webapps.dist /docker/openmrs/www/webapps注意要用webapps.dist代替webapps，否则看不到webapps里的网页。然后编写compose模板（这里对vim开头的命令做一个说明，不会的可以看）：vim /docker/openmrs/docker-compose.yml写入以下内容version: "3.9" services: tomcat: image: tomcat:7.0.109 ports: - "8081:8080" volumes: - /docker/openmrs/www:/usr/local/tomcat depends_on: - mysql logging: driver: syslog mysql: image: mysql:8.0.29 command: "mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci" environment: MYSQL_ROOT_PASSWORD: 123456 volumes: - /docker/openmrs/data/mysql:/var/lib/mysql - /docker/openmrs/conf/mysql:/etc/mysql/conf.d restart: always logging: driver: syslog其中 MYSQL_ROOT_PASSWORD: 123456 是指定数据库的root用户密码，可以把这个密码改的复杂一点，但是没必要记住，因为写在文件中了。另外，这里最好是mysql开启后再开启tomcat，笔者在tomcat服务里使用了 depends_on 来限定，并且tomcat没有设置 restart: always （这样树莓派开机的时候就只有mysql打开，而tomcat还没有打开），这是因为如果tomcat在mysql之前打开的话，openmrs可能会因为找不到数据库而以为自己没有初始化过，后面访问的时候会变成初始的页面，那么之前存储的病人数据虽然没丢，但是找回来也是有些麻烦的。保存完这个文件后，开启服务：cd /docker/openmrs docker-compose config docker-compose up第一次运行需要等待一两分钟，当没有新的日志输出时，浏览器访问 树莓派ip:8081 即可看到tomcat介绍页面（左上角粗体黑字 Apache Tomcat/7.0.109）。接下来把刚才下载的openmrs.war这个文件用WinSCP放到树莓派的/docker/openmrs/www/webapps里，然后tomcat日志会输出以下内容：tomcat_1 | Aug 09, 2022 1:29:00 PM org.apache.catalina.startup.HostConfig deployWAR tomcat_1 | INFO: Deploying web application archive [/usr/local/tomcat/webapps/openmrs.war] tomcat_1 | Aug 09, 2022 1:29:14 PM org.apache.catalina.startup.TldConfig execute tomcat_1 | INFO: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time. tomcat_1 | WARN - OpenmrsUtil.getRuntimePropertiesFilePathName(2128) |2022-08-09T13:29:17,146| Unable to find a runtime properties file at /usr/local/tomcat/openmrs-runtime.properties tomcat_1 | WARN - OpenmrsUtil.getRuntimePropertiesFilePathName(2156) |2022-08-09T13:29:17,225| Unable to find properties file: /root/.OpenMRS/openmrs-runtime.properties tomcat_1 | WARN - OpenmrsUtil.getRuntimeProperties(2089) |2022-08-09T13:29:17,227| Unable to find a runtime properties file. Initial setup is needed. View the webapp to run the setup wizard. tomcat_1 | WARN - OpenmrsUtil.getRuntimePropertiesFilePathName(2128) |2022-08-09T13:29:17,901| Unable to find a runtime properties file at /usr/local/tomcat/openmrs-runtime.properties tomcat_1 | WARN - OpenmrsUtil.getRuntimePropertiesFilePathName(2156) |2022-08-09T13:29:17,903| Unable to find properties file: /root/.OpenMRS/openmrs-runtime.properties tomcat_1 | WARN - OpenmrsUtil.getRuntimeProperties(2089) |2022-08-09T13:29:17,904| Unable to find a runtime properties file. Initial setup is needed. View the webapp to run the setup wizard. tomcat_1 | Aug 09, 2022 1:29:17 PM org.apache.catalina.startup.HostConfig deployWAR tomcat_1 | INFO: Deployment of web application archive [/usr/local/tomcat/webapps/openmrs.war] has finished in [17,598] ms最后一行说明openmrs应用已成功部署，这时浏览器访问 树莓派ip:8081/openmrs 即可看到openmrs初始化界面（版本2.5.0），这里讲一下初始设置：第一个页面语言选择 English （没有中文），第二个页面（选择安装模式）选 Advanced ，第三个页面（数据库相关设置）在 Database connection 里将 localhost 改为 mysql ，修改前：jdbc:mysql://localhost:3306/@DBNAME@?autoReconnect=true&sessionVariables=default_storage_engine=InnoDB&useUnicode=true&characterEncoding=UTF-8修改后：jdbc:mysql://mysql:3306/@DBNAME@?autoReconnect=true&sessionVariables=default_storage_engine=InnoDB&useUnicode=true&characterEncoding=UTF-8这里是因为用容器运行mysql，所以ip填容器名；另外下面选择No，Password填123456（或者自己改的其他密码）。第四个页面（数据库相关设置）第三个问题是填openmrs使用的数据库用户，选No、Password填123456（自建用户总是有一些错误，不如直接把数据库的root给openmrs，反正是容器），第五个页面不改动，第六个页面填两次Admin123（或者自己设置密码，这个密码需要记住，满足下面的4条要求即可，建议所有涉及密码的地方都用一个统一的密码），第七个页面不用填，第八个页面确认一下设置即可开始初始化（没什么好确认的），第九个页面显示初始化进度，需要等待大约6~10分钟。初始化完成后，网页显示登录页面，输入刚才设置的admin、Admin123（或其他密码），会进入Add On Manager页面，这里是管理扩展（modules，也称mod）的页面（用openmrs:2.4.3版本时第一次admin登录说不是admin、无权限管理扩展，这是自带扩展的一个bug；笔者教程中使用的是2.5.0版本，不会遇到这个问题）。现在还不能进入EMR，因为刚安装好的openmrs只是内核、还没有交互界面。openmrs官方mod站在 http://addons.openmrs.org ，上面的mod比较多。官方mod站中间上面点击RefApp 2.11，这里列出的扩展大概是功能比较齐全的，都装上就差不多了，但是这里下载也是太慢了，在笔者提供的压缩文件中，modules文件夹里面就是下载好的扩展。这里要安装的扩展比较多，先在putty里按一下 ctrl+C 停止openmrs，然后把这些扩展（omod文件）用WinSCP移动到 /docker/openmrs/www/webapps/openmrs/WEB-INF/bundledModules 目录下（这个目录下不应该出现文件夹，而只应该有omod文件），另外要注意该目录下已经有fhir2、owa、webservices.rest三个扩展，把它们3个删掉。移动完毕后，再次启动openmrs：cd /docker/openmrs docker-compose up等待大约8分钟后，不再有新日志输出，最后一行日志为：INFO: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.浏览器访问 树莓派ip:8081/openmrs ，可看到页面UI比较完善了。以用户密码admin、Admin123（或自己设置的密码），选择 Outpatient Clinic （门诊）登录系统并试用。这里讲一下openmrs的基础操作，主要是门诊病历书写：书写病历首先需要一个病人。如果是以前看过的病人，在 Find Patient Record 里可以找到；如果是新病人则需要注册，选择 Register a patient，按步骤填入病人信息（推荐middle name写中文，其他两个写拼音），注意有些信息可以不填。在 Find Patient Record 里找到病人后点击该病人，或者注册病人后自动跳转，都会来到这个病人的详情页，点击右侧 General Actions 里的 Start Visit 即可开始接诊，新建 Visit Note 即可开始写病历。这个病历比较简陋，只有诊断和病历主体；诊断方面，比如输入 pulp ，有一个牙髓根尖周疾病的诊断，可以添加为主诊断，然后添加第二、第三诊断，就是背这些疾病的英文名比较麻烦，也可以自己写中文诊断，诊断字典里没有的也可以；病历主体方面，需要自己准备病历模板。由于这个病历主要是自己能看懂，所以写一些关键的就行了，比如做根管时几个根管的长度；病人有没有全身疾病等。另外需要注意，openmrs里使用的时区和中国时区不同，笔者暂时没找到改时区的方法。一般openmrs里的时间比中国上海时间早8个小时。现在已经完成了openmrs的搭建。如果想在实际环境中使用这种方法，为了安全，可对openmrs进行备份，树莓派上输入：mkdir -p /backup/openmrs crontab -e加上一行：0 6-20/1 * * * /usr/bin/zip -r /backup/openmrs/backup.zip /docker/openmrs这样就在6点到20点之间，每小时对openmrs备份一次。如果某天openmrs不能进入，可用WinSCP把/backup/openmrs目录下的这个backup.zip备份文件复制到自己的电脑上后解压（建议每过几天就把这个文件转移到自己的电脑上一次），按前面 0.准备工作-（1）购买并设置树莓派 的方法重新准备一张写入树莓派系统的sd卡，在新的树莓派系统中使用 mkdir /docker 命令，然后把解压后的文件夹用WinSCP放到/docker目录下，最后输入下面两行命令：cd /docker/openmrs docker-compose start mysql docker-compose up即可，非常的简单。另外需要提醒，如果在实际环境中使用此方法，放到公网上的话（通过公网访问树莓派的方法见附录），建议晚上不开，需要写病历的时候再开（每次打开需要等待10min左右）：#这两行是关闭openmrs的命令 cd /docker/openmrs docker-compose stop #这三行是打开openmrs的命令 cd /docker/openmrs docker-compose start mysql docker-compose up -d注意打开的命令稍微有所变化， docker-compose start mysql 输入后建议等待至少2min后再输入 docker-compose up -d ，保证mysql服务在tomcat之前打开；使用 docker-compose up -d 时不会输出日志，也可以安全关闭putty。而且openmrs自带了4个其他用户，为了安全，建议在 System Administration - Manage Accounts 里把这4个用户 retire 掉。最后，其实openmrs在github上提供了 官方compose模板 ，但是比较难用，因为maven（一种包管理器）在国内下载太慢，感兴趣的可以通过 v2ray + v2rayA 连接到国外源试试（见 Docker实践——如何查阅wiki百科及V2Ray ）。官方compose的使用方法是点击 Code ，在下拉菜单中点击 Download ZIP ，下载后把zip包解压，改名为openmrs，在树莓派上用 mkdir /docker 创建一个目录，把这个openmrs文件夹放到/docker目录下，进行如下操作：cd /docker/openmrs docker-compose build docker-compose up即可安装openmrs，同样在浏览器中访问，只是把 树莓派ip:8081/openmrs 改成 树莓派ip:8080/openmrs 。但由于网络问题，多半无法用官方模板构建，笔者即使成功构建，也无法正常访问openmrs。到这里已经完成openmrs的介绍。如果想试试后面的方法，可使用如下命令把树莓派系统恢复到初始状态（先按 ctrl+C 再执行下列命令）：cd /docker/openmrs docker-compose stop docker container rm openmrs-tomcat-1 docker container rm openmrs-mysql-1 docker image rm tomcat:7.0.109 docker image rm mysql:8.0.29 docker network rm openmrs_default cd / rm -r /docker/openmrs若提示 Error: No such container: openmrs-tomcat-1 ，则把所有 - 换成 _ 试试。总结一下，openmrs比较简洁，可以根据需要自己安装不同的扩展、满足多种需要。但是openmrs需要自己下扩展，算是一个扣分项（不过笔者提供的扩展已经够用了）。可根据自己的情况决定是否要用openmrs。2.方法二：gnuhealth 的树莓派系统gnuhealth是一个开源的HIS系统，2008年由一个西班牙医生 Luis Falcón 创立。gnuhealth的安装可分为两种方法，一是系统镜像法，二是正常安装。gnuhealth也提供了一个试用的网页，地址为 http://federation.gnuhealth.org:8000/ ，用户名是admin，密码为gnusolidario。gnuhealth的使用将在后面说明。下面这两个网页提供给懂linux的读者，gnuhealth的安装说明在 https://en.wikibooks.org/wiki/GNU_Health/Installation ，embedded版的初步说明则在 https://en.wikibooks.org/wiki/GNU_Health/Embedded （这个基本没用）。若无法访问，可从笔者的网页备份查看，即 安装说明 和 embedded版说明 。方法二、三都主要参照了第一个网页。在 gnuhealth官网 上提供了树莓派版本的embedded系统镜像，可以将树莓派打造成一个HIS的服务器（调度中心）。它的用法和写入树莓派系统镜像是一样的，也是用烧录工具把一个img文件写入sd卡中，然后树莓派插上sd卡就完事。镜像的官网下载页面在 https://gnuhealth.org/embedded.html （这个网页可以不看），实际的树莓派4系统镜像下载页面在 https://www.gnuhealth.org/downloads/embedded/raspberry/rpi4/ ，下载.gz结尾的文件，解压得到img镜像（本文写作时，镜像名为 gnuhealth-3.6.4-rpi4-SD-opensuse-leap15.2-xfce.img ），用树莓派官方烧录工具写到sd卡（这一次只选镜像和sd卡，不用改设置）、插到树莓派上，树莓派插上网线开机。注意一定要插上网线，因为这里写入操作系统时没法设置wifi。开机大约3min后，同样是在路由器里找到树莓派的局域网ip，用putty连接（如果 connetion refused 就再等一会儿）。系统有两个用户，分别是root和gnuhealth，密码都是 freedom ，用root进入系统（后面每次重启都是用root进系统），进行以下操作：passwd root passwd gnuhealth vim /etc/sysconfig/network/config前两行命令是更改root和gnuhealth用户的密码，把密码改成自己的。第三行命令会打开/etc/sysconfig/network/config这个文件，其中有一行是 NETCONFIG_DNS_STATIC_SERVERS="" （按ESC，按 / 后粘贴内容，然后按enter，可以查找），把它改成：NETCONFIG_DNS_STATIC_SERVERS="8.8.8.8 8.8.4.4"这是因为gnuhealth没有设置DNS服务器（DNS可以把域名解析成公网ip），所以添加了google的两个DNS服务器。然后进行以下操作：reboot apt-get update apt-get upgrade -y reboot第三个命令可能要执行一会儿。重启、重新用putty连接后，输入以下命令：rm /etc/localtime ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime date apt-get install -y xrdp systemctl enable xrdp systemctl start xrdp前三条命令用于更换时区为上海，使用 date 命令可显示正确的时间。后三条命令是安装xrdp（远程桌面相关），接下来试试用远程桌面连接到树莓派，gnuhealth用的OpenSUSE，自带tigervnc，也就是Xvnc，开启时先设置一个密码：vncpasswd这个是tigervnc的密码，建议可以和root、gnuhealth用户的密码一样；需要输入两次，另外还会提示是否输入一个只能看的密码（用这个密码连接远程桌面时只能看不能动），这个可以不设置。然后在自己的笔记本或台式机上，按 win+R ，输入 mstsc 后执行，打开windows的远程桌面（或者在搜索框中，就是关机键旁边的放大镜图标中搜索远程桌面），输入树莓派的局域网ip，连接，选择 Xvnc ，用户名和密码就是gnuhealth（这里不要用root登录）和刚才设置的密码。桌面左下角第四个图标是 xterm ，打开后执行：/home/gnuhealth/.local/bin/gnuhealth-client就会打开gnuhealth的客户端，可以直接连入试用（大概看一下界面即可），密码是gnusolidario。这里的数据库是health36rpi，用户名是admin，需要记住。然后在windows机器上用对应的tryton客户端5.0.23版本查看，访问 tryton客户端5.0.23下载地址 ，安装后，使用方法和刚才一样，Host填 树莓派ip:8000，Database是health36rpi，User name为admin，输入密码登录。这里的gnuhealth版本比较老（ ls -al /home/gnuhealth 可以看到有个 gnuhealth-3.6.4 ，说明这个版本是3.6.4；另外这里用的tryton是5.0.23版，最新的已经到6.0.17版了），可以升级一下（下面的操作不建议跟做，因为最后有一些问题）。根据 gnuhealth升级教程 （或 升级教程备份网页），进行下面的操作：su root systemctl stop gnuhealth mkdir -p /backup/gnuhealth chown -R gnuhealth:users /backup/gnuhealth su gnuhealth ~/gnuhealth/tryton/server/util/gnuhealth-control backup --backdir /backup/gnuhealth --database health36rpi mv ~/gnuhealth ~/gnuhealth_3.6.4如果下载版本不同，可能 health36rpi 这里要改动。到这里为止就把老版本的gnuhealth进行了备份，接下来安装新版本的gnuhealth：cd ~ wget https://ftp.gnu.org/gnu/health/gnuhealth-latest.tar.gz tar -xzf gnuhealth-latest.tar.gz rm gnuhealth-latest.tar.gz ls -al笔者下载时，最新版本为 gnuhealth-4.0.4 ：cd gnuhealth-4.0.4 wget -qO- https://ftp.gnu.org/gnu/health/gnuhealth-setup-latest.tar.gz | tar -xzvf - mkdir ~/.pip vim ~/.pip/pip.conf在 pip.conf 中写入以下内容：[global] index-url = https://pypi.tuna.tsinghua.edu.cn/simple [install] trusted-host=mirrors.aliyun.com这步操作是把python源换成国内的。然后继续执行：pip install --upgrade pip bash ./gnuhealth-setup install cd .. source ~/.gnuhealthrc createdb test cdexe python3 ./trytond-admin --all --database=test本来这里的 --database= 应该用 health36rpi 或者 ghdemo40 的，由于笔者在使用原来的demo或者后来新的demo数据库时有些问题（缺各种module），所以这里用 createdb test 命令新建了一个新的test库（由于是空数据库，后面登录查看是完全没有数据的）。最后一个命令要运行几分钟，之后会要求输入一个邮箱，可以填自己的邮箱（也可以随便填）；设置两次密码，还是建议用通用密码。最后开启服务：su root systemctl start gnuhealth然后就可以用最新的tryton客户端查看了，比如在windows机器上访问 tryton客户端下载地址 ，安装后，使用方法和刚才一样，Host填 树莓派ip:8000，Database是test，User name为admin，输入密码登录，但是数据库是空的。gnuhealth的树莓派系统差不多就是这样使用，它的优点是使用较简单；缺点是扩展性差，比如不能装docker，因为这个发行版是OpenSUSE；但是docker对HIS功能完全没有影响，如果只用树莓派跑gnuhealth还是完全ok的；还有就是gnuhealth的升级（迁移旧的数据库）有些问题，笔者不知道如何解决，如果自己新建一个数据库又不知道怎么把它打造成和官方的demo一样；如果新的tryton客户端能支持访问老版本也好，可惜不能，只能用老版本的tryton客户端；或者好像也可以不用下tryton客户端、直接用网页访问gnuhealth。可惜gnuhealth在这三个方面都介绍的语焉不详，感觉并不是很方便。所以在下一种方法中，笔者试着从树莓派的官方镜像开始，搭建gnuhealth，可惜遇到了同样的问题。3.方法三：gnuhealth 直接安装（无法使用）按 （1）购买并设置树莓派 中的方法准备一个树莓派的操作系统，然后开始安装。根据 安装说明 ，gnuhealth需要的环境有：Operating system: GNU/Linux or FreeBSD for the server. RDBMS Database: PostgreSQL >= 10.x Document-oriented Database for Health Information System / Person Master Index: PostgreSQL :>= 10.x Python: >= 3.6 Gunicorn : 19.9 # WSGI HTTP服务器，用pip安装 Flask : 1.0 # python写的Web应用框架 Tryton 6.0 # python写的，提供登录和界面，以及客户端 Bash shell PIP for Python 3 # 要求用pip命令时是pip3但这里面只有postgresql是需要自己准备的，其他的东西要不就是不重要，要不就是树莓派系统自带，要不就是gnuhealth提供了一个安装脚本、可自动安装。唯一一个例外是Gunicorn，根据gnuhealth的wiki手册，gnuhealth默认使用的HTTP服务器是Werkzeug、使用时要下载tryton客户端，如果换成Gunicorn就可以从浏览器打开、不用装tryton客户端，可惜介绍的并不是很详细，笔者也没有兴趣继续探索（openmrs已经能完全满足笔者需要了），希望懂行的朋友可以在评论不吝赐教。用root用户登录树莓派，在树莓派上进行如下操作：adduser gnuhealth apt-get install -y postgresql postgresql-client service postgresql start su - postgres -c "psql -t -P format=unaligned -c 'show hba_file'"注意第二个命令执行时需要设置密码，另外还有 Full Name 这些东西（这些可直接按enter跳过）。最后一个命令会显示 /etc/postgresql/XX/main/pg_hba.conf （或者是 /var/lib/pgsql/data/pg_hba.conf ），记住数字（笔者实验时为13），备份并修改这个文件：su postgres cp /etc/postgresql/XX/main/pg_hba.conf /etc/postgresql/XX/main/pg_hba.conf_backup vim /etc/postgresql/XX/main/pg_hba.conf对没有注释的部分，改成下面这样：# TYPE DATABASE USER ADDRESS METHOD # "local" is for Unix domain socket connections only #local all all peer local all all trust # IPv4 local connections: host all all 127.0.0.1/32 trust # IPv6 local connections: host all all ::1/128 trust # Allow replication connections from localhost, by a user with the # replication privilege. local replication all peer host replication all 127.0.0.1/32 ident host replication all ::1/128 ident然后进行以下操作：su root service postgresql restart su - postgres -c "createuser --createdb --no-createrole --no-superuser gnuhealth" su gnuhealth cd ~ wget https://ftp.gnu.org/gnu/health/gnuhealth-latest.tar.gz tar -xzf gnuhealth-latest.tar.gz rm gnuhealth-latest.tar.gz ls -al这时应该会显示一些文件，其中有一个的名字为 gnuhealth-X.X.X ，笔者实验时的版本为 gnuhealth-4.0.4 ，下面以 gnuhealth-X.X.X 指代：cd gnuhealth-X.X.X ./scripts/demo/install_demo_database.sh 40 wget -qO- https://ftp.gnu.org/gnu/health/gnuhealth-setup-latest.tar.gz | tar -xzvf - mkdir ~/.pip vim ~/.pip/pip.conf这里的第二行命令是用gnuhealth自带的脚本下载demo数据库，其数据库下载网页为 这个网页 ，如果要下载其他的就把 40 改成 32 、 34 等。最后一行命令，也是修改一下python源，写入：[global] index-url = https://pypi.tuna.tsinghua.edu.cn/simple [install] trusted-host=mirrors.aliyun.com然后安装gnuhealthpip install --upgrade pip bash ./gnuhealth-setup install cd .. source ~/.gnuhealthrc createdb test cdexe python3 ./trytond-admin --all --database=test本来应该 --database=ghdemo40 ，但是demo数据库有些问题。同样用 createdb test 创建test数据库代替。这里也会要求输入一个邮箱，可以填自己的邮箱（也可以随便填）；设置两次密码，还是用通用密码。然后继续执行：su root vim /etc/systemd/system/gnuhealth.service写入：[Unit] Description=GNU Health Server After=network.target [Service] Type=simple User=gnuhealth WorkingDirectory=/home/gnuhealth ExecStart=/home/gnuhealth/start_gnuhealth.sh Restart=on-abort [Install] WantedBy=multi-user.target然后修改一下 start_gnuhealth.sh ：su gnuhealth vim /home/gnuhealth/start_gnuhealth.sh将最后一行的：python3 ./trytond $@ || bailout改成：python3 ./trytond $@ --logconf /home/gnuhealth/gnuhealth/tryton/server/config/gnuhealth_log.conf || bailout这是设置gnuhealth的log格式，在上一个方法中其实也可以这样写。最后开启gnuhealth：su root systemctl enable gnuhealth systemctl start gnuhealth同样在windows机器上访问 tryton客户端下载地址 下载tryton客户端，安装后，使用方法和刚才一样，Host填 树莓派ip:8000，Database是test，User name为admin，输入密码登录，但是数据库是空的。从树莓派系统搭建的gnuhealth扩展性强了，可以装docker，但是笔者同样不知道如何安装demo数据库、用gunicorn从网页访问gnuhealth。4.了解：新致“开源”医疗新致“开源”医疗的官网为 http://code.tntlinking.com/#/detail/1 ，打着开源的噱头，其实是商业公司开发的产品。它的官网是面对企业开发者的，需要关注微信公众号，手机号注册签约，并同意签约协议才能下载源码，协议内容如下：新致开源医疗项目商业合作协议 更新日期：2022年6月6日 新致开源医疗项目商业合作协议（下称“本协议”）包含您在经销新致开源医疗平台的商品或服务时所应遵守的条款和条件。本协议由武汉新致医卫信息技术有限公司（下称“本公司”或“我们”）与您或您所代表的实体（下称“用户”或“您”）签订。如果您代表某一实体签订本协议，您向我们声明并保证，您具有合法代表该实体之权限且有可约束该实体遵守本协议条款与条件之合法权限。除非我们另有通知，本协议自您接受之日起生效。您与本我们在下文中单独提及时会被称作一方，一同提及时会被称作双方。 根据《中华人民共和国民法典》的有关规定，经双方友好协商，本着长期平等合作，互利互惠的原则，为实现“新致开源医疗产品（HIS）商业发行版”（以下简称“发行版”）的商业推广和联合运营，创造良好的经济效益和社会效益，达成以下协议： 一、合作宗旨 促进国内普惠医疗数字化的发展，利用您广泛的市场资源优势，发挥我们医疗软件的研发能力，合作实现发行版的技术升级与市场营运。 二、合作范围 1、市场推广 1.1、双方共同对发行版进行推广、销售，包括但不限于直接或间接参加医院系统的招投标、代理招募和中小规模诊疗机构推广； 1.2、双方共同对发行版进行品牌力提升，包括但不限于参加国内各类重大医疗行业大会。 2、产品研发 2.1、推动发行版和其他系统进行兼容、集成； 2.2、对发行版进行迭代开发，遵循GPL开源协议（《GNU GENERAL PUBLIC LICENSE》）。 三、合作方式 1、您以现有的市场营销网络及社会资源为基础，进一步的开发市场潜力，逐步形成一个规范化且有区域影响力的营销网络； 2、您根据市场客群需求，推广发行版，收集反馈用户需求； 3、我们通过天天数链平台（www.tntlinking.com）提供经过确权的发行版源代码及相关资料库。 四、权力及义务 1、您的义务 1.1、您承诺并同意在发行版合作期内，必须遵守国家相关法律、正规合法运营及缴纳税收，如发生任何违法违纪行为所产生的一切法律责任及赔偿均由您独立承担； 1.2、您严格在我们许可下使用发行版，需在系统中保留“武汉新致医卫信息技术有限公司@2017开源授权”的字样； 1.3、根据商业化推广要求，您在与客户完成签约后10日之内，向我们报备客户相关信息，提供签章合同扫描件的关键页。 2、您的权利 2.1、我们授权您对发行版进行销售、修改，修改过程中需遵守本协议第四条4.1.2款约定； 2.2、您在发行版销售过程中，有权自主决定目标客户、销售价格、服务条款等相关内容。 3、我们的义务 3.1、提供给您的发行版拥有原始且完整的著作权； 3.2、对发行版产品进行持续研发和升级； 3.3、在合作过程中，对涉及到您的有关机密数据进行严格保密。 4、我们的权利 4.1、我们对您推广发行版的市场情况具有知情权； 4.2、我们对您使用发行版运营收益情况具有知情权； 4.3、我们可书面询问您上述两款的情况，您应于5个工作日内进行答复并对我们提出的疑义提供材料证明。 五、利益分配 1、分配规则 1.1、若报价以产品加实施的方式成交，则我们收取产品成交价的40%； 1.2、除上述5.1.1所述的报价方式外成交的，我们收取项目成交价的15%。 2、支付方式 2.1、您应在收到客户第一笔款后的7日内告知我们，在收到我们按照同比例额度开具的增值税专用发票（税率：6%）后30日内支付给我们； 2.2、您应在收到客户最后一笔款后的7日内告知我们，在收到我们按照剩余额度开具的增值税专用发票（税率：6%）后30日内支付给我们； 2.3、我们须保证所提供发票的真实有效合法性。您逾期告知或逾期付款的，每逾期一天，应按逾期款的1%（百分之一）向我们支付违约金，直至支付完毕之日止。逾期若达30日，我们有权单方面解除本协议。 六、其它 1、适用法律及管辖 本协议适用法律为中华人民共和国法律。您和平台同意，本协议签订地为中国上海市浦东新区。在合同履行过程中双方如发生纠纷，应协商解决；协商不成的，双方应向协议签订地有管辖权的人民法院以诉讼的方式解决。 2、声明 您在使用本公司提供的签约下载服务之前，请您务必审慎阅读、充分理解本协议各条款内容，包括但不限于免除或者限制责任的条款。如您不同意本服务协议及/或随时对其的修改，您可以主动停止使用新致公司提供的签约下载服务；您一旦使用本公司提供的签约下载服务，即视为您已了解并完全同意本服务协议各项内容，包括本公司对服务协议随时所做的任何修改，并成为我们的用户。该协议有几个关键点：（1）使用GPL开源协议。GPL要求必须开放源码，不能做成闭源的商业软件发布，但是可以做成开源的商业软件收钱。（2）如果开发者二次开发的产品卖出钱了，该公司收取40%或15%。（3）一旦同意该协议，公司后续若对该协议进行更改，也视为自动同意。或者也可以选择解约。光同意该协议还没完，后面需要输入公司信息等，所以这个“开源”系统不是面对个人开发者的，主要是卖钱的。这其实也是一个缩影。在github或sourceforge上可以找到国外真正开源的HIS，虽然别人打着援助落后地区的旗号、可能不适用于国内医院，但确实是真开源；国内的市场太小，也没有这种需求，难以找到真正免费开源的HIS。附录：通过公网访问EMR或HIS这里用到了frp，并且是通过docker运行frp。更详细的内容见 Docker树莓派实践——frp（附dashboard开启https、frp双向验证） 。在方法二中，系统镜像为OpenSUSE、装不了docker，则也可以在树莓派上直接安装frp，其下载地址为 https://github.com/fatedier/frp/releases 。在方法一中，openmrs使用的是树莓派的8081端口；在方法二、三中，gnuhealth使用的是8000端口。在购买的云服务器上进行如下操作（云服务器使用Docker镜像）：sudo su root mkdir -p /docker/frps/conf vim /docker/frps/conf/frps.ini写入如下内容：[common] bind_port = 7000 token = 你的密码 dashboard_port = 7500 dashboard_user = wuhuqifei dashboard_pwd = wuhuqifei kcp_bind_port = 7000token即密码，要设置的复杂一点；dashboard_user和dashboard_pwd也可自行更改。然后在云服务器的防火墙中打开7000、7500端口。注意，是在云服务器详情的 安全-防火墙 中新增两个个规则，打开这两个端口；另外，在刚才云服务的设置里，应该也打开了8081端口。然后在云服务器上继续操作：docker run -d --name frps -v /docker/frps/conf/frps.ini:/etc/frp/frps.ini --network host --restart=always snowdreamtech/frps之后可以查看 云服务器公网ip:7500 ，输入dashboard_user和dashboard_pwd即用户名和密码，查看frps的运行情况。在树莓派上进行如下操作：mkdir -p /docker/frpc/conf nano /docker/frpc/conf/frpc.ini写入如下内容：[common] server_addr = 云服务器公网ip地址 server_port = 7000 token = 你的密码 login_fail_exit = false [pi4B-http-1] type = tcp local_ip = localhost local_port = 8081 #如果是gnuhealth，则改成8000 remote_port = 8081然后执行：docker run -d --name frpc -v /docker/frpc/conf/frpc.ini:/etc/frp/frpc.ini --network host --restart=always snowdreamtech/frpc最后访问 云服务器公网ip:8081/openmrs ，即可从公网访问到自己的openmrs；或者在tryton客户端的Host填 云服务器公网ip:8081 ，可通过公网访问gnuhealth。
步骤/目录： 1.编写compose模板 2.运行并验证本文首发于个人博客https://lisper517.top/index.php/archives/25/，转载请注明出处。本文的目的是使用docker、docker-compose在树莓派上运行qbittorrent。本文实验日期为2022年8月1日。本文使用的是树莓派4B（内存8G版），系统为Pi OS 64位桌面版（2022年4月4日更新，镜像名 2022-04-04-raspios-bullseye-arm64.img ）。之前的文章中曾提到过qbittorrent，详见 树莓派+Samba+frp搭建NAS（网络云存储） 。简单来说，qbittorrent的作用是离线下载，一般搭配NAS使用。在docker官方仓库网页进行搜索，最后笔者选用的是linuxserver/qbittorrent镜像，其下载量为100M+，一个月前更新。1.编写compose模板如前所述，笔者使用树莓派+samba搭建了NAS，挂载一块硬盘到分享目录，路径为/home/samba_user/shared。如果没有NAS也无所谓，但由于树莓派本身存储较小，最好还是外接存储。另外，samba目前在官方仓库没有下载量大的官方镜像，所以仍然是直接在本地跑。在树莓派上进行如下操作：docker pull linuxserver/qbittorrent mkdir -p /docker/qbittorrent/conf mkdir /docker/qbittorrent/ssl_certificate id samba_user #这里的samba_user是为samba创建的系统用户 #输出uid=1002、gid=1003 nano /docker/qbittorrent/docker-compose.yml写入以下内容：version: "3.9" services: qbittorrent: image: linuxserver/qbittorrent environment: - PUID=1002 - PGID=1003 - TZ=Asia/Shanghai - WEBUI_PORT=8080 volumes: - /docker/qbittorrent/conf:/config - /home/samba_user/shared/DLs:/downloads - /docker/qbittorrent/ssl_certificate:/ssl_certificate ports: - "8080:8080" - "6881:6881" - "6881:6881/udp" restart: unless-stopped这里需要解释一下docker-qbittorrent的注意事项。PUID、PGID用于指定一位宿主机系统用户，该用户需要拥有宿主机的下载目录（本例中为/home/samba_user/shared），这样才不会有读写权限的问题，使用 id 用户名 可查看uid、gid。2.运行并验证cd /docker/qbittorrent docker-compose config docker-compose up若要后台运行docker-frpc，可使用docker-compose up -d命令。这时使用ufw打开8080、6881端口，即可浏览器登录到 树莓派ip:8080 ，出现qbittorrent网页UI，初始用户名密码为admin和adminadmin。在顶部工具栏下方，右边的齿轮图标为设置。接下来对qbittorrent进行一些初始设置：在 Web UI 中将语言改为中文；在 下载 中，将 默认Torrent管理模式 改为自动，勾选 保存未完成的torrent到 、 复制.torrent文件到（这两个目录自选），下面的 下载完成时发送电子邮件到 用的也是SMTP、有需要的可以打开， Torrent完成时运行外部程序 也可根据自身需要打开；在 速度 中，可以选择下载速度限制；在 BitTorrent 中，可调整最大同时下载数；在 Web UI 中，可设置使用HTTPS，修改用户名密码。
步骤/目录： 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 文件即可。用一些网页工具可以转换 .ico 文件。（7）使用其它主题typecho自带的主题其实已经足够使用，笔者一向认为内容大于形式，但是看久了总会审美疲劳。很多人为typecho开发了各种主题，本文介绍一下 Joe ，搭建后的效果参考 这个演示站点 ，比较花里胡哨。到 Joe 的项目地址下载zip包，放到 /docker/typecho/conf/typecho/app/usr/themes 目录，然后就可以使用了。需要注意的是，由于采用了Joe的那一套php页面，所以需要再次手动添加目录索引、备案号。（8）升级版本根据 官方文档 和 这篇文章 ，
步骤/目录： 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常识与基础。步骤/目录： 一、初识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.高级网络配置这一节属于进阶内容。详见高级网络配置。