当前位置: 首页 > article >正文

dockerdockerfiledocker-compose操作nginx

zzhua/spring-boot-docker-basic - dockerfile
zzhua/spring-boot-docker-demo - docker-compose

[启动容器操作]
# 只打印了信息后,容器就停止了。
# 然后,再使用docker start 容器id,容器又停止了
docker run hello-world

# 以交互模式启动一个容器,在容器内执行/bin/bash命令
# 然后,exit会导致这个容器直接停止;ctrl+p+q 退出不会导致这个容器停止
# 退出容器后,使用docker exec -it 容器id /bin/bash 再次进入容器内部,exit 退出容器 不会导致这个容器停止
docker run -it centos:7 /bin/bash 

# 以交互模式启动一个容器, 会直接进入到了容器内部
# 然后,exit会导致这个容器直接停止;ctrl+p+q 退出不会导致这个容器停止
# 退出容器后,使用docker exec -it 容器id /bin/bash 再次进入容器内部,exit 退出容器 不会导致这个容器停止
# docker logs 容器id 查看也没有日志输出
docker run -it centos:7

# docker ps -a 进行查看, 会发现容器已经退出
# Docker容器后台运行,就必须有一个前台进程
docker run -d centos:7

# 以下2个都可以,
# docker ps -a 进行查看, 会发现容器还在运行
docker run -it -d centos:7
docker run -it -d centos:7 /bin/bash

# 前台交互式启动
# 此时,会输出日志
# ctrl + c 会导致当前容器停止
# 需要 ctrl+p+q退出,不会导致这个容器停止,
# 如果停止了,需要使用docker start 容器id 来启动容器
# 重新启动容器后,使用docker attach 容器id 进入容器内部,ctrl+c 停止输出日志 会导致这个容器停止
# 重新启动容器后,使用docker exec -it 容器id /bin/bash 进入容器内部,exit 退出容器 不会导致这个容器停止
docker run -it redis:6.0.8

# 后台守护式启动
# docker ps -a 进行查看, 会发现容器还在运行
# 启动后,不要使用docker attach 容器id 进入容器内部
# 启动后,使用 docker exec -it 容器id /bin/bash 进入容器内部,exit 退出容器 不会导致这个容器停止
docker run -d redis:6.0.8	

# 启动1个容器,并进入终端
# 进入后,exit退出容器 会导致这个容器停止
# 进入后,需要 ctrl+p+q退出,不会导致这个容器停止
# 然后,使用docker exec -it 容器id /bin/bash 进入容器内部,exit 退出容器 不会导致这个容器停止
docker run -it ubuntu /bin/bash

# 启动1个容器,并返回1个容器id
# docker ps -a 进行查看, 会发现容器还在运行
docker run -it -d ubuntu

# docker ps -a 进行查看, 会发现容器已经退出
# Docker容器后台运行,就必须有一个前台进程
docker run -d ubuntu

[阿里云镜像操作]
# 将指定容器id提交为镜像
# 相当于docker export之后,再docker import
# 相当于docker export之后,再docker import
docker commit 0fd0503d058f zzhua/my-alpine:1.0

# 客户端登录阿里云镜像仓库
# 然后输入密码
docker login --username=1126shuangshuang crpi-hugadtddf72petnn.cn-shenzhen.personal.cr.aliyuncs.com

# 将本地指定镜像id 打上新的标签
# 格式是 镜像源地址/命名空间/镜像仓库:标签名
docker tag d716099f1afe crpi-hugadtddf72petnn.cn-shenzhen.personal.cr.aliyuncs.com/zzhua173/my-alpine:1.0

# 将本地的镜像 推送到阿里云镜像仓库
# 此时,可以在阿里云镜像仓库中看到此镜像了
docker push crpi-hugadtddf72petnn.cn-shenzhen.personal.cr.aliyuncs.com/zzhua173/my-alpine:1.0

# 拉取阿里云镜像
docker pull crpi-hugadtddf72petnn.cn-shenzhen.personal.cr.aliyuncs.com/zzhua173/my-alpine:1.0

[私有镜像仓库操作]
# 拉取docker私有库镜像
docker pull registry 

# 会启动1个镜像仓库容器
# 当目录不存在时,会自动创建
# 启动后,可查看镜像列表 curl -XGET http://119.23.61.24:5000/v2/_catalog
# --privileged=true扩大容器的权限解决挂载目录没有权限的问题
docker run -d -p 5000:5000 -v /zzyyuse/myregistry/:/tmp/registry --privileged=true registry

# 给指定镜像重新打标签
docker tag my-alpine:1.0 119.23.61.24:5000/my-alpine:1.0

# 推送前需要修改客户端的/etc/docker/daemon.json 添加上"insecure-registries":["119.23.61.24:5000"]
# 然后重启docker
# systemctl daemon-reload
# systemctl restart docker
# 推送到私有库中后,就可以查看到了 curl -XGET http://119.23.61.24:5000/v2/_catalog
docker push 119.23.61.24:5000/my-alpine:1.0

# 从私有库中拉取镜像
docker pull 119.23.61.24:5000/my-alpine:1.0


[挂载目录]
# 挂载后,可通过docker inspect 容器id 查看Mounts 看到挂载的设置
# 默认挂载目录后面省略了:rw, 也可以设置为:ro表示容器不能对该目录写入数据, 而只能宿主机写入
docker run -it --name myu3 --privileged=true -v /tmp/myHostData3:/tmp/myDockerData3 ubuntu /bin/bash
docker run -it --name myu4 --privileged=true -v /tmp/myHostData4:/tmp/myDockerData4:ro ubuntu /bin/bash

# --volumes-from 表示继承已有容器的卷规则
docker run -it --name myu5 --privileged=true --volumes-from myu3  ubuntu /bin/bash


# 基于alpine构建1个容器,创建/data目录,并且在/data目录下创建a.txt,data-item文件夹(此文件夹下在创建1个a.txt),创建/data2目录
docker run -it --name my-alpine alpine
# 将构建的这个容器提交为镜像 zzhua/my-alpine:1.0
docker commit 4f1e0f221e55 zzhua/my-alpine:1.0
# 测试挂载目录:会把容器的/data目录中的原有内容给清空掉
docker run -ti -d -v /my-alpine/data:/data zzhua/my-alpine:1.0
# 测试挂载文件:
# 首先宿主机的/my-alpine/data2/data.txt文件必须存在,如果不存在会报错,并且会开启1个容器,但容器会停止,并且会在宿主机上创建/my-alpine/data2/data.txt作为目录;
# 重新开始测试,在/my-alpine/目录下创建data.txt文件,然后执行下面的命令,它会把原有容器的文件内容给覆盖掉
docker run -ti -d -v /my-alpine/data.txt:/data2/data.txt zzhua/my-alpine:1.0


#拉取tomcat8镜像
docker pull billygoo/tomcat8-jdk8

# 此时,直接输出了日志,按ctrl + p + q退出,不会导致这个容器停止
# 此时,可以通过 http://119.23.61.24:8080/ 访问到tomcat的页面了
docker run -it -p 8080:8080  billygoo/tomcat8-jdk8

# 运行tomcat容器,返回容器id
# 此时,可以通过 http://119.23.61.24:8080/ 访问到tomcat的页面了
docker run -d -p 8080:8080 --name mytomcat8 billygoo/tomcat8-jdk8

# 这样挂载目录会把容器中的/usr/local/tomcat/webapps目录下的原有内容都给清空掉
docker run -d -p 8080:8080 --name mytomcat8 -v $PWD/webapps:/usr/local/tomcat/webapps billygoo/tomcat8-jdk8

# 这样可以在 /var/lib/docker/volumes/tomcat8-webapps/_data目录下可以看到/usr/local/tomcat/webapps目录下的内容还保留着
# 这个目录可以通过docker inspect 容器id 查看Mounts知道
# 可以通过docker volume ls 查看到挂载的所有的卷
# 可以通过docker volume inspect tomcat8-webapps 查看到指定的卷挂载信息
docker run -d -p 8080:8080 --name mytomcat8 -v tomcat8-webapps:/usr/local/tomcat/webapps billygoo/tomcat8-jdk8

# 注意当宿主机中的/tomcat目录下没有RUNNING.txt这个文件时, 这样执行会报错,并且执行完成后会在宿主机的/tomcat下创建1个RUNNING.txt目录
# /*docker: Error response from daemon: failed to create task for container: 
#   failed to create shim task: OCI runtime create failed: runc create failed: 
#   unable to start container process: error during container init: 
#   error mounting "/tomcat/RUNNING.txt" to rootfs at "/usr/local/tomcat/RUNNING.txt": 
#   mount /tomcat/RUNNING.txt:/usr/local/tomcat/RUNNING.txt (via /proc/self/fd/6), 
#   flags: 0x5000: not a directory: unknown:Are you trying to mount a directory onto a file (or vice-versa)? 
#   Check if the specified host path exists and is the expected type.
# */
# 而当宿主机中的/tomcat目录下有RUNNING.txt这个文件时, 这样执行不会报错,并且会把容器中的这个文件给覆盖掉,并且相互同步
docker run -d -p 8080:8080 --name mytomcat8 -v /tomcat/RUNNING.txt:/usr/local/tomcat/RUNNING.txt billygoo/tomcat8-jdk8

[安装mysql]

#拉取mysql5.7镜像
docker pull mysql:5.7

# 启动1个mysql实例
# 启动之后,即可使用本地的navicat连接上
docker run --name mysql01 -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql:5.7

# 进入容器内部
# 执行mysql -uroot -p123456 进入mysql
docker exec -it mysql01 bash

# 可以查看到mysql01容器的日志
docker logs mysql01

# 指定配置文件 /etc/mysql/my.cnf(默认没有这个文件) /etc/mysql/conf.d/(默认空文件夹) /etc/mysql/mysql.conf.d/(默认空文件夹)有读取优先级
#              在/etc/下有my.cnf文件,里面指定了datadir=/var/lib/mysql,!includedir /etc/mysql/conf.d/,!includedir /etc/mysql/mysql.conf.d/
# 指定数据目录 /var/lib/mysql。执行下面的命令后,容器中的/var/lib/mysql和宿主机的/mysql/data中都有文件了,此时查看文件创建时间说明这个文件是容器运行时生成的,否则容器中的/var/lib/mysql目录下的所有文件会被清空才对
# 在宿主机的/mysql目录下创建1个conf目录,并在此conf目录下创建my.cnf配置文件,配置文件中配置[mysqld]port=3308
# 以下命令会启动1个mysql容器
# 然后可以本地使用navicat可以连接上mysql证明这个配置是生效了
# 其它的就是需要熟悉my.cnf配置文件了
# 然后创建表插入数据,之后删除这个容器,然后重新使用下面命令创建1个新容器,navicat连上去后发现数据还在
docker run \
--name mysql01 \
-p 3306:3308 \
--privileged=true \
-v /mysql/conf:/etc/mysql/conf.d/ \
-v /mysql/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:5.7

# 在宿主机的/mysql目录下创建1个conf目录,并在此conf目录下创建my.cnf配置文件,配置文件中配置[mysqld]port=3308
# 以下命令会启动1个mysql容器
# 然后可以本地使用navicat可以连接上mysql证明这个配置是生效了
# 进入容器内部,发现多了/etc/mysql/my.cnf配置文件,并且这个配置文件配置的端口生效了
docker run \
--name mysql01 \
-p 3306:3308 \
--privileged=true \
-v /mysql/conf/my.cnf:/etc/mysql/my.cnf/ \
-v /mysql/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:5.7

# 在宿主机的/mysql目录下为空时,执行如下命令
# 执行的结果是 在宿主机会创建1个/mysql/conf/my.cnf/这个文件夹,容器中也会创建/etc/mysql/my.cnf/这个文件夹
# 因为容器中不存在/etc/mysql/my.cnf这个配置文件
docker run \
--name mysql01 \
-p 3306:3308 \
--privileged=true \
-v /mysql/conf/my.cnf:/etc/mysql/my.cnf \
-v /mysql/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:5.7

