docker 容器,镜像,compose相关

镜像

获取镜像

docker pull 拉取镜像

格式: docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

  1. Docker Registry 地址:<域名/IP>[:端口号], 默认地址为Docker Hub
  2. 仓库名:用户名/软件名,在Docker Hub上,如果不写用户名,则默认为library(官方镜像)

docker run 以镜像启动并运行一个容器

格式:docker run [选项] 镜像标识 [启动后要执行命令] [参数...]

eg: docker run -it --rm -p 4000:80 ubuntu:18.04 bash
eg: docker run -it --rm -p 4000:80 ubuntu:18.04 ls
eg: docker run -d --rm -p 4000:80 ubuntu:18.04 ls

  1. 选项:

    • -it:这是两个参数,-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i 则让容器的标准输入保持打开。我们这里打算进入 bash 执行一些命令并查看返回结果,因此我们需要交互式终端。最后通过exit退出,如果没有这个参数,则CRTL+C退出
    • --rm:容器退出后随之将其删除。
    • -d:后台守护运行
    • -p:将容器内的端口与主机绑定,将容器的80端口绑定到主机4000上
    • 镜像标识:可以是镜像的名字+版本号,也可以是镜像的ID,如果没有版本号,则默认是:latest版本
  2. 如果是镜像的名字,并且本地没有,则会默认先执行docker pull将镜像下载再启动。

镜像查询

列出镜像

docker images或者docker image ls

列表中只会显示顶层镜像,一般来说这些信息就够了

$ docker images

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
python              2.7-slim            99079b24ed51        12 days ago         120MB
node                10.15.1             8fc2110c6978        2 weeks ago         897MB
<none>              <none>              7b25fb2216b1        2 days ago          1.08GB
nginx               latest              f09fe80eb0e7        2 weeks ago         109MB

仓库名标签镜像ID创建时间镜像体积

列表上镜像体积总和并非所有镜像实际硬盘消耗空间。因为docker镜像是多层存储,可以继承和复用,所以不同镜像可能使用相同的基础镜像,也就是共同层。

查看镜像占用空间

docker system df

$ docker system df

TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
Images              9                   2                   1.895GB             1.663GB (87%)
Containers          2                   0                   2B                  2B (100%)
Local Volumes       0                   0                   0B                  0B
Build Cache         0                   0                   0B                  0B

虚悬镜像(dangling image)

一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为 <none>。

上面列表中的:

<none>              <none>              7b25fb2216b1        2 days ago          1.08GB

这个镜像原本是有镜像名和标签,出现这种情况的原因:

  1. 随镜像维护,发布新版本后,重新docker pull时,这个镜像名被转移到新下载的镜像上,旧的镜像名称则被取消
  2. docker build时,由于新旧镜像同名,旧镜像的名称则被取消了

专门过滤查询

docker images -f dangling=true

$ docker images -f dangling=true

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
<none>              <none>              7b25fb2216b1        2 days ago          1.08GB

一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用docker image prune删除。

中间层镜像

默认的docker image ls或者docker images列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 -a 参数。

$ docker images -a

这样会看到很多无标签的镜像,与之前的虚悬镜像不同,这些无标签的镜像很多都是中间层镜像,是其它镜像所依赖的镜像。这些无标签镜像不应该删除,否则会导致上层镜像因为依赖丢失而出错

过滤查询镜像

docker images [参数或者条件]

  1. 根据仓库名列出镜像
$ docker images ubuntu
  1. 列出特定的某个镜像,也就是说指定仓库名和标签
$ docker image ls ubuntu:18.04
  1. 过滤器参数 --filter或者-f
    • 查询虚悬镜像: -f dangling=true
    • 查询在某个镜像建立之后的镜像: -f since=ubuntu:18.04
    • 之前: -f before=ubuntu:18.04
    • 如果镜像构建时,定义了LABEL,还可以通过LABEL来过滤: label=com.example.version=0.1

输出格式

  1. 只输出镜像ID
$ docker images -q

5f515359c7f8
05a60462f8ba
fe9198c04d62
  1. 自定义输出
# 包含镜像ID和仓库名
$ docker image ls --format "{{.ID}}: {{.Repository}}"

05a60462f8ba: nginx
00285df0df87: <none>
f753707788c5: ubuntu
f753707788c5: ubuntu
1e0c3dd64ccd: ubuntu

# 以表格等距显示,并且有标题行,和默认一样,不过自己定义列:
$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"

IMAGE ID            REPOSITORY          TAG
05a60462f8ba        nginx               latest
00285df0df87        <none>              <none>
f753707788c5        ubuntu              18.04
f753707788c5        ubuntu              latest

删除本地镜像

docker image rm或者docker rmi

格式:docker image rm [选项] <镜像标识1> [<镜像标识2> ...]