# 在宿主机的/mysql目录下为空时,执行如下命令
# 执行的结果是 会创建容器,但是会报错,因为容器中存在/etc/my.cnf这个文件,而宿主机中不存在,所以会在宿主机中创建/mysql/conf/my.cnf/这个文件夹
docker run \
--name mysql01 \
-p 3306:3308 \
--privileged=true \
-v /mysql/conf/my.cnf:/etc/my.cnf \
-v /mysql/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:5.7

# 如果要覆盖容器中的/etc/my.cnf这个配置文件,正确的做法是先把容器中的配置文件给弄出来,建1个/mysql/conf/my.cnf配置文件
# 然后执行下面的命令将容器中的/etc/my.cnf这个配置文件给覆盖掉
docker run \
--name mysql01 \
-p 3306:3308 \
--privileged=true \
-v /mysql/conf/my.cnf:/etc/my.cnf \
-v /mysql/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:5.7


[安装redis]
# 拉取redis6镜像
docker pull redis:6.0.8

# 启动1个redis实例
# 启动之后,即可使用本地的redis-destop连接上
# 查看当前正在运行的redis所使用的配置 config get * , config get bind, config get dir
docker run -d -p 6379:6379 --name redis01 redis:6.0.8

# 启动1个redis实例
# 并在启动时指定配置
# docker exec -it 32 bash进入容器,
# 一进入redis,当前目录就是/data,然后在当前目录启动redis,而redis.conf配置文件默认配置的dir是./,所以这里挂载到容器的/data目录
# 执行 redis-cli -p 6380 进入redis, config get port查看配置
docker run -d -p 6379:6379 --name redis01 redis:6.0.8 redis-server --save 60 1 --loglevel warning --port 6380

# 在/redis下创建conf目录,在conf目录下创建redis.conf配置文件,端口改为6380,dir 改为 /data,配置好后再使用如下命令启动redis
# 就是在容器启动后使用指定的配置文件启动redis服务。其中:原镜像的容器中/usr/local/etc/redis 不存在此目录,因此会创建
# 查看当前正在运行的redis所使用的配置 config get * , config get bind, config get dir
# 本地使用redis-desktop连接上redis,存储一些数据,然后save
# 然后,删除容器,重新执行下面的命令,再次连接上redis发现执行保存的数据又恢复了
# 可以通过logfile 指定日志生成的文件,
#     通过appendonly yes 开启 AOF 持久化
#     通过appendfilename 指定AOF 文件名,
#     通过dir指定数据持久化的位置,
#     通过protected-mode指定为 no 禁用 Redis 的保护模式,允许外部连接
#     通过bind 0.0.0.0 允许其他服务访问
#     通过dbfilename指定rdb持久化文件名
#     通过requirepass foreign2024@设置密码,这一步暂时不做
#     通过启动命令指定配置文件的位置
docker run -d -p 6380:6380 \
-v /redis/conf:/usr/local/etc/redis \
-v /redis/data:/data \
--name redis01 \
redis:6.0.8 redis-server /usr/local/etc/redis/redis.conf

[安装nginx]

# 拉取nginx镜像
docker pull nginx:1.24

# 启动1个nginx实例,ctrl + p + q退出,不会导致这个容器停止,docker ps查看到容器已启动
# 进入到容器内部,执行nginx -t 测试nginx配置文件的语法并且显示当前正在生效的配置,发现检查的是 /etc/nginx/nginx.conf配置文件,
#                 里面引入了/etc/nginx/conf.d/*.conf,而conf.d/default.conf配置了1个server的监听80端口,路径为 /usr/share/nginx/html
# 进入到容器内部,执行which nginx nginx程序所在目录为 /usr/sbin/nginx
# 可以通过nginx -V查看nginx的版本和配置参数,可以看到nginx程序位置,日志位置,nginx配置文件位置,nginx.pid位置
# 可以通过nginx -T查看nginx程序整合所有配置文件后的所使用的配置
docker run -it nginx:1.24

# 启动1个nginx实例,ctrl + p + q退出,不会导致这个容器停止,docker ps查看到容器已启动
# 此时访问http://119.23.61.24/即可看到nginx的欢迎页
# 可以通过docker logs -f 容器id 实时查看nginx的访问日志和错误日志
docker run -it -p 80:80 nginx:1.24

# 启动1个nginx实例,返回容器id,docker ps查看到容器已启动
# 此时访问http://119.23.61.24/即可看到nginx的欢迎页
docker run -d -p 80:80 nginx:1.24

# 启动1个nginx实例,
# 但是会清空掉容器中 /etc/nginx/conf.d中原有的default.conf文件,那么下面挂载/usr/share/nginx/html目录就没有意义了
docker run \
-d \
--name nginx01 \
-v /nginx/html:/usr/share/nginx/html \
-v /nginx/conf:/etc/nginx/conf.d \
nginx:1.24


# docker run --rm是 Docker Run 命令的一个选项,它的作用是在容器停止后自动删除容器。
# 当我们运行一个临时的容器时,使用 --rm 可以确保容器在停止后自动被删除,从而避免占用过多的磁盘空间。
# 比如:docker run --rm -v $(pwd):/app -w /app python:3 python hello.py
#       这个命令会下载 Python 3 镜像,并将当前目录挂载到容器的 /app 目录中。然后在容器中执行 python hello.py 命令,输出 “Hello Docker!”。
#       在命令执行完毕后,容器会自动删除,而且当前宿主机目录中的 hello.py 文件不会被删除,它仍然存在于本地文件系统中。
# 下面的命令可以将镜像中的/etc/nginx/nginx.conf配置文件保存到宿主机的指定目录中
docker run --rm --entrypoint=cat nginx:1.24 /etc/nginx/nginx.conf > /nginx/conf/nginx.conf
# 然后再使用下面的命令挂载配置文件,注意要确保宿主机上已存在/nginx/conf/nginx.conf这个文件
# 但是注意,由于当前宿主机中/nginx/html目录下没有文件,所以容器中的/usr/share/nginx/html会被清空,
#           因此http://119.23.61.24/返回403页面,复制1个index.html到宿主机的/nginx/html目录下,
#           再次访问http://119.23.61.24/返回正常页面
# 由于原有的nginx.conf配置文件引入了default.conf,里面监听了80端口,并且指定了对应的访问目录,
# 所以如果需要自定义,干脆就不要引入default.conf了,自己配置监听80的server
docker run \
--privileged=true \
-d \
--name nginx01 \
-p 80:80 \
-v /nginx/html:/usr/share/nginx/html \
-v /nginx/conf/nginx.conf:/etc/nginx/nginx.conf \
nginx:1.24

# 在宿主机的/nginx目录下创建conf,html,log目录,在/nginx/conf目录下创建nginx.conf配置文件并配置访问日志和错误
# 使用如下命令启动1个nginx容器
# 访问http://119.23.61.24/aaa可以看到/nginx/log/access.log/nginx/log/error.log有日志记录了
# 可以修改完宿主机的nginx.conf配置文件,然后docker restart nginx容器id即可让配置生效
# 注意,宿主机上挂载的目录不要删除,否则到容器内也无法创建对应的目录
docker run \
--privileged=true \
-d \
--name nginx01 \
-p 80:80 \
-v /nginx/html:/usr/share/nginx/html \
-v /nginx/conf/nginx.conf:/etc/nginx/nginx.conf \
-v /nginx/log:/var/log/nginx \
nginx:1.24

其中/nginx/conf/nginx.conf配置可以如下:
# vue项目打包后的dist文件夹放到/nginx/html目录中
# access_log和error_log也正常输出
# 如果删除nginx的这2个日志文件,可以执行nginx -s reload重新生成这2个日志文件
# nginx能够正常将/api的请求转发到另外的容器服务中,可以访问 http://192.168.134.3:8080/test01得到数据,现在只需要访问http://192.168.134.3/api/test即可
# nginx能够代理ws,可以访问ws://192.168.134.3:8080/wsTest/websocket建立连接,现在只需要ws://192.168.134.3/wsTest/websocket
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    log_format  main  '$http_origin = $http_host = $remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    #gzip  on;
    # include /etc/nginx/conf.d/*.conf;
    
    # 配合下面的 location /ws/ 一起使用
    # 定义变量,兼容HTTP和WebSocket两种请求协议
    map $http_upgrade $connection_upgrade {
       default          keep-alive;  # 默认 keep-alive,表示HTTP协议。
       'websocket'      upgrade;     # 若是 websocket 请求,则升级协议 upgrade。
    }   
    server {
       listen 80;
       server_name 192.168.134.3;
	   
       location / {
           root /usr/share/nginx/html/dist;
           index index.html index.htm;
           try_files $uri $uri/ /index.html;
       }
       
       # 可以访问 http://192.168.134.3:8080/test01得到数据,现在只需要访问 http://192.168.134.3/api/test 即可,将转发到http://192.168.134.3:8080/test01
       location ^~/api/ {
       
           proxy_pass http://192.168.134.3:8080/;
           
           # 客户端要访问的主机,客户端会携带Host请求头,转发的目标服务能够拿到这个请求头值192.168.134.3;如果不配置这个,那么转发的目标服务拿到的是nginx设置的192.168.134.3:8080
           proxy_set_header   Host             $host;
           
           # 客户端的真实ip,这个值由nginx自己解析出来
           proxy_set_header   X-Real-IP        $remote_addr;
           
           # 由于request.getRemoteAddr()默认获取到的是TCP层直接连接的客户端的IP,
           # 对于Web应用服务器来说由于经过nginx转发,所以直接连接它的客户端实际上是Nginx,也就是TCP层是拿不到真实客户端的IP。
           # 为了解决上面的问题,很多HTTP代理会在HTTP协议头中添加X-Forwarded-For头,用来追踪请求的来源。
           # X-Forwarded-For包含多个IP地址,每个值通过逗号+空格分开,最左边(client1)是最原始客户端的IP地址,
           # 中间如果有多层代理,每一层代理会将连接它的客户端IP追加在X-Forwarded-For右边。
           # 获取客户端真实IP的方法,首先从HTTP头中获取X-Forwarded-For,如果X-Forwarded-For头存在就按逗号分隔取最左边第一个IP地址,不存在就通过X-Real-IP去拿,
           #                         如何还为空,就通过request.getRemoteAddr()获取IP地址
           proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
           
           #Access-Control-Allow-Headers(请求头,响应头,预请求)     (携带Cookie情况下不能为*)
           #Access-Control-Allow-Methods(请求头,响应头,预请求)     (携带Cookie情况下不能为*)
           #Access-Control-Allow-Origin(响应头,预请求/正常请求)     (携带Cookie情况下不能为*)
           #Access-Control-Allow-Credentials(响应头,预请求/正常请求)(携带Cookie情况下要设置为true)
           #Access-Control-Max-Age(响应头,预请求)(单位s)
           
           # 允许跨域请求的域,* 代表所有
           # 注意当使用axios时,并且withCredentials为true时,则会在请求中传递cookie,此时不能使用*,否则会报错: 
           #                    The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'
           # 注意当要使用多个origin值时,可以使用nginx的map指令来根据$http_origin来映射不同的值
           # 注意当允许所有Origin源都能访问,可以直接设置为$http_origin
      		 add_header 'Access-Control-Allow-Origin' $http_origin;
            
    	   # 允许带上cookie请求
           # 如果服务器返回任何set-cookie响应头, 那么必须返回Access-Control-Allow-Credentials: true, 否则将不会在客户端上创建cookie
		   add_header 'Access-Control-Allow-Credentials' 'true';
	
		   # 允许请求的方法,比如 GET/POST/PUT/DELETE
		   add_header 'Access-Control-Allow-Methods' 'GET,POST,DELETE,PUT';
	
		   # 允许客户端携带请求头
           # 否则报错:Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
    			 add_header 'Access-Control-Allow-Headers' 'Content-Type,Authorization,token,X-Requested-With,x-token';
            
           # 保留20天
           add_header 'Access-Control-Max-Age' 1728000;
            
           # 允许暴露给客户端的响应头
           # add_header 'Access-Control-Expose-Headers' 'token,Authorization';
           
           # 这里用来测试,当客户端发出预检请求时,响应时没有这个响应头。说明预检请求只会携带下面if中的请求头,不会携带这里定义的响应头。
           add_header 'outer' 'outer-value';

           # 预检请求的处理
           if ($request_method = 'OPTIONS') {
           
              add_header 'Content-Type' 'text/plain;charset=UTF-8';
              add_header 'Content-Length' 0;
              # 这里必须要加这个响应头,否则报错: Response to preflight request doesn't pass access control check: 
              #                                   No 'Access-Control-Allow-Origin' header is present on the requested resource.
              add_header 'Access-Control-Allow-Origin' '$http_origin';
              
              # 这里必须要加这个响应头,否则报错: Response to preflight request doesn't pass access control check: 
              #                                   Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
              #add_header 'Access-Control-Allow-Headers' '*';
              # 这里必须要加这个响应头,否则报错: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Credentials'
              #                                   header in the response is '' which must be 'true' when the request's credentials mode is 'include'. 
              #                                   The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
              add_header 'Access-Control-Allow-Credentials' 'true';
              
              # 携带Cookie情况下不能为*
              # add_header 'Access-Control-Allow-Headers' '*';
              # 当axios开启withCredentials时,必须添加允许跨域请求携带的请求头,非预检请求的非简单的跨域请求会报错:
              #                               Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
              add_header 'Access-Control-Allow-Headers' 'Content-Type,x-token';
              
              # 保留20天
              add_header 'Access-Control-Max-Age' 1728000;
              
              add_header 'inner' 'inner-value';           
              return 204;
           }
       }      
       # 客户端可以 ws://192.168.134.3:8080/wsTest/websocket,现在客户端请求 ws://192.168.134.3/wsTest/websocket,将被转发到 http://192.168.134.3:8080/wsTest/websocket
       location ~^/wsTest/ {      
           proxy_redirect off;           
           proxy_pass http://192.168.134.3:8080;          
           proxy_http_version 1.1;
           # 如果不配置这个 如果客户端一直不发送消息过来,经测试默认1分钟之后连接会关闭。所以需要心跳机制。
           proxy_read_timeout 36000s;
           proxy_send_timeout 36000s;          
           # 升级协议头 websocket
           # 浏览器会携带Connection头: Upgrade;Upgrade头: websocket;
           proxy_set_header Connection "Upgrade";
           proxy_set_header Upgrade $http_upgrade;        
           # proxy_set_header Host $host;
           proxy_set_header X-Real_IP $remote_addr;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           proxy_set_header X-Forwarded-Proto $scheme; 
       }
       # 配合上面的 map $http_upgrade $connection_upgrade 一起使用
       # 客户端可以 ws://192.168.134.3:8080/wsTest/websocket,现在客户端请求 ws://192.168.134.3/ws/wsTest/websocket,将被转发到 http://192.168.134.3:8080/wsTest/websocket
       location /ws/ {
            proxy_pass http://192.168.134.3:8080/; # 转发到后端接口
    		# 如果不配置这个 如果客户端一直不发送消息过来,经测试默认1分钟之后连接会关闭。所以需要加上心跳机制。
            proxy_read_timeout 36000s;
            proxy_send_timeout 36000s;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
        }
    }
}





[Dockerfile]
# Dockerfile -> 镜像 -> 容器
# 执行步骤:
#   1.docker从基础镜像运行一个容器
#   2.执行一条指令并对容器作出修改
#   3.执行类似docker commit的操作提交一个新的镜像层
#   4.docker再基于刚提交的镜像运行一个新容器
#   5.执行dockerfile中的下一条指令直到所有指令都执行完成
# 指令:
#   FROM
#          定义基于哪个镜像构建新的镜像
#          Dockerfile的第一条必须是FROM
#   MAINTAINER   
#          指定新镜像的创建者名字和邮箱
#          MAINTAINER bulut<bulut123@163.com>
#   ENV
#          用来在构建镜像过程中设置环境变量,这个环境变量可以在后续的任何RUN指令中使用,也可以在其它指令中直接使用这些环境变量
#          ENV MY_PATH /app/test
#   ADD
#          将宿主机目录下的文件拷贝进镜像且会自动处理URL和解压tar压缩包
#   COPY
#          类似ADD,拷贝文件和目录到镜像中
#          容器内的指定路径不用事先建好,会自动创建
#   VOLUME
#           表示挂载数据卷的目录位置,如果没有设置,就需要在运行run命令时通过-v进行设置
#   RUN    
#          新镜像构建时在容器上运行的指令      
#          shell格式:等同于在终端操作的shell命令;RUN yum -y install vim
#          exec格式:RUN [“可执行文件”, “参数1”, “参数2”] ;RUN ["./test.php", "dev", "offline"],等同于RUN ./test.php dev offline
#   EXPOSE
#          当前容器对外暴露的端口。如果没有设置,就需要在运行run命令时通过-p进行设置
#          EXPOSE 8080
#   WORKDIR
#          指定在创建容器后,终端默认登陆的进来工作目录,一个落脚点
#   USER
#          指定该镜像以什么样的用户去执行,如果都不指定默认是root
#   CMD
#          设置容器默认要运行的命令及参数,指定通过新镜像启动容器时,需要执行的指令。
#          CMD在docker run时运行。
#          shell格式 : CMD <命令>
#          exec格式 : CMD [“可执行文件”, “参数1”, “参数2”, …]
#          参数列表格式:CMD [ “参数1”, “参数2”, …]。在指定了ENTRYPOINT指令后,用CMD指定具体的参数。
#          Dockerfile可以有多个CMD指令,但是只有最后一个有效。CMD会被docker run之后的参数替换。
#          如果docker run后面指定了容器运行的命令,则CMD指令无效,CMD会被docker run之后的参数替换
#   ENTRYPOINT
#          指定通过新镜像启动容器时,需要执行的指令,可以追加命令。
#          ENTRYPOINT在docker run时运行
#          命令格式:ENTRYPOINT ["<excuteable>","<param1>","<param2>",...]
#          类似于CMD指令,但是ENTRYPOINT不会被docker run后面的命令覆盖,而且这些命令行参数会被当作参数送给 ENTRYPOINT 指令指定的程序
#          ENTRYPOINT可以和CMD一起用,会将CMD的内容作为参数传递给ENTRYPOINT指令,他两个组合会变成 <ENTRYPOINT> "<CMD>"


# 创建1个centos7文件夹,在此文件夹中创建名为Dockerfile的文件。命令和参数必须使用双引号。
FROM centos:7
CMD ["ls","-a"]
# 可以构建为1个镜像,使用docker images可以看到这个镜像。多次执行命令,发现这个镜像id不变,猜测镜像id类似于md5唯一标识1个镜像文件。
docker build -t cmd:1.0 . 
# 使用指定的文件来构建镜像
docker build -f dockerfile01 -t cmd:1.0 .
# 运行该镜像,启动容器,发现输出日志,就没了。然后就使用docker ps -a 查看容器已经停止了。docker logs 容器id 查看就是之前输出的日志。
# docker start 容器id 返回容器id。然后就使用docker ps -a 查看容器又停止了。docker logs 容器id 查看日志输出了2遍,也就是每启动1次输出一遍。
docker run cmd:1.0
# 运行该镜像,启动容器,发现输出日志,就没了。然后就使用docker ps -a 查看容器已经停止了。docker logs 容器id 查看就是之前输出的日志。
# docker start 容器id 返回容器id。然后就使用docker ps -a 查看容器又停止了。docker logs 容器id 查看日志输出了2遍,也就是每启动1次输出一遍。
docker run -it cmd:1.0
# 运行该镜像,启动容器,返回容器id。然后就使用docker ps -a 查看容器已经停止了。docker logs 容器id 查看就是之前输出的日志。
# docker start 容器id 返回容器id。然后就使用docker ps -a 查看容器又停止了。docker logs 容器id 查看日志输出了2遍,也就是每启动1次输出一遍。
docker run -d cmd:1.0
# 使用该命令运行该镜像,启动容器时会报错,因为 CMD命令会被-l所替代,由于没有-l命令,所以找不到。这说明-l把参数给替换了。但是,使用docker ps -a 查看容器处于Created,证明容器还是被创建了。
docker run cmd:1.0 -l
# 运行该镜像,启动容器,发现输出日志,就没了。然后就使用docker ps -a 查看容器已经停止了。docker logs 容器id 查看就是之前输出的日志。
# docker start 容器id 返回容器id。然后就使用docker ps -a 查看容器又停止了。docker logs 容器id 查看日志输出了2遍,也就是每启动1次输出一遍。
docker run cmd:1.0 ls
# 运行该镜像,启动容器,未输出日志,而是进入了容器内部,使用ctrl + p + q退出,不会导致这个容器停止。docker logs 容器id 查看未有任何日志。
# docker stop 容器id 关闭容器。再docker start 容器id 启动容器,此时未有任何输出。docker ps 查看容器已启动。docker logs 容器id 查看未有任何日志。
docker run -it cmd:1.0 bash
# 运行该镜像,启动容器,返回容器id。然后就使用docker ps -a 查看容器已经停止了。docker logs 容器id 查看到了输出的日志。
# docker start 容器id 返回容器id。然后就使用docker ps -a 查看容器又停止了。docker logs 容器id 查看日志输出了2遍,也就是每启动1次输出一遍。
docker run -it -d cmd:1.0
# 运行该镜像,启动容器,返回容器id。然后就使用docker ps -a 查看容器还在运行中。docker logs 容器id 未查看到日志。
# docker stop 容器id 关闭容器。再docker start 容器id 启动容器,此时未有任何输出。docker ps 查看容器已启动。docker logs 容器id 查看未有任何日志。
docker run -it -d cmd:1.0 bash

# 创建1个centos7文件夹,在此文件夹中创建名为Dockerfile的文件。命令和参数必须使用双引号。
FROM centos:7
ENTRYPOINT ["ls","-a"]
# 可以构建为1个镜像,使用docker images可以看到这个镜像。多次执行命令,发现这个镜像id不变,猜测镜像id类似于md5唯一标识1个镜像文件。
docker build -t cmd:2.0 . 
# 使用指定的文件来构建镜像
docker build -f dockerfile02 -t cmd:2.0 .
# 运行该镜像,启动容器,发现输出日志,就没了。通过输出日志,观察到很明显执行的命令为ls -a -l,说明docker run后面指定的参数添加到了后面。
docker run cmd:2.0 -l

# 创建1个centos7文件夹,在此文件夹中创建名为dockerfile03的文件。
FROM centos:7
RUN touch /a.txt
CMD echo halo > /a.txt
CMD echo world >> /a.txt
CMD /bin/bash
# 可以构建为1个镜像,使用docker images可以看到这个镜像。
docker build -f dockerfile03 -t cmd:3.0 .
# 运行该镜像,启动容器,发现进入到了容器内部。ctrl + p + q退出,不会导致这个容器停止。
# 再次docker exec -it 容器id bash 进入容器查看 /a.txt发现内容是空的,说明只有最后1个CMD命令执行了。
docker run -it cmd:3.0

## 虚悬镜像,即镜像仓库名和tag都为none的镜像。这可能是因为强制使用[镜像名:tag]删除镜像,但是容器还引用着这个镜像的缘故。
# 查看虚悬镜像
docker image ls -f dangling=true
# 删除虚悬镜像
docker image prune

## 构建1个[unbuntu:1.0镜像],文件名为Dockerfile,内容如下
FROM ubuntu
MAINTAINER dy<dy@163.com>
ENV MYPATH /usr/local
WORKDIR $MYPATH
RUN apt-get update
RUN apt-get install net-tools
RUN apt-get install -y iproute2
RUN apt-get install -y inetutils-ping
EXPOSE 80
CMD /bin/bash
# 在Dockerfile所在目录下执行 构建命令
docker build -t unbuntu:1.0 .
# 运行该镜像,启动容器,发现进入到了容器内部,并且当前目录在/usr/local。ctrl + p + q退出,不会导致这个容器停止.
docker run -it unbuntu:1.0