镜像标识: 镜像ID,仓库名:标签,镜像摘要(太麻烦了,这个不常用)

删除行为

  1. Untagged 删除当前指向的标签,因为同一个镜像有多个标签,当删除所指定的标签后,可能别的标签指向了这个镜像,这是不会删除
  2. Deleted 真正删除

组合批量删除

# 删除仓库名为node的所有标签镜像
$ docker image rm $(docker image ls -q node)

# 删除创建ubuntu:18.04之前的所有
$ docker image rm $(docker image ls -q -f before=ubuntu:18.04)

镜像保存

类似于git commit,不过一般不会使用,而是用写Dockerfile的形式生成。因为通过commit生成的,会把每一层的操作都记录下来,包括一些不必要的操作,最后可能会导致镜像臃肿。

格式:docker commit [选项] <容器ID或者容器名> [仓库名[:标签]]

eg: docker commit --auhtor "XXX@xxx.com" --message "修改了什么" server nginx:test

用Dockerfile定制镜像

Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

  1. 在一个空白目录中,建立一个文本文件,命名为Dockerfile
  2. Dockerfile内容为:
FROM nginx
RUN echo '<h1>hello world!</h1>' > /usr/share/nginx/html/index.html
  1. 构建镜像

格式:docker build [选项] <上下文路径或者URL或者tar压缩包>

eg: docker build -t nginx:test1 .

以当前目录为上下文构建名为nginx:test1的镜像

eg: docker build https://github.com/twang2218/gitlab-ce-zh.git#:11.1

这行命令指定了构建所需的 Git repo,并且指定默认的 master 分支,构建目录为 /11.1/,然后 Docker 就会自己去 git clone 这个项目、切换到指定分支、并进入到指定目录后开始构建。

eg: docker build http://server/context.tar.gz

如果所给出的 URL 不是个 Git repo,而是个 tar 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。

  • 上下文: 构建镜像时的基础目录,所有的构建所需文件都不能在这个上下文目录之外,在构建时会将上下文中的所有文件打包(除了.dockerignore中忽略的),同时在Dockerfile中的命令的默认相对路径会以这个上下文路径为基础,当然也不能超出这个目录

  • 默认读取的是当前执行命令的路径下名为Dockerfile的文件,也可以手动指定-f ../node.Dockerfile

容器

容器是独立运行的一个或一组应用,以及它们的运行态环境。

容器启动

新建并启动

docker run命令,在获取镜像里面有,就不单说了

启动时的流程:

  1. 检查本地是否存在指定的镜像,不存在就从公有仓库下载
  2. 利用镜像创建并启动一个容器
  3. 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
  4. 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  5. 从地址池配置一个 ip 地址给容器
  6. 执行用户指定的应用程序
  7. 执行完毕后容器被终止

启动已经终止的容器

格式:docker container start [选项] 容器标识1 [容器标识2...]

守护态运行

后台运行

增加参数-d

eg: docker run -d nginx

使用了-d命令后,容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用docker logs查看)。

docker container logs [容器ID或者容器名]

查看容器信息

docker container ls

守护态运行

容器是否会一直运行,与docker run指定的命令有关,和-d无关

eg:

# 不会一直运行的
$ docker run -d ubuntu:18.04 ls

b64f8e71882d26cffa34453284b40f73cbdc66054cd8a0ba79cb73092dc76726

$ docker logs b64f8e71882d26cffa34453284b40f73cbdc66054cd8a0ba79cb73092dc76726

bin
boot
dev
etc
...

# node index.js 是启一个服务,所以会保持运行在后台,如果没有-d就是保持运行在前台
$ docker run -d node node index.js

终止容器

除了在容器中关闭运行的应用来关闭容器外,也可以使用docker container stop来终止,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止。

格式:docker container stop 容器标识

终止状态的容器可以用 docker container ls -a 命令看到

重新启动终止态容器

docker container start

启动处于终止状态的容器

重启容器

docker container restart

会将一个运行态的容器终止,然后再重新启动它。

进入容器

在使用 -d 参数时,容器启动后会进入后台。

某些时候需要进入容器进行操作,包括使用docker attach命令或docker exec命令,推荐使用 docker exec 命令

因为在attach中,使用exit会导致容器停止,而exec不会

$ docker run -dit ubuntu

39ee1ddf67b9819fca8bbe71e15a131a6f4df18efabd229d199dabc744ce5be5

$ docker container ls

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
39ee1ddf67b9        ubuntu:18.04        "/bin/bash"         36 seconds ago      Up 35 seconds                           elegant_lumiere      

# 没有-t时,没有分配伪终端,但是命令执行结果任然可以返回
$ docker exec -i 39ee1ddf67b9 bash

$ ls

bin
boot
dev
...

# 分配了伪终端
$ docker exec -it 39ee1ddf67b9 bash