## 构建1个[centos7-java8镜像],文件名为dockerfile01,内容如下
FROM centos:7
RUN mkdir -p /usr/lib/jvm
# 这个压缩包会自动解压到/usr/lib/jvm下
ADD ./jdk-8u221-linux-x64.tar.gz /usr/lib/jvm
# 这里只能用单引号
RUN sed -i '$a export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_221' /etc/profile
RUN sed -i '$a export JRE_HOME=${JAVA_HOME}/jre' /etc/profile
RUN sed -i '$a export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib' /etc/profile
RUN sed -i '$a export  PATH=${JAVA_HOME}/bin:$PATH' /etc/profile
# 不能直接运行source /etc/profile,因为RUN指令只在构建过程中生效,因此放到~/.bashrc文件中
RUN echo 'source /etc/profile' >> ~/.bashrc
CMD /bin/bash

## 构建1个[centos7-java8镜像],文件名为dockerfile02,内容如下
##############################################################
FROM centos:7
# 修改时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# 安装net-tools以支持netstat命令,不需要提前创建这个文件夹,会自动创建,注意后面必须加/表示是目录
# 找个有网的机器下载rpm包 yum install yum-utils,yumdownloader --resolve net-tools
COPY ./net-tools-2.0-0.25.20131004git.el7.x86_64.rpm /rpm_package/
RUN rpm -ivh /rpm_package/net-tools-2.0-0.25.20131004git.el7.x86_64.rpm
# 创建文件夹
RUN mkdir -p /usr/lib/jvm
# 这个压缩包会自动解压到/usr/lib/jvm下
ADD ./jdk-8u221-linux-x64.tar.gz /usr/lib/jvm
# 设置环境变量
ENV JAVA_HOME /usr/lib/jvm/jdk1.8.0_221
ENV JRE_HOME=${JAVA_HOME}/jre
ENV CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
ENV PATH=${JAVA_HOME}/bin:$PATH
CMD /bin/bash
##############################################################
# 将jdk-8u221-linux-x64.tar.gz压缩包放在与dockerfile02同目录下,在该目录下执行下面命令
docker build -f dockerfile02 -t centos7-java8:1.0 .
# 查看该镜像创建成功
docker images centos7-java8:1.0
# 运行该镜像,启动容器,发现进入到了容器内部。ctrl + p + q退出,不会导致这个容器停止
# java -version 说明java已经配置好了, date 说明时间也正确了
docker run -it centos7-java8:1.0	
# 删除镜像
docker rmi centos7-java8:1.0

##############################################################
# 执行后,当前ssh窗口被锁定,屏幕输出日志,无法继续输入命令,可按CTRL + C打断程序运行,程序会退出,或直接关闭窗口,程序也会退出。
java -jar app.jar  
# &代表在后台运行。执行后,返回进程id,并在屏幕输出日志。按enter即可结束输出日志。可在当前窗口可以继续输入命令。当窗口关闭,程序会退出。
java -jar app.jar &
# nohup意思是不挂断运行命令,当账户退出或终端关闭时,程序仍然运行。
# 执行后,返回进程id,会在当前目录下生成nohup.out文件,无论是sout/serr/log都会输出到这个文件中。当窗口关闭,程序不会退出仍可访问。
nohup java -jar app.jar &
# nohup执行后面不带&。执行后,此时无法输入命令。按ctrl + c会打断程序运行,程序会退出。所以nohup必须配合&一起使用。并且会在当前目录下生成nohup.out文件。
nohup java -jar app.jar
# command >out.file是将command的输出重定向到out.file文件,即输出内容不打印到屏幕上,而是输出到out.file文件中。因为nohup默认写入nohup.out文件。
# 执行后,返回进程id,可以在当前窗口继续输入命令。并且会在当前目录下生成nohup.out文件,无论是sout/serr/log都会输出到这个文件中。当窗口关闭,程序不会退出仍可访问。
nohup java -jar app.jar >temp.txt &
# 2 代表标准错误输出,即stderr。
# &1 代表标准输出
# command > springboot.log 是将command的输出重定向到springboot.log文件,因为nohup默认写入nohup.out文件。
# 2 > &1 是将把原本要输出到stderr的错误信息重定向输出到stdout,而stdout又重定向到指定文件了,所以所有信息都写入到指定文件。
#        shell上: 0表示标准输入,1表示标准输出,2表示标准错误输出
#        在java代码里面用System.err.println()输出的就是标准错误输出
# nohup 代表不挂断的方式运行 
# & 代表让nohup命令后台运行,否则会被Ctrl + c结束
nohup java -jar app.jar > springboot.log 2>&1 &
# 不输出日志
# /dev/null可以看作黑洞,等价于一个只写文件。所有写入它的内容都会永远丢失,尝试从它那儿读取内容则什么也读不到。也就是将所有产生的日志将被丢弃
nohup java -jar app.jar > /dev/null 2>&1 &
# 以上都是没有配置logback的测试,现在配置logback再来看看区别
# 这个会将使用log输出的都写到日志文件和nohup.out中。而System.out和System.err都只会写到nohup.out中。
# nohup.out文件会输出在这条命令执行时所在的目录
nohup java -jar app.jar &
# 忽略标准输出和标准错误输出
nohup java -jar app.jar > /dev/null 2>&1 &
##############################################################

## 基于centos7-java8镜像构建1个[springboot项目镜像],文件名为dockerfile03,内容如下
##############################################################
# 基于centos7-java8镜像构建
FROM centos7-java8:1.0
# 设置环境变量方便下面引用
ENV JAR_NAME app.jar
# 引用设置的环境变量,当前目录可省略./,目标文件夹不需要提前创建,注意后面要带/
COPY ${JAR_NAME} /app/backend/
# 切换到此目录,当启动容器后,使用docker exec -it 容器id bash进入容器后,会来到此目录
WORKDIR /app/backend/
# 此处不需要修改jar包的名字
# RUN mv spring-boot-docker-basic-1.0-SNAPSHOT.jar app.jar
# 暴露端口,在运行该镜像时,仍需要使用 -p 映射端口。只是起到1个规范的作用。
EXPOSE 8080
ENTRYPOINT ["java", "-Dfile.encoding=UTF-8", "-Xms60m", "-Xmx300m" ,"-jar", "/app/backend/app.jar", "--server.port=8080", "--spring.profiles.active=dev"]
##############################################################
# 给镜像打上新的标签,以便于推送到阿里云镜像仓库
docker tag 65fc6a227775  crpi-hugadtddf72petnn.cn-shenzhen.personal.cr.aliyuncs.com/zzhua173/centos7-java8:1.0
# 推送到阿里云镜像仓库,注意必须使用 镜像仓库/镜像名:tag
docker push crpi-hugadtddf72petnn.cn-shenzhen.personal.cr.aliyuncs.com/zzhua173/centos7-java8:1.0


# 将spring-boot-docker-basic-1.0-SNAPSHOT.jar放到与dockerfile03文件同目录下
# 备注代码地址: https://gitee.com/zzhua195/spring-boot-docker-basic
docker build -f dockerfile03 -t app:1.0 .
# 使用 --cap-add 明确添加指定功能:SYS_PTRACE , 否则使用不了jmap,jinfo等工具查看jvm参数
# 1. 使用jps命令来获取Java进程的进程ID jps -mlv 或使用ps -ef命令查找 
# 2. 查看某个Java进程的所有参数 jinfo -flags <进程ID> 可查看到初始化堆内存大小/最大堆内存大小/新生代默认内存大小/新生代最大内存大小
#    查看java进程使用的系统属性 jinfo -sysprops <进程ID> 
#    输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等 jmap -heap <进程ID>
# 执行如下命令会进入到容器内部,并且屏幕正在打印springboot启动的日志信息,按ctrl + p + q退出,不会导致这个容器停止。
# 或者ctrl + c停止后,需要使用docker start 容器id 即可启动该容器。
# 容器启动成功后,可http://192.168.134.3:8080/test01访问到接口,证明项目部署成功。
docker run --cap-add=SYS_PTRACE -it -p 8080:8080 app:1.0
# 显示容器完整信息,不要使用省略号。可以查看到容器id、镜像id、启动命令、端口映射、容器名
docker ps --no-trunc
# 会进入到容器内部,当前所处目录为/app/backend
docker exec -it 容器id  bash


# 使用-d运行该容器,一切正常。
docker run --cap-add=SYS_PTRACE -d -p 8080:8080 app:1.0
# 会进入到容器内部,当前所处目录为/app/backend,查看当前目录下的logs/app.log其中输出了springboot的启动日志
docker exec -it 容器id  bash
# 关闭容器
docker stop 容器id
# 启动容器
docker start 容器id
# 查看容器日志,可以看到有2个springboot的logo输出了,说明一共输出过2次,
# 这说明docker start 容器id 运行容器会把java -jar 再执行一遍。
docker logs 容器id -f -n 100


下面是配合测试的命令方便复制,就保留了下来
docker build -f dockerfile03 -t app:1.0 .
docker rm -f $(docker ps -aq)
docker rmi app:1.0
docker exec -it $(docker ps -aq) bash

[测试volume]
# 基于centos7-java8镜像构建
FROM centos7-java8:1.0
# 设置环境变量方便下面引用
ENV JAR_NAME app.jar
# 引用设置的环境变量,当前目录可省略./,目标文件夹不需要提前创建,注意后面要带/
COPY ${JAR_NAME} /app/backend/
# 切换到此目录,当启动容器后,使用docker exec -it 容器id bash进入容器后,会来到此目录
WORKDIR /app/backend/
# 此处不需要修改jar包的名字
# RUN mv spring-boot-docker-basic-1.0-SNAPSHOT.jar app.jar
# 暴露端口,在运行该镜像时,仍需要使用 -p 映射端口。只是起到1个规范的作用。
EXPOSE 8080
# 设置挂载数据卷
# 其中 /data1和/data2和/data3/data4在原容器中并不存在,它们会在容器创建时中创建这些目录,而原/root内部是有文件的
VOLUME ["/data1","/data2"]
VOLUME /data3/data4
VOLUME /app/backend/logs
VOLUME /root
ENTRYPOINT ["java", "-Dfile.encoding=UTF-8", "-Xms60m", "-Xmx300m" ,"-jar", "/app/backend/app.jar", "--server.port=8080", "--spring.profiles.active=dev"]
# 当使用如下命令创建容器后,可以通过docker inspect 容器id 查看Mounts查询到所有挂载的都是Type:volume。并且都会为这5个目录在/var/lib/docker/volumes/创建对应的目录了
# 并且宿主机的也会把容器中的/tmp文件夹下的内容都挂载到了本地,容器中的/app/backend/logs也挂载到了本地,/data1和/data2和/data3/data4 也挂载到了本地,并且它们都是双向同步的
docker run --cap-add=SYS_PTRACE -d -p 8080:8080 app:1.0
# 当使用如下命令创建容器后,可以通过docker inspect 容器id 查看Mounts查询到/root和/app/backend/logs都是Type:bind,其它的都是Type:volume。另外3个未指定-v挂载的都在/var/lib/docker/volumes/创建对应的目录了
# 并且发现容器中的 /root目录被清空掉了。由于/app/bakend/logs/app.log是后面生成的,所以宿主机和容器都是保持一致的。
# 结论:所以在使用-v挂载的时候,一定要确保不要清空掉一些重要的配置文件,要留好备份。对于容器中的重要文件使用volume挂载而不要自定义挂载。对于容器运行时生成的文件可以大胆的挂载到自定义目录。
docker run --cap-add=SYS_PTRACE -d -p 8080:8080 -v /boot01/root:/root -v /boot01/logs:/app/backend/logs app:1.0
# 所以最好只挂载/app/backend/logs这个目录
docker run --cap-add=SYS_PTRACE -d -p 8080:8080 -v /boot01/logs:/app/backend/logs --name app01 app:1.0

# 基于centos7构建镜像
FROM centos:7
MAINTAINER dy<dy@163.com>
ENV MYPATH /usr/local
WORKDIR $MYPATH
RUN yum install -y yum-utils
# 安装vim编辑器
RUN yum -y install vim
# 安装ifconfig命令查看网络IP
RUN yum -y install net-tools
# 安装java8 及 lib 库
RUN yum -y install glibc.i686
RUN mkdir /usr/local/java
# ADD 是相对路径jar,把jdk-8u391-linux-x64.tar.gz添加到容器中,安装包必须要和Dockerfile文件在同一位置
ADD jdk-8u391-linux-x64.tar.gz /usr/local/java
# 配置java环境变量
ENV JAVA_HOME /usr/local/java/jdk1.8.0_391
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH $JAVA_HOME/lib/dt.tar:$JAVA_HOME/lib/tools.jar$JRE_HOME/lib:$CLASSPATH
ENV PATH $JAVA_HOME/bin/:$PATH
EXPOSE 80
CMD echo $PATH
CMD echo "success--------------------ok"
CMD /bin/bash

# 基于centos:8构建镜像
FROM centos:latest
MAINTAINER dy<dy@163.com>
ENV MYPATH /usr/local
WORKDIR $MYPATH
# 更改镜像,需要将镜像从 mirror.centos.org 更改为 vault.centos.org
RUN cd /etc/yum.repos.d/
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*
RUN sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
# 安装中文语言包
RUN yum -y install glibc-langpack-zh
RUN yum install -y yum-utils
# 安装vim编辑器
RUN yum -y install vim
# 安装ifconfig命令查看网络IP
RUN yum -y install net-tools
# 安装java8 及 lib 库
RUN yum -y install glibc.i686
RUN mkdir /usr/local/java
# ADD 是相对路径jar,把jdk-8u391-linux-x64.tar.gz添加到容器中,安装包必须要和Dockerfile文件在同一位置
ADD jdk-8u391-linux-x64.tar.gz /usr/local/java
# 配置java环境变量
ENV JAVA_HOME /usr/local/java/jdk1.8.0_391
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH $JAVA_HOME/lib/dt.tar:$JAVA_HOME/lib/tools.jar$JRE_HOME/lib:$CLASSPATH
ENV PATH $JAVA_HOME/bin/:$PATH
EXPOSE 80
CMD echo $PATH
CMD echo "success--------------------ok"
CMD /bin/bash

# 自定义镜像myubuntu
FROM ubuntu
MAINTAINER dy<dy@163.com>
ENV MYPATH /usr/local
WORKDIR $MYPATH
RUN apt-get update
RUN apt-get install net-tools
RUN apt-get install -y iproute2
RUN apt-get install -y inetutils-ping
EXPOSE 80
CMD echo $MYPATH
CMD echo "install inconfig cmd into ubuntu success--------ok"
CMD /bin/bash


## 构建1个springboot的docker镜像
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local
# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar
# 安装JDK
RUN cd $JAVA_DIR \
 && tar -xf ./jdk8.tar.gz \
 && mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar

# 基础镜像使用java
FROM java:8
# 作者
MAINTAINER DY
# volume 指定临时文件为/tmp,在主机/var/lib/docker目录下创建了一个临时文件并链接到容器的/tmp
VOLUME /tmp
# 将jar包添加到容器中并更名为 dy_docker.jar
ADD spring-boot-docker-demo-0.0.1-SNAPSHOT.jar dy_docker.jar
# 运行jar包
RUN bash -c 'touch /dy_docker.jar'
ENTRYPOINT ["java","-jar","/dy_docker.jar"]
# 暴露6001端口作为微服务
EXPOSE 6001

[Docker网络]
# 容器间的互联和通信以及端口映射
# 容器lP变动时候可以通过服务名直接网络通信而不受到影响

docker启动时会产生一个名为docker0的虚拟网桥172.17.0.1,这可以通过 ifconfig 查看到
它在内核层连通了其他的物理或虚拟网卡,这就将所有容器和本地主机都放到同一个物理网络
Docker默认指定了docker0接口的IP地址和子网掩码,让主机和容器之间可以通过网桥相互通信。

此时ifconfig,可以看到docker0(172.17.0.1)、ens33(192.168.134.3)、lo(127.0.0.1)、virbr0(192.168.122.1)

# 查看Docker网络,
# 默认情况下有3个网络模式
# bridge    为每一个容器分配、设置IP等,并将容器连接到一个docker0虚拟网桥,默认为该模式。
#           使用--network bridge指定,默认使用dockerO	
#           
# host      容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。
#           使用--network host指定
#           
# none      容器有独立的Network namespace,但并没有对其进行任何网络设置,如分配 veth pair 和网桥连接,IP等。
#           使用--network none指定
#
# container 新创建的容器不会创建自己的网卡和配置自己的lP而是和—个指定的容器共享IP、端口范围等。
#           使用--network container:NAME或者容器ID指定
#
# 可以使用docker network create 网络名字 来创建Docker网络。
docker network ls

# 查看网络数据源
docker network inspect 网络名字

# 删除网络
docker network rm 网络名字

# 创建网络
# docker network ls 可以看到多了1个a_network
# docker network inspect a_network 可以看到它的Gateway是172.19.0.1,Subnet是172.19.0.0/16
# 此时ifconfig,可以看到docker0、ens33、lo、virbr0、br-7aa1ccc28187
# 然后把这个自定义网络删除掉 docker network rm a_network
docker network create a_network

docker network --help
Commands:
  connect     Connect a container to a network
  create      Create a network
  disconnect  Disconnect a container from a network
  inspect     Display detailed information on one or more networks
  ls          List networks
  prune       Remove all unused networks
  rm          Remove one or more networks

# 查看容器的网络模式,查看networks
docker inspect 容器ID or 容器名字

[bridge网络]
Docker服务默认会创建一个docker0网桥(其上有一个docker0内部接口),该桥接网络的名称为docker0,
它在内核层连通了其他的物理或虚拟网卡,这就将所有容器和本地主机都放到同一个物理网络。
Docker默认指定了docker0接口的IP地址和子网掩码,让主机和容器之间可以通过网桥相互通信。

# 使用 docker run --cap-add=SYS_PTRACE -d -p 8080:8080 app:1.0
#      docker run --cap-add=SYS_PTRACE -d -p 8081:8080 app:1.0 依次启动2个容器
# 使用 docker inspect 容器ID1, docker inspect 容器ID2 命令分别查看它们的networks节点,
#      发现它们用的都是bridge,其中gateway就是docker0的172.17.0.1,IPAddress则分别是172.17.0.2,172.17.0.3 可以发现容器ip是递增的,
#                              其中NetworkID就是 docker network ls 命令查看到的 bridge的 NETWORK ID
# 此时 docker network inspect bridge 的IPAM节点下可以看到它的Gateway是172.17.0.1,Subnet是172.17.0.0/16",
#      并且看到Containers节点下 bridge网络下有2个容器,它们分别使用172.17.0.2/16,172.17.0.3/16,这里可以看到具体的容器id和容器所分配的ip,
#      并且此时分别进入这2个容器内部,使用ifconfig可以查看到只有lo和eth0,其中eth0分别就是172.17.0.2,172.17.0.3,
#      这说明容器启动时如果不指定网络的话,默认使用的就是bridge网络,而bridge用的就是docker0
# 此时ifconfig,可以看到docker0、ens33、lo、virbr0、veth8013873、veth530c653 
#      也就是容器1和容器2内部都有1个eth与外部的1个veth一一对应,
#      而veth都连通了docker0,因此容器1和容器2可以通信。
# 此时在容器2的内部 curl 172.17.0.2:8080/test01 正常返回了数据;
#     在容器2的内部 curl 192.168.134.3:8080/test01 正常返回了数据;
#     在浏览器中访问 http://192.168.134.3:8080/test01 正常返回了数据;
#     在浏览器中访问 http://192.168.134.3:8081/test01 正常返回了数据;
# 那么当容器2关闭,再启动1个容器3,再开启容器2,则原来的容器2的ip就发生了变化。
# 所以,docker容器内部的ip是有可能会发生改变的
# 把所有容器删除后,docker network inspect bridge 的Containers节点下看到没有容器在这个网络下了,ifconfig查看到2个veth也被删了。

[host网络]
直接使用宿主机的IP地址与外界进行通信,不再需要额外进行NAT转换。
此时容器的IP借用主机的,所以容器共享宿主机网络IP,这样的好处是外部主机与容器可以直接通信。

# 为了避免其它的影响,先删除所有的容器 docker rm -f $(docker ps -aq)
# 然后执行下面的命令 创建1个 使用 host网络的容器,
# 执行时出现了警告信息WARNING: Published ports are discarded when using host network mode,紧接着返回了容器id,说明容器创建成功了
# 然后浏览器访问 http://192.168.134.3:8082/test01 正常返回了数据,
# 这说明端口映射是没有成功的,仍然使用的是8080端口。
# 此时,docker ps                   查看到该容器的ports是空的
#       docker inspect 容器id       查看到networks节点使用的是host,NetworkID是docker network ls 命令查看到的 host的 NETWORK ID
#       docker network inspect host 查看到Containers节点下有这个容器的容器id
docker run -d -p 8082:8080 --name host01 --network host app:1.0

# 为了避免其它的影响,继续删除所有的容器 docker rm -f $(docker ps -aq)
# 然后执行下面的命令 创建1个 使用 host网络的容器,
# 执行时没有出现警告信息了,
# 然后浏览器访问 http://192.168.134.3:8082/test01 正常返回了数据,
# 此时,docker ps                   查看到该容器的ports是空的
#       docker inspect 容器id       查看到networks节点使用的是host,NetworkID是docker network ls 命令查看到的 host的 NETWORK ID
#       docker network inspect host 查看到Containers节点下有这个容器的容器id
# 此时,进入容器内部执行ifconfig,发现跟宿主机的网络完全一致
# 这说明使用host网络时,不需要指定映射端口,而是镜像里面使用EXPOSE暴露的端口来占用主机的端口
# 再次执行下面的命令,返回了容器id,然后docker ps -a 发现刚刚创建的容器退出了,然后docker logs 新容器id 从日志中发现是端口占用导致的退出。
docker run -d --name host01 --network host app:1.0

[none]
none是禁用网络功能,只有lo标识(就是127.0.0.1表示本地回环)
在none模式下,并不为Docker容器进行任何网络配置。
也就是说,这个Docker容器没有网卡、IP、路由等信息,只有一个lo。
需要我们自己为Docker容器添加网卡、配置IP等。

# 为了避免其它的影响,删除所有的容器 docker rm -f $(docker ps -aq)
# 然后执行下面的命令 创建1个 使用 none网络的容器,
# 此时无法访问这个容器
# 此时,docker ps                   查看到该容器的ports是空的
#       docker inspect 容器id       查看到networks节点使用的是none,NetworkID是docker network ls 命令查看到的 none的 NETWORK ID
#       docker network inspect none 查看到Containers节点下有这个容器的容器id
# 此时,进入容器内部执行ifconfig,发现只有lo,即127.0.0.1这个网卡。
docker run -d --name test01 --network none app:1.0

[container]
新建的容器和已经存在的一个容器共享一个网络ip配置而不是和宿主机共享。
新创建的容器不会创建自己的网卡,配置自己的IP,而是和一个指定的容器共享IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。

# 为了避免其它的影响,删除所有的容器 docker rm -f $(docker ps -aq)
# 此时宿主机上执行ifconfig,发现只有docker0、ens33、lo、virbr0
# 因为容器会共用同一个ip地址,因此端口肯定不能重复,所以就不能使用前面的app:1.0镜像了
# 拉取alpine镜像
docker pull alpine
# 启动1个容器,并进入终端,在容器中执行ifconfig,发现只有eth0(172.17.0.2)和lo(127.0.0.1)
# 退出容器后,在宿主机上执行ifconfig,发现有docker0、ens33、lo、virbr0、vethd7975c7
# 这其实就是模式使用的bridge网络
# 此时,docker inspect 容器id         查看到networks节点使用的是bridge,NetworkID是docker network ls 命令查看到的 bridge 的 NETWORK ID
#       docker network inspect bridge 查看到Containers节点下有这个容器的容器id
docker run -it --name alpine1 alpine /bin/sh
# 再启动1个容器,并指定为container使用alpine1的网络,
# 进入终端,在容器中执行ifconfig,发现只有eth0(172.17.0.2)和lo(127.0.0.1)与alpine1完全一致
# 退出容器后,在宿主机上执行ifconfig,发现有docker0、ens33、lo、virbr0、vethd7975c7
# 此时,docker inspect 容器id         查看到networks节点下是个空的内容
#       docker network inspect bridge 查看到Containers节点下只有上面那个容器的id
docker run -it --name alpine2 --network container:alpine1 alpine /bin/sh