root@39ee1ddf67b9:/#

容器的导入和导出

导出容器

导出容器快照

docker export

eg:

$ docker container ls -a

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
7691a814370e        ubuntu:18.04        "/bin/bash"         36 hours ago        Exited (0) 21 hours ago                       test

$ docker export 7691a814370e > ubuntu.tar

导入容器

从容器快照中导入为镜像

docker import

eg:

$ cat ubuntu.tar | docker import - test/ubuntu:test1
$ docker image ls

REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
test/ubuntu         test1               9d37a6082e97        About a minute ago   171.3 MB

此外,也可以通过指定 URL 或者某个目录来导入

docker import http://example.com/exampleimage.tgz example/imagerepo

用户既可以使用docker load来导入镜像存储文件到本地镜像库,也可以使用docker import来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。

删除容器

删除终止状态的容器

docker container rm

$ docker container rm test
test

如果需要删除运行中的容器,需要加一个-f的参数

清理所有处于终止状态的容器

docker container prune

容器间通信

新建网络

$ docker network create -d bridge web

a0ff52c9783cf21da40c2c62ed81765d165658e591b04f7e1c139fe689f4fe9e

$ docker network ls

NETWORK ID          NAME                DRIVER              SCOPE
a0ff52c9783c        web                 bridge              local

创建一个名为web的网络

-d指定网络类型,有bridgeoverlay。其中overlay用于Swarm mode

将容器连接到网络上

运行一个容器并连接到新建的网络上

$ docker run -it --rm --name node1 --network web node sh

再开一个新终端

$ docker run -it --rm --name node2 --network web node sh

再开一个终端

$ docker container ls

CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
f2b50d6261a7        node                "node"              26 seconds ago       Up 25 seconds                           node2
da2b12415ff8        node                "node"              About a minute ago   Up About a minute                       node1

连接测试

在node1中

$ ping node2

PING node2 (172.25.0.3) 56(84) bytes of data.
64 bytes from node2.web (172.25.0.3): icmp_seq=1 ttl=64 time=0.148 ms
64 bytes from node2.web (172.25.0.3): icmp_seq=2 ttl=64 time=0.168 ms

在node2中

$ ping node1

PING node1 (172.25.0.2) 56(84) bytes of data.
64 bytes from node1.web (172.25.0.2): icmp_seq=1 ttl=64 time=0.199 ms
64 bytes from node1.web (172.25.0.2): icmp_seq=2 ttl=64 time=0.174 ms

Docker Compose

通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)

Compose 中有两个重要的概念:

  1. 服务 (service):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。

  2. 项目 (project):由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml 文件中定义。

Compose 的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。

基础使用

web应用

新建文件夹,在其中创建server.js

const http = require('http')

const app = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' })
    console.log(`<h1>【${req.method}】: ${req.url}</h1>`)
    const resText = `<h1>【${req.method}】: ${req.url}</h1>`
    res.end(resText)
})
app.listen(4000)

Dockerfile

FROM node

ADD . /web

WORKDIR /web

RUN npm config set registry http://registry.npm.taobao.org/ \
    && npm install

CMD ["node", "./server.js"]

docker-compose.yml

version: "3"

services:
    web:
        container_name: web
        build:
            context: ./
            dockerfile: Dockerfile
        port:
            - "4001:4000"

运行compose

$ docker-compose up

Compose 命令

命令格式

$ docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]

命令选项

  1. -f, --file指定使用的Compose模板文件,默认为当前目录的docker-compose.yml,可以多次指定

  2. -p, --project-name指定项目别名,默认将使用所在目录名称作为项目名

  3. --x-networking使用Docker的可拔插网络后端特性

  4. --x-network-driver指定网络后端的驱动,默认为bridge

  5. --verbose输出更多调试信息

  6. -v, --version打印版本并退出

相关命令使用

build

格式:docker-compose build [options] [SERVICE...]

构建(重新构建)项目中的服务容器。

服务容器一旦构建后,将会带上一个标记名

可以随时在项目目录下运行 docker-compose build 来重新构建服务。

选项:

  1. --force-rm 删除构建过程中的临时容器

  2. --no-cache 构建镜像过程中不使用cache(会使构建过程时间变长)

  3. --pull 始终尝试通过pull来获取更新版本的镜像

config

验证 Compose 文件格式是否正确,若正确则显示配置,若格式错误显示错误原因。

down

此命令将会停止 up 命令所启动的容器,并移除网络

exec

进入指定的容器。

images

列出 Compose 文件中包含的镜像。

kill

格式:docker-compose kill [options] [SERVICE...]

logs

格式:docker-compose logs [options] [SERVICE...]

查看服务容器的输出。默认情况下,docker-compose 将对不同的服务输出使用不同的颜色来区分。可以通过--no-color来关闭颜色。