[自定义网络]

# 为了避免其它的影响,删除所有的容器 docker rm -f $(docker ps -aq)
# 宿主机执行 ifconfig,有docker0(172.17.0.1)、ens33(192.168.134.3)、lo(127.0.0.1)、virbr0(192.168.122.1)
# 然后执行下面的命令 创建1个容器,默认使用的是bridge网络,
# 进入到容器内部,执行ifconfig,有eth0(172.17.0.2)和lo(127.0.0.1)
# 在宿主机执行 ifconfig,有docker0(172.17.0.1)、ens33(192.168.134.3)、lo(127.0.0.1)、virbr0(192.168.122.1)、veth2d790f9
# 在宿主机执行 docker ps,在ports可以看到这个容器的端口映射关系 0.0.0.0:8082->8080/tcp, :::8082->8080/tcp
# 在宿主机执行 docker inspect 容器id,看到使用networks节点的bridge,其中gateway就是docker0的172.17.0.1,IPAddress是172.17.0.2
#                                                                   其中NetworkID就是 docker network ls 命令查看到的 bridge的 NETWORK ID
# 在宿主机执行 docker network inspect bridge,看到IPAM节点下可以看到它的Gateway是172.17.0.1,Subnet是172.17.0.0/16",
#                                             并且看到Containers节点下有这个容器id,并且IPv4Address是172.17.0.2
docker run --cap-add=SYS_PTRACE -d -p 8082:8080 --name app01 app:1.0
# 然后继续执行下面的命令 创建另1个容器,默认使用的是bridge网络,
# 进入到容器内部,执行ifconfig,有eth0(172.17.0.3)和lo(127.0.0.1)
# 在宿主机执行 ifconfig,有docker0(172.17.0.1)、ens33(192.168.134.3)、lo(127.0.0.1)、virbr0(192.168.122.1)、veth2d790f9、vethd3653fa
# 在宿主机执行 docker ps,在ports可以看到这个容器的端口映射关系 0.0.0.0:8083->8080/tcp, :::8083->8080/tcp
# 在宿主机执行 docker inspect 容器id,看到使用networks节点的bridge,其中gateway就是docker0的172.17.0.1,IPAddress是172.17.0.3
#                                                                   其中NetworkID就是 docker network ls 命令查看到的 bridge的 NETWORK ID
# 在宿主机执行 docker network inspect bridge,看到IPAM节点下可以看到它的Gateway是172.17.0.1,Subnet是172.17.0.0/16",
#                                             并且看到Containers节点下 bridge网络下有这个容器id,并且IPv4Address是172.17.0.3。还有上面那个容器的id和IPv4Address是172.17.0.2.
docker run --cap-add=SYS_PTRACE -d -p 8083:8080 --name app02 app:1.0
# 2个容器启动之后
# 进入到容器1(它的ip是172.17.0.2),执行 ping 172.17.0.3 能正常ping通;执行 ping 192.168.134.3 能正常ping通;执行 ping www.baidu.com 能正常ping通;
# 进入到容器2(它的ip是172.17.0.3),执行 ping 172.17.0.2 能正常ping通;执行 ping 192.168.134.3 能正常ping通;执行 ping www.baidu.com 能正常ping通;
# 可是在容器1中 ping app02 会输出 ping: app02: Name or service not known
# 可是在容器2中 ping app01 会输出 ping: app01: Name or service not known


# 现在需要app01和app02能够使用容器名ping通
# 为了避免其它的影响,删除所有的容器 docker rm -f $(docker ps -aq)
# 宿主机执行 ifconfig,有docker0(172.17.0.1)、ens33(192.168.134.3)、lo(127.0.0.1)、virbr0(192.168.122.1)、br-157a94eac48e(172.21.0.1)
# 创建1个p_network网络,docker network ls 可以查看到这个新建的网络名,
#                       docker network inspect p_network 可以查看到IPAM节点下可以看到它的Gateway是172.21.0.1,Subnet是172.21.0.0/16,
docker network create p_network
# 然后执行下面的命令 创建1个容器,使用刚刚创建的p_network网络,
# 进入到容器内部,执行 ifconfig,有eth0(172.21.0.2)和lo(127.0.0.1)
# 在宿主机执行 ifconfig,有docker0(172.17.0.1)、ens33(192.168.134.3)、lo(127.0.0.1)、virbr0(192.168.122.1)、br-157a94eac48e(172.21.0.1)、veth8a9f762
# 在宿主机执行 docker ps,在ports可以看到这个容器的端口映射关系  0.0.0.0:8082->8080/tcp, :::8082->8080/tcp
# 在宿主机执行 docker inspect 容器id,看到使用networks节点的p_network,其中gateway就是p_network的172.21.0.1,IPAddress是172.21.0.2
#                                                                      其中NetworkID就是 docker network ls 命令查看到的 bridge的 NETWORK ID
# 在宿主机执行 docker network inspect p_network,看到IPAM节点下可以看到它的Gateway是172.21.0.1,Subnet是172.21.0.0,
#                                                并且看到Containers节点下 有这个容器id,并且IPv4Address是172.21.0.2
docker run --cap-add=SYS_PTRACE -d -p 8082:8080 --name app01 --network p_network app:1.0
# 然后继续执行下面的命令 创建另1个容器,继续使用刚刚创建的p_network网络,
# 进入到容器内部,执行 ifconfig,有eth0(172.17.0.3)和lo(127.0.0.1)
# 在宿主机执行 ifconfig,有有docker0(172.17.0.1)、ens33(192.168.134.3)、lo(127.0.0.1)、virbr0(192.168.122.1)、br-157a94eac48e(172.21.0.1)、veth8a9f762、veth3a3baf6
# 在宿主机执行 docker ps,在ports可以看到这个容器的端口映射关系 0.0.0.0:8083->8080/tcp, :::8083->8080/tcp
# 在宿主机执行 docker inspect 容器id,看到使用networks节点的bridge,其中gateway就是p_network的172.21.0.1,IPAddress是172.21.0.3
#                                                                   其中NetworkID就是 docker network ls 命令查看到的 bridge的 NETWORK ID
# 在宿主机执行 docker network inspect p_network,看到IPAM节点下可以看到它的Gateway是172.21.0.1,Subnet是172.21.0.0,
#                                                并且看到Containers节点下 有这个容器id,并且IPv4Address是172.21.0.3。还有上面那个容器的id和IPv4Address是172.17.0.2.
docker run --cap-add=SYS_PTRACE -d -p 8083:8080 --name app02 --network p_network app:1.0
# 2个容器启动之后
# 进入到容器1(它的ip是172.17.0.2),执行 ping app03 能正常ping通;
# 进入到容器2(它的ip是172.17.0.3),执行 ping app02 能正常ping通;



docker-compose
# Compose是Docker公司推出的一个工具软件,可以管理多个Docker容器组成一个应用。
# 你需要定义一个YAML格式的配置文件docker-compose.yml,写好多个容器之间的调用关系。
# 然后,只要一个命令,就能同时启动/关闭这些容器。

# docker建议我们每一个容器中只运行一个服务,因为docker容器本身占用资源极少,所以最好是将每个服务单独的分割开来
# 如果需要同时部署很多个服务,每个服务都要单独写Dockerfle然后在构建镜像,构建容器,所以docker官方给我们提供了docker-compose多服务部署的工具

# Compose允许用户通过一个单独的docker-compose.yml模板文件(YAML格式)来定义一组相关联的应用容器为一个项目project,
# 可以很容易地用一个配置文件定义一个多容器的应用,然后使用一条指令安装这个应用的所有依赖,完成构建。

例如:要实现一个Web微服务项目,除了Web服务容器本身,往往还需要再加上后端的数据库mysql服务容器,redis服务器,注册中心eureka,甚至还包括负载均衡容器等等…

# Docker-Compose解决了容器与容器之间如何管理编排的问题。

# 单独安装docker-compose
curl -SL https://github.com/docker/compose/releases/download/v2.23.3/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
# 赋予执行权限,读写权限
chmod +x /usr/local/bin/docker-compose
# 版本信息
docker-compose --version
# 建立软连接:
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

# 卸载 单独安装的docker-compose
sudo rm /usr/local/bin/docker-compose

# Compose使用的三个步骤
编写Dockerfile定义各个微服务应用并构建出对应的镜像文件
使用docker-compose.yml定义一个完整业务单元,安排好整体应用中的各个容器服务。
最后,执行docker-compose up命令来启动并运行整个应用程序,完成一键部署上线


# docker-compose.yml文件解读
version: "3.2"
services:
  nacos:
    image: nacos/nacos-server
    environment:
      MODE: standalone
    ports:
      - "8848:8848"     
  mysql:
    image: mysql:5.7.25 
    environment:
      MYSQL_ROOT_PASSWORD: 123456
    volumes:
      - "$PWD/mysql/data:/var/lib/mysql"
      - "$PWD/mysql/conf:/etc/mysql/conf.d/"     
  userservice:
    build: ./user-service
  orderservice:
    build: ./order-service
  gateway:
    build: ./gateway
    ports:
      - "10010:10010"
## 其中包含5个service服务
## image: nacos/nacos-server: 基于nacos/nacos-server镜像构建
## environment:环境变量 
## MODE: standalone:单点模式启动
## ports:端口映射,这里暴露了8848端口
## mysql:数据库
## image: mysql:5.7.25:镜像版本是mysql:5.7.25
## environment:环境变量
## MYSQL_ROOT_PASSWORD: 123:设置数据库root账户的密码为123
## volumes:数据卷挂载,这里挂载了mysql的data、conf目录,其中有我提前准备好的数据
## userservice、orderservice、gateway:都是基于Dockerfile临时构建的

# docker-compose.yml的编写规则介绍
# 以mysql服务为例,说明下常用字段的含义:
version: '3'#第一层:compose的版本号
services:   #第二层:服务配置信息
  mysql1:   #服务名
    image: mysql  #该服务所基于的镜像名
    environment:  #该服务的环境变量
      MYSQL_ROOT_PASSWORD: "ut.123456"
    ports:        #该服务的暴露端口
      - "3306:3306"
    container_name: "mysql1" #容器名
    networks: #该服务所加入的网络段
      - dev
    volumes:  #挂载数据卷
      - /platform/mysql/conf:/etc/my.cnf.d/my.cnf
      - /platform/mysql/data:/var/lib/mysql:rw"
networks:     #第三层:网络环境
  dev:
    driver: bridge

# Compose命令格式
docker compose 命令的基本格式为:docker-compose [-f …] [options] [COMMAND] [ARGS…]
# 下面命令要运行,当前目录必须要有docker-compose.yml, docker-compose.yaml, compose.yml, compose.yaml这些文件之一
docker-compose -h	                 # 查看帮助
docker-compose up	                 # 启动所有docker-compose服务
docker-compose up -d	             # 启动所有docker-compose服务并后台运行
docker-compose down	                 # 停止并删除容器、网络、卷、镜像。
docker-compose exec yml里面的服务id	 # 进入容器实例内部 docker-compose exec docker-compose.yml文件中写的服务id /bin/bash
docker-compose ps	                 # 展示当前docker-compose编排过的运行的所有容器
docker-compose top	                 # 展示当前docker-compose编排过的容器进程
docker-compose logs yml里面的服务id	 # 查看容器输出日志
docker-compose config	             # 检查配置
dokcer-compose config -q	         # 检查配置,有问题才有输出
docker-compose restart	             # 重启服务
docker-compose start	             # 启动服务
docker-compose stop	                 # 停止服务

# docker-compose命令示例
# 下面命令要运行,当前目录必须要有docker-compose.yml, docker-compose.yaml, compose.yml, compose.yaml这些文件之一
docker-compose ps	               显示所有容器
docker-compose build nginx	       构建nginx镜像
docker-compose up -d nginx	       构建启动nignx容器
docker-compose exec nginx bash	   登录到nginx容器中
docker-compose pause nginx	       暂停nignx容器
docker-compose unpause nginx	   恢复ningx容器
docker-compose start nginx	       启动nignx容器
docker-compose stop nginx	       停止nignx容器
docker-compose restart nginx	   重新启动nginx容器
docker-compose rm nginx	           删除nginx容器
docker-compose down	               删除nginx容器和镜像
docker-compose logs -f nginx	   查看nginx的实时日志

[docker-compose部署服务]
# 如果不使用docker-compose
#     需要先启动mysql和redis容器,
#     然后打包jar应用为镜像,启动应用镜像为容器应用为镜像,启动应用镜像为容器
#     容器间的启停或宕机,有可能导致IP地址对应的容器实例变化,映射出错

# 如果使用docker-compose
# 1. 首先将代码中的配置文件的ip改为docker-compose.yaml中定义的服务名或容器名,即mysql和redis或redis01,因为服务名作为主机来访问
# 2. 构建jar应用镜像 docker build -t docker-compose-test:1.0 .
# 基础镜像使用java
FROM centos7-java8:1.0
# 作者
MAINTAINER zzhua
# volume 指定临时文件为/tmp,在主机/var/lib/docker目录下创建了一个临时文件并链接到容器的/tmp
VOLUME /tmp
# 将jar包添加到容器中并更名为 docker-compose-test.jar
ADD spring-boot-docker-demo.jar docker-compose-test.jar
# 运行jar包
RUN bash -c 'touch /docker-compose-test.jar'
ENTRYPOINT ["java","-jar","/docker-compose-test.jar"]
# 暴露6001端口作为微服务
EXPOSE 6001
# 3. 编写docker-compose.yml,
#    注意:如果下面的内容直接复制到服务器上使用可能会报错
#          ERROR: .UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb6 in position 27: invalid start byte,
#          删除中文注释即可
# 版本号
version: "3"
services:
  # 定义服务名称
  microService:
    image: docker-compose-test:1.0
    container_name: ms01
    ports:
      - "6001:6001"
    volumes:
      - /app/microService:/data
    networks:
      - p_net
    depends_on:
      - redis
      - mysql
  redis:
    image: redis:6.0.8
	container_name: redis01
    ports:
      - "6379:6379"
    volumes:
      - /app/redis/conf/redis.conf:/etc/redis/redis.conf
      - /app/redis/data:/data
    networks:
      - p_net
    command: redis-server /etc/redis/redis.conf
  mysql:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: '123456'
      MYSQL_ALLOW_EMPTY_PASSWORD: 'no'
      MYSQL_DATABASE: 'db2024'
      MYSQL_USER: 'zzhua'
      MYSQL_PASSWORD: '123456'
    ports:
      - "3306:3306"
    volumes:
      - /app/mysql/db:/var/lib/mysql
      - /app/mysql/conf/my.cnf:/etc/my.cnf
      - /app/mysql/init:/docker-entrypoint-initdb.d
    networks:
      - p_net
networks:
  p_net:
# 4. 准备/app/redis/conf/redis.conf配置文件,而mysql也需要提前准备/app/mysql/conf/my.cnf配置文件
# 5. 镜像都准备好了,配置文件也准备好了,docker-compose.yml文件也准备好了
#    现在执行 docker-compose config -q 检查配置,有问题才输出
#    现在执行 docker-compose up -d 其中没有加container_name的会以当前文件夹名+镜像名+序号组成容器名称,
#                                      加了container_name的使用container_name作为容器名称
#    现在执行 docker-compose ps 查看由当前文件夹中的docker-compose.yml文件所构建的所有运行中的容器,包括启动失败退出的
#    可以执行 docker-compose logs 可以查看所有服务的启动日志
#    可以执行 docker-compose logs <服务名称>    可以查看指定服务的启动日志
#    可以执行 docker-compose logs -f <服务名称> 可以实时查看指定服务的启动日志
#    可以执行 docker-compose <服务名称>         可以查看指定服务的启动日志或启动错误日志
#    按照上面配置好并启动后,此时可以连接到mysql: 192.168.134.3 3306 root密码是123456,
#                                                连接成功发现db2024这个数据库已经创建好了,创建t_user表,并手动插入1条用户数据;
#                                    连接到redis:192.168.134.3 6379 连接成功; 
#    访问服务 http://192.168.134.3:6001/users/1,返回插入的用户数据,查看redis缓存中也有了数据,这说明整个服务已经部署成功。
#    可以执行 docker network ls 可以看到创建了1个新的网络 p_net
#    在宿主机执行 docker network inspect p_net 查看"Containers"节点,发现正好有这3个容器
#    可以执行 docker-compose exec microService bash 进入ms01容器内部,
#             但是不能使用 docker-compose exec ms01 bash进入到容器内部,
#             可以使用docker exec -it ms01 bash 进入ms01容器内部
#    进入ms01容器的内部,执行ping redis01,ping mysql 发现都是能正常ping通的,这说明这3个容器在同一个网络中

#    可以执行 docker-compose pause <服务名称>   可以暂停指定的服务
#    可以执行 docker-compose down 关闭当前docker-compose.yml中定义的所有容器服务,此时docker ps -a 将查看不到任何容器,但是挂载的数据不会删除


[docer的基本命令]
docker info
docker version
docker -v 
docker system df

# 查看所有容器
docker ps -a

# 删除所有容器
docker rm -f $(docker ps -aq)

# 查看容器日志
docker logs 容器id|容器名

# 进入容器
docker exec -it xxx bash

# 进入唯一的容器 通用方法
docker exec -it $(docker ps -aq) bash

# 删除所有dangling数据卷(即无用的Volume,僵尸文件)
docker volume rm $(docker volume ls -qf dangling=true)

# 删除所有关闭的容器
docker ps -a | grep Exit | cut -d ' ' -f 1 | xargs docker rm





























# 这种方式 访问 119.23.61.24/server1/location1 会拼接成 /home/www/myweb/server1/location1/index_sr1_location1.html 找到文件
location /server1/location1{
	root /home/www/myweb;
	index index_sr1_location1.html;
}

# 当找不到资源时,转发给当前所在server的/404.html路径,继续匹配
error_page 404 /404.html;
#配置错误页面转向
location = /404.html {
	root /home/www/myweb;
	index 404.html;
}

# 直接返回指定内容
location /get_text {
  default_type text/html; 
  return 200 '<h1>GOOD</h1>';
}

配置虚拟主机如下,并修改本地host
访问119.23.61.24/test时,返回119.23.61.24:80
访问www.zzhua-host.com/test时,返回www.zzhua-host.com:80
当所有没有匹配时,使用default_server,如果也没有配置default_server,则使用第一个server
server {
	listen       80;
	server_name  119.23.61.24;

	location /test {
	  default_type text/html; 
	  return 200 "119.23.61.24:80";
	}

}
server {
	listen       80;
	server_name  www.zzhua-host.com;

	location /test {
	  default_type text/html; 
	  return 200 "www.zzhua-host.com:80";
	}
}

location /abc 匹配/abc,/abc?p1=TOM,/abc/,/abcd123,/abc/aaa
location =/abc 表示精确匹配 只能匹配 /abc ,/abc?p1=TOM,不能匹配 /abc/
location ~^/abc\w$  用于表示当前uri中包含了正则表达式,并且区分大小写
location ~*^/abc\w$ 用于表示当前uri中包含了正则表达式,并且不区分大小写
location ^~/abc     用于表示不包含正则表达式的uri,比~的优先级更高

# root 设置请求的根目录 
# root处理方式:root路径+location路径
# 所以使用root时,随便加不加斜杠
# 当访问 119.23.61.24时,会访问到/usr/share/nginx/html/index.html
# 当访问 119.23.61.24/时,会访问到/usr/share/nginx/html/index.html
# 当访问 119.23.61.24/a.txt时,会访问到/usr/share/nginx/html/a.txt
# 当访问 119.23.61.24/dist/logo.png时,会访问到/usr/share/nginx/html/dist/logo.png
location / {
	root   /usr/share/nginx/html;
	index  index.html index.htm;
}

# 当访问 119.23.61.24/images/test.png时,会访问到/usr/share/nginx/html/images/test.png
location /images {
	root /usr/share/nginx/html;
}

# 当访问 119.23.61.24/images/test.png时,会访问到/usr/share/nginx/html/images/test.png
location /images {
	root /usr/share/nginx/html/;
}

# 当访问 119.23.61.24/images/test.png时,会访问到/usr/share/nginx/html/images/test.png
location /images/ {
	root /usr/share/nginx/html/;
}

# 当访问 119.23.61.24/images/test.png时,会访问到/usr/share/nginx/html/images/test.png
location /images/ {
	root /usr/share/nginx/html;
}

# 这里root配置的是html,这个目录可以通过nginx -V查看到--prefix选项的值再拼接上html
location / {
	root html;
}


# alias 设置请求的根目录 
# alias处理方式:使用alias路径替换location路径
# 当访问 119.23.61.24/images/test.png时,会访问到/usr/share/nginx/html/images/test.png
location /images {
	alias  /usr/share/nginx/html/images;
}

# 当访问 119.23.61.24/images/test.png时,返回404,
# 因为alias用的是替换,也就是会访问到 /usr/share/nginx/html/test.png,由于这个文件不存在,因此404
location /images {
	alias  /usr/share/nginx/html;
}

# 当访问 119.23.61.24/images/test.png时,正常返回
# 当使用alias时,当上面加了/,下面也必须加/,因为alias用的是替换
location /images/ {
	alias  /usr/share/nginx/html/;
}

# index:设置网站的默认首页
# index后面可以跟多个设置,中间使用空格隔开,如果访问的时候没有指定具体访问的资源,则会依次进行查找,找到第一个为止。
location / {
	root   /usr/share/nginx/html;
	index  index.html index.htm;
}

# error_page:设置网站的错误页面,即,当出现对应的响应code后,如何来处理。
# 可以指定具体跳转的地址,必须使用http开头
server {
	error_page 404 http://www.baidu.com;
}
# 可以指定重定向地址
server{
	error_page 404 /50x.html;
	error_page 500 502 503 504 /50x.html;
	location =/50x.html{
		root html;
	}
}
# 使用location的@符合完成错误信息展示
server{
	error_page 404 @jump_to_error;
	location @jump_to_error {
		default_type text/plain;
		return 404 'Not Found Page...';
	}
}

# add_header用于添加响应头
add_header My-Header zzhua;


# 反向代理
# proxy_pass 请求转发
# proxy_pass规则1. proxy_pass代理地址端口后无任何字符,转发后地址:代理地址+访问URL目录部分
# proxy_pass规则2. proxy_pass代理地址端口后有目录(包括 / ),转发后地址:代理地址+访问URL目录部分去除location匹配目录
# 现访问http://localhost:8082/api/index.html,http://localhost:8082/api/blogs 能正常返回数据
# 配置proxy_pass, nginx将请求转发到8082的服务上去
# 访问http://localhost:8082/api/blogs ,http://localhost/api/index.html 能正常返回数据
server {
	listen 80;
	server_name localhost;
	location / {
		proxy_pass http://localhost:8082;
	}
	location /test { # 用于测试配置修改后是否生效
		default_type text/html;
		return 200 'halo1';
	}
}



# proxy_set_header 该指令可以更改Nginx服务器接收到的客户端请求的请求头信息,然后将新的请求头发送给代理的服务器
# 以下配置了2个server,80服务器收到/api的请求后,添加username请求头,转发给81服务,81服务获取到这个请求头然后返回
http {
    include       mime.types;
    default_type  application/octet-stream;
	
	server {
		listen 80;
		server_name localhost;
		location /api {
			default_type text/html;
			proxy_pass http://localhost:81;
			proxy_set_header username tom;
		}
		location /test { # 用于测试配置修改后是否生效
			default_type text/html;
			return 200 'halo5';
		}
	}
	
	server {
		listen 81;
		server_name localhost;
		location / {
		    default_type text/html;
			return 200 $http_username;
		}
	}
}