pause

格式:docker-compose pause [SERVICE...]

暂停一个服务容器。

ps

格式:docker-compose ps [options] [SERVICE...]

列出项目中目前的所有容器。

选项:

  1. -q只打印容器的 ID 信息。

pull

格式:docker-compose pull [options] [SERVICE...]

拉取服务依赖的镜像

选项:

  1. --ignore-pull-failures忽略拉取镜像过程中的错误。

push

推送服务依赖的镜像到 Docker 镜像仓库。

restart

格式:docker-compose restart [options] [SERVICE...]

重启项目中的服务

选项:

  1. -t, --timeout指定重启前停止容器的超时(默认为10秒)

rm

格式:docker-compose rm [options] [SERVICE...]

删除所有(停止状态的)服务容器。推荐先执行 docker-compose stop 命令来停止容器。

选项:

  1. -f, --force强制直接删除,包括非停止状态的容器。一般尽量不要使用该选项。
  2. -v删除容器所挂载的数据卷

run

格式:docker-compose run [options] [-p PORT...] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]

在指定服务上执行一个命令。

eg:

$ docker-compose run ubuntu ping www.baidu.com

默认情况下,如果存在关联,则所有关联的服务将会自动被启动,除非这些服务已经在运行中。

如果不希望自动启动关联的容器,可以使用 --no-deps 选项,将不会启动 web 容器所关联的其它容器。

$ docker-compose run --no-deps web python manage.py shell

该命令类似启动容器后运行指定的命令,相关卷、链接等等都将会按照配置自动创建。

与启动容器后运行指定的命令不同点:

  1. 给定命令将会覆盖原有的自动运行命令;

  2. 不会自动创建端口,以避免冲突。

选项:

  1. -d后台运行容器

  2. --name为容器起一个别名

  3. --entrypoint覆盖默认的容器启动指令(CMD)

  4. -e KEY=VAL设置环境变量值,可多次使用选项设置多个环境

  5. -u, --user="" 指定运行容器的用户名或者uid

  6. --no-deps不自动启动关联的服务容器

  7. --rm运行命令后自动删除容器,-d模式下忽略

  8. -p, --publish=[]映射容器端口到本地主机

  9. --service-ports配置服务端口并映射到本地主机

scale

格式:docker-compose scale [options] [SERVICE=NUM...]

设置指定服务运行的容器个数。

通过 service=num 的参数来设置数量。

eg:

$ docker-compose scale web=3 db=2

将启动 3 个容器运行 web 服务,2 个容器运行 db 服务。

一般的,当指定数目多于该服务当前实际运行容器,将新创建并启动容器;反之,将停止容器。

选项:

  1. -t, --timeout 停止容器时候的超时(默认10秒)

start

格式:docker-compose start [SERVICE...]

启动已经存在的服务容器。

stop

格式:docker-compose stop [options] [SERVICE...]

停止已经处于运行状态的容器,但不删除它。通过 docker-compose start 可以再次启动这些容器。

选项:

  1. -t同上

top

查看各个服务容器内运行的进程

unpause

格式:docker-compose unpause [SERVICE...]

恢复处于暂停状态中的服务。

up

格式:docker-compose up [options] [SERVICE...]

该命令十分强大,它将尝试自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器的一系列操作。

链接的服务都将会被自动启动,除非已经处于运行状态。

可以说,大部分时候都可以直接通过该命令来启动一个项目。

默认情况,docker-compose up启动的容器都在前台,控制台将会同时打印所有容器的输出信息,可以很方便进行调试。

当通过Ctrl-C停止命令时,所有容器将会停止。

如果使用docker-compose up -d,将会在后台启动并运行所有的容器。一般推荐生产环境下使用该选项。

默认情况,如果服务容器已经存在,docker-compose up将会尝试停止容器,然后重新创建(保持使用volumes-from挂载的卷),以保证新启动的服务匹配docker-compose.yml文件的最新内容。如果用户不希望容器被停止并重新创建,可以使用docker-compose up --no-recreate。这样将只会启动处于停止状态的容器,而忽略已经运行的服务。如果用户只想重新部署某个服务,可以使用docker-compose up --no-deps -d <SERVICE_NAME>来重新创建服务并后台停止旧服务,启动新服务,并不会影响到其所依赖的服务。

选项:

  1. -d在后台运行服务容器

  2. --no-color不使用颜色来区分不同的服务的控制台输出

  3. --no-deps不启动服务所链接的容器

  4. --force-recreate强制重新创建容器,不能与--no-recreate同时使用

  5. --no-recreate如果容器已经存在,则不重新创建,不能与--force-recreate同时使用

  6. --no-build不自动构建缺失的服务镜像

  7. -t, --timeout 同上

version

格式:docker-compose version

打印版本信息

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容