## 负载均衡配置
upstream backend{
	server 192.168.200.146:9001 down;    # 此服务器将不可用
	server 192.168.200.146:9002 backup;  # 当9003服务器不可用时,才会把请求交给9002处理
	server 192.168.200.146:9003;
}
server {
	listen 8083;
	server_name localhost;
	location /{
		proxy_pass http://backend;
	}
}

## 负载均衡配置设置权重
upstream backend{
	server 192.168.200.146:9001 weight=10;
	server 192.168.200.146:9002 weight=5;
	server 192.168.200.146:9003 weight=3;
}
server {
	listen 8083;
	server_name localhost;
	location /{
		proxy_pass http://backend;
	}
}

## 负载均衡配置设置ip_hash
upstream backend{
	ip_hash;
	server 192.168.200.146:9001;
	server 192.168.200.146:9002;
	server 192.168.200.146:9003;
}
server {
	listen 8083;
	server_name localhost;
	location /{
		proxy_pass http://backend;
	}
}

## nginx制作下载站点 
# 浏览器访问 http://119.23.61.24/images/,注意因为location后面带了斜杠,所以访问的时候路径最后面也要带斜杠
# 浏览器访问 http://119.23.61.24/images/test.png 即可查看/usr/share/nginx/html/images/test.png这个文件
location /images/ {
	root /usr/share/nginx/html;
	# 强制浏览器下载
	# add_header Content-Disposition 'attachment';
	autoindex on;
	autoindex_exact_size on;
	autoindex_format html;
	autoindex_localtime on;
} 

## websocket配置

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$http_origin = $http_host = $remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    # include /etc/nginx/conf.d/*.conf;
    
    # 配合下面的 location /ws/ 一起使用
    # 定义变量,兼容HTTP和WebSocket两种请求协议
    map $http_upgrade $connection_upgrade {
       default          keep-alive;  # 默认 keep-alive,表示HTTP协议。
       'websocket'      upgrade;     # 若是 websocket 请求,则升级协议 upgrade。
    }
     
    server {
       listen 80;
       server_name 192.168.134.3;
       location / {
           root /usr/share/nginx/html/dist;
           index index.html index.htm;
           try_files $uri $uri/ /index.html;
       }
       
       # 可以访问 http://192.168.134.3:8080/test01得到数据,现在只需要访问 http://192.168.134.3/api/test 即可,将转发到http://192.168.134.3:8080/test01
       location ^~/api/ {
       
           proxy_pass http://192.168.134.3:8080/;
           
           # 客户端要访问的主机,客户端会携带Host请求头,转发的目标服务能够拿到这个请求头值192.168.134.3;如果不配置这个,那么转发的目标服务拿到的是nginx设置的192.168.134.3:8080
           proxy_set_header   Host             $host;
           
           # 客户端的真实ip,这个值由nginx自己解析出来
           proxy_set_header   X-Real-IP        $remote_addr;
           
           # 由于request.getRemoteAddr()默认获取到的是TCP层直接连接的客户端的IP,
           # 对于Web应用服务器来说由于经过nginx转发,所以直接连接它的客户端实际上是Nginx,也就是TCP层是拿不到真实客户端的IP。
           # 为了解决上面的问题,很多HTTP代理会在HTTP协议头中添加X-Forwarded-For头,用来追踪请求的来源。
           # X-Forwarded-For包含多个IP地址,每个值通过逗号+空格分开,最左边(client1)是最原始客户端的IP地址,
           # 中间如果有多层代理,每一层代理会将连接它的客户端IP追加在X-Forwarded-For右边。
           # 获取客户端真实IP的方法,首先从HTTP头中获取X-Forwarded-For,如果X-Forwarded-For头存在就按逗号分隔取最左边第一个IP地址,不存在就通过X-Real-IP去拿,
           #                         如何还为空,就通过request.getRemoteAddr()获取IP地址
           proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
           
           #Access-Control-Allow-Headers(请求头,响应头,预请求)     (携带Cookie情况下不能为*)
           #Access-Control-Allow-Methods(请求头,响应头,预请求)     (携带Cookie情况下不能为*)
           #Access-Control-Allow-Origin(响应头,预请求/正常请求)     (携带Cookie情况下不能为*)
           #Access-Control-Allow-Credentials(响应头,预请求/正常请求)(携带Cookie情况下要设置为true)
           #Access-Control-Max-Age(响应头,预请求)(单位s)
           
           # 允许跨域请求的域,* 代表所有
           # 注意当使用axios时,并且withCredentials为true时,则会在请求中传递cookie,此时不能使用*,否则会报错: 
           #                    The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'
           # 注意当要使用多个origin值时,可以使用nginx的map指令来根据$http_origin来映射不同的值
           # 注意当允许所有Origin源都能访问,可以直接设置为$http_origin
      	   add_header 'Access-Control-Allow-Origin' $http_origin;
            
    	   # 允许带上cookie请求
           # 如果服务器返回任何set-cookie响应头, 那么必须返回Access-Control-Allow-Credentials: true, 否则将不会在客户端上创建cookie
		   add_header 'Access-Control-Allow-Credentials' 'true';
		   
		   # 允许请求的方法,比如 GET/POST/PUT/DELETE
		   add_header 'Access-Control-Allow-Methods' 'GET,POST,DELETE,PUT';
		   
		   # 允许客户端携带请求头
           # 否则报错:Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
    			 add_header 'Access-Control-Allow-Headers' 'Content-Type,Authorization,token,X-Requested-With,x-token';
            
           # 保留20天
           add_header 'Access-Control-Max-Age' 1728000;
            
           # 允许暴露给客户端的响应头
           # add_header 'Access-Control-Expose-Headers' 'token,Authorization';
           
           # 这里用来测试,当客户端发出预检请求时,响应时没有这个响应头。说明预检请求只会携带下面if中的请求头,不会携带这里定义的响应头。
           add_header 'outer' 'outer-value';

           # 预检请求的处理
           if ($request_method = 'OPTIONS') {
           
              add_header 'Content-Type' 'text/plain;charset=UTF-8';
              add_header 'Content-Length' 0;
              # 这里必须要加这个响应头,否则报错: Response to preflight request doesn't pass access control check: 
              #                                   No 'Access-Control-Allow-Origin' header is present on the requested resource.
              add_header 'Access-Control-Allow-Origin' '$http_origin';
              
              # 这里必须要加这个响应头,否则报错: Response to preflight request doesn't pass access control check: 
              #                                   Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
              #add_header 'Access-Control-Allow-Headers' '*';
              # 这里必须要加这个响应头,否则报错: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Credentials'
              #                                   header in the response is '' which must be 'true' when the request's credentials mode is 'include'. 
              #                                   The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
              add_header 'Access-Control-Allow-Credentials' 'true';
              
              # 携带Cookie情况下不能为*
              # add_header 'Access-Control-Allow-Headers' '*';
              # 当axios开启withCredentials时,必须添加允许跨域请求携带的请求头,非预检请求的非简单的跨域请求会报错:
              #                               Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
              add_header 'Access-Control-Allow-Headers' 'Content-Type,x-token';
              
              # 保留20天
              add_header 'Access-Control-Max-Age' 1728000;
              
              add_header 'inner' 'inner-value';
              
              return 204;
           }
       }
       
       # 客户端可以 ws://192.168.134.3:8080/wsTest/websocket,现在客户端请求 ws://192.168.134.3/wsTest/websocket,将被转发到 http://192.168.134.3:8080/wsTest/websocket
       location ~^/wsTest/ {
       
           proxy_redirect off;           
           proxy_pass http://192.168.134.3:8080;
           
           proxy_http_version 1.1;
           # 如果不配置这个 如果客户端一直不发送消息过来,经测试默认1分钟之后连接会关闭。所以需要心跳机制。
           proxy_read_timeout 36000s;
           proxy_send_timeout 36000s;
           
           # 升级协议头 websocket
           # 浏览器会携带Connection头: Upgrade;Upgrade头: websocket;
           proxy_set_header Connection "Upgrade";
           proxy_set_header Upgrade $http_upgrade;
           
           # proxy_set_header Host $host;
           proxy_set_header X-Real_IP $remote_addr;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           proxy_set_header X-Forwarded-Proto $scheme; 
       }
       

       # 配合上面的 map $http_upgrade $connection_upgrade 一起使用
       # 客户端可以 ws://192.168.134.3:8080/wsTest/websocket,现在客户端请求 ws://192.168.134.3/ws/wsTest/websocket,将被转发到 http://192.168.134.3:8080/wsTest/websocket
       location /ws/ {
            proxy_pass http://192.168.134.3:8080/; # 转发到后端接口
    		# 如果不配置这个 如果客户端一直不发送消息过来,经测试默认1分钟之后连接会关闭。所以需要加上心跳机制。
            proxy_read_timeout 36000s;
            proxy_send_timeout 36000s;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
        }
    }
}




## ssl证书配置





最简单的nginx配置文件

user  nobody;
worker_processes  1;
events {
    worker_connections  1024;
} 
http {
    include       mime.types;
    default_type  application/octet-stream;
 
    sendfile        on;
 
    keepalive_timeout  65;
 
    # 开启GZIP压缩
    gzip on;
    gzip_buffers 32 4K;
    gzip_comp_level 6;
    gzip_min_length 100;
    gzip_types application/javascript text/css text/xml;
    #配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)
    gzip_disable "MSIE [1-6]\."; 
    gzip_vary on;
  
    server {
        listen       80;
        server_name  reggie;
 
        location / {
            # Vue项目文件的存的路径
            root   /usr/local/projects/reggie/reggie_vue/dist;
            try_files $uri $uri/ /index.html;
            # index  index.html index.htm;
        }
 
        # 配置反向代理 后端接口
        location ^~/api/ {
            # reggieboot-container 为Spring Boot项目的容器名称
            proxy_pass http://reggieboot-container:8080/;
        }
 
        # 配置反向代理 图片资源
        location ^~/reggie_images/ {
             # reggieboot-container 为Spring Boot项目的容器名称
            proxy_pass http://reggieboot-container:8080/reggie_images/;
        }
 
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    } 
}

http://www.kler.cn/a/380806.html

相关文章:

  • git commit应遵循的提交规范
  • kafka里的consumer 是推还是拉?
  • 学习虚幻C++开发日志——定时器
  • 气象大模型学习笔记
  • Php实现钉钉OA一级审批,二级审批
  • 测长机在测量长度尺寸方面有哪些优势?如何保证测量的准确性?
  • Mac电脑技巧:适用于Mac的免费外置硬盘数据恢复软件
  • FreeRTOS 队列详解
  • 【spark的集群模式搭建】Standalone集群模式的搭建(简单明了的安装教程)
  • Mybatis 注意传递多种参数,不一定都有参数值,用xml如何写出查询语句
  • IntelliJ IDEA插件开发-核心概念介绍
  • 【JavaScript】JavaScript开篇基础(4)
  • windows_worm
  • 医院信息化与智能化系统(15)
  • JVM结构图
  • 解决虚拟机启动报:此主机支持AMD-V,但AMD-V处于禁用状态
  • 基于Multisim光控夜灯LED电路带计时功能(含仿真和报告)
  • QT 实现自定义开机加载动画二
  • [Web安全 网络安全]-学习文章汇总导航(持续更新中)
  • k8s的发展历史
  • 1251. 平均售价(left join on后面加条件和where 后面加条件的区别、nvl()函数的使用)
  • 如何在 IntelliJ IDEA 中调整 `Ctrl+/` 快捷键生成注释的位置
  • Percona XtraBackup数据备份方案
  • Java学习教程,从入门到精通,Java对象和类语法知识点(20)
  • pdf转图片
  • Angular解析本地json文件