当你的一个应用需要用到多个docker镜像时,你会怎么做,写一个bash/Python脚本吗? 其实docker已经有工具可以来定义和运行复杂的应用,这就是docker-compose

安装

docker-compose使用Python编写,已加入Pypi,所以安装只需要

pip3 install docker-compose

使用

术语

场景

最常见的项目是 web 网站,该项目应该包含 web 应用和缓存。

下面我们用 Python 来建立一个能够记录页面访问次数的 web 网站。

Web应用

新建文件夹,在文件夹中创建app.py

from flask import Flask
from redis import Redis

app = Flask(__name__)
redis = Redis(host='redis', port=6379)

@app.route('/')
def hello():
    count = redis.incr('hits')
    return 'Hello World! 该页面已被访问 {} 次。\n'.format(count)

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)

Dockerfile

文件夹中还需要Dockerfile

FROM python:3.6-alpine
ADD . /code
WORKDIR /code
RUN pip install redis flask
CMD ["python", "app.py"]

docker-compose.yml

以及docker-compose.yml

version: '3'
services:

  web:
    build: .
    ports:
     - "5000:5000"

  redis:
    image: "redis:alpine"

运行

docker-compose up

现在访问本地的5000端口看看

命令

build

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

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

服务容器一旦构建后,将会带上一个标记名,例如对于web项目中的一个db容器,可能是web_db

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

选项包括:

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> 来重新创建服务并后台停止旧服务,启动新服务,并不会影响到其所依赖的服务。

选项:

down

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

logs

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

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

该命令在调试问题的时候十分有用。

config

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

ps

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

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

选项:

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

stop

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

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

选项:

-t, --timeout TIMEOUT 停止容器时候的超时(默认为 10 秒)。

rm

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

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

选项:

-f, --force 强制直接删除,包括非停止状态的容器。一般尽量不要使用该选项。

-v 删除容器所挂载的数据卷。

这几个命令应该是最常用的了吧

Compose模板文件

模板文件是使用 Compose 的核心,涉及到的指令关键字也比较多。但大家不用担心,这里面大部分指令跟docker run相关参数的含义都是类似的。

默认的模板文件名称为docker-compose.yml,格式为 YAML 格式。

version: "3"

services:
  webapp:
    image: examples/web
    ports:
      - "80:80"
    volumes:
      - "/data"

注意每个服务都必须通过image指令指定镜像或build指令(需要 Dockerfile)等来自动构建生成镜像。

如果使用 build 指令,在 Dockerfile 中设置的选项(例如:CMD, EXPOSE, VOLUME, ENV 等) 将会自动被获取,无需在 docker-compose.yml 中重复设置。

下面分别介绍各个指令的用法。

build

指定Dockerfile所在文件夹的路径(可以是绝对路径,或者相对 docker-compose.yml文件的路径)。 Compose 将会利用它自动构建这个镜像,然后使用这个镜像。

version: '3'
services:

  webapp:
    build: ./dir

你也可以使用context指令指定Dockerfile所在文件夹的路径。

使用dockerfile指令指定Dockerfile文件名。

使用arg指令指定构建镜像时的变量。

version: '3'
services:

  webapp:
    build:
      context: ./dir
      dockerfile: Dockerfile-alternate
      args:
        buildno: 1

command

覆盖容器启动后默认执行的命令。

command: echo "hello world"

depends_on

解决容器的依赖、启动先后的问题。以下例子中会先启动redis db 再启动web

version: '3'

services:
  web:
    build: .
    depends_on:
      - db
      - redis

  redis:
    image: redis

  db:
    image: postgres

注意:web 服务不会等待 redis db 「完全启动」之后才启动。

tmpfs

挂载一个 tmpfs 文件系统到容器。

tmpfs: /run
tmpfs:
  - /run
  - /tmp

env_file

从文件中获取环境变量,可以为单独的文件路径或列表。

如果通过 docker-compose -f FILE 方式来指定 Compose 模板文件,则 env_file 中变量的路径会基于模板文件路径。

如果有变量名称与 environment 指令冲突,则按照惯例,以后者为准。

env_file: .env

env_file:
  - ./common.env
  - ./apps/web.env
  - /opt/secrets.env

环境变量文件中每一行必须符合格式,支持 # 开头的注释行。

# common.env: Set development environment
PROG_ENV=development

environment

设置环境变量。你可以使用数组或字典两种格式。

只给定名称的变量会自动获取运行 Compose 主机上对应变量的值,可以用来防止泄露不必要的数据。

environment:
  RACK_ENV: development
  SESSION_SECRET:

environment:
  - RACK_ENV=development
  - SESSION_SECRET

healthcheck

通过命令检查容器是否健康运行。

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost"]
  interval: 1m30s
  timeout: 10s
  retries: 3

image

指定为镜像名称或镜像 ID。如果镜像在本地不存在,Compose 将会尝试拉取这个镜像。

image: ubuntu
image: orchardup/postgresql
image: a4bc65fd

pid

跟主机系统共享进程命名空间。打开该选项的容器之间,以及容器和宿主机系统之间可以通过进程 ID 来相互访问和操作。

pid: "host"

ports

暴露端口信息。

使用宿主端口:容器端口 (HOST:CONTAINER) 格式,或者仅仅指定容器的端口(宿主将会随机选择端口)都可以。

ports:
 - "3000"
 - "8000:8000"
 - "49100:22"
 - "127.0.0.1:8001:8001"

注意:当使用 HOST:CONTAINER 格式来映射端口时,如果你使用的容器端口小于 60 并且没放到引号里,可能会得到错误结果,因为 YAML 会自动解析 xx:yy 这种数字格式为 60 进制。为避免出现这种问题,建议数字串都采用引号包括起来的字符串格式。

secrets

存储敏感数据,例如 mysql 服务密码。

version: "3.1"
services:

mysql:
  image: mysql
  environment:
    MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
  secrets:
    - db_root_password
    - my_other_secret

secrets:
  my_secret:
    file: ./my_secret.txt
  my_other_secret:
    external: true

ulimits

指定容器的 ulimits 限制值。

例如,指定最大进程数为 65535,指定文件句柄数为 20000(软限制,应用可以随时修改,不能超过硬限制) 和 40000(系统硬限制,只能 root 用户提高)。

  ulimits:
    nproc: 65535
    nofile:
      soft: 20000
      hard: 40000

volumes

数据卷所挂载路径设置。可以设置为宿主机路径(HOST:CONTAINER)或者数据卷名称(VOLUME:CONTAINER),并且可以设置访问模式 (HOST:CONTAINER:ro)。

该指令中路径支持相对路径。

volumes:
 - /var/lib/mysql
 - cache/:/tmp/cache
 - ~/configs:/etc/configs/:ro
如果路径为数据卷名称,必须在文件中配置数据卷。

version: "3"

services:
  my_src:
    image: mysql:8.0
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

这些应该够用了吧

实战NextCloud

网盘已经成为大家生活之中必不可少的工具,无奈国内的网盘要么限速,要么限制容量,而国外的网盘基本上都被挡在了墙外。这个时候,自建云就成了一个很好的选择,而NextCLoud就是自建云中的佼佼者。

要求:

配置文件

docker-compose.yml


version: '3'

services:
    app:
        image: nextcloud:fpm-alpine
        container_name: nextcloud_app
        # NextCloud镜像
        restart: unless-stopped
        volumes:
            - app:/var/www/html
        env_file:
            - ./mysql.env
            - ./app.env
        # 有两个环境文件
        networks:
            nextcloud:
                aliases:
                    - app
        depends_on:
            - mariadb
            - redis
        # 依赖于mariadb和redis
        

    cron:
        image: nextcloud:fpm-alpine
        container_name: nextcloud_cron
        # NextCloud的定时服务镜像
        restart: unless-stopped
        volumes:
            - app:/var/www/html
        entrypoint: /cron.sh
        depends_on:
            - mariadb
            - redis

    omgwtfssl:
        image: superseb/omgwtfssl
        # 用于签名SSL证书的镜像
        restart: "no"
        volumes:
          - ./certs:/certs
        env_file:
          - ./ssl.env
        networks:
            - nextcloud
          
    web:
        image: nginx:alpine
        container_name: nextcloud_web
        # Nginx镜像,用于端口转发和ssl连接
        restart: unless-stopped
        volumes:
        
            - ./nginx.conf:/etc/nginx/nginx.conf:ro
            - nextcloud_app:/var/www/html:ro
            - ./certs/cloud.yadomin.ml.crt:/etc/ssl/nginx/fullchain.pem
            - ./certs/cloud.yadomin.ml.key:/etc/ssl/nginx/privkey.pem
        ports:
            - "80:80"
            - "443:443"
            # 开放了两个端口
        networks:
            - nextcloud
        depends_on:
            - app

    mariadb:
        image: mariadb:latest
        container_name: nextcloud_mariadb
        command: [
            "--character-set-server=utf8mb4",
            "--collation-server=utf8mb4_unicode_ci",
        ]
        volume:
            - db:/var/lib/mysql
        restart: unless-stopped
        environment:
            - MYSQL_RANDOM_ROOT_PASSWORD="yes"
        env_file:
            - ./mysql.env
        networks:
            nextcloud:
                aliases:
                    - mariadb

    redis:
        image: redis:alpine
        container_name: nextcloud_cache
        restart: unless-stopped
        networks:
            nextcloud:
                aliases:
                    - redis
    # 两个数据库镜像

volumes:
    app:
    db:
networks:
    nextcloud:

nginx.conf

worker_processes  1;

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


events {
    worker_connections  1024;
}

# 需要把以下所有的cloud.yadomin.ml修改为自己的域名

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

    log_format  main  '$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;

    upstream php-handler {
        server app:9000;
    }

    server {
        listen 80;
        listen [::]:80;

        server_name cloud.yadomin.ml;
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name cloud.yadomin.ml;

        ssl_certificate     /etc/ssl/nginx/fullchain.pem;
        ssl_certificate_key /etc/ssl/nginx/privkey.pem;

        ssl_protocols             TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
        ssl_ciphers               ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256;
        ssl_prefer_server_ciphers on;
        ssl_ecdh_curve            secp384r1;

        # Add headers to serve security related headers
        # Before enabling Strict-Transport-Security headers please read into this
        # topic first.
        # add_header Strict-Transport-Security "max-age=15768000;
        # includeSubDomains; preload;";
        #
        # WARNING: Only add the preload option once you read about
        # the consequences in https://hstspreload.org/. This option
        # will add the domain to a hardcoded list that is shipped
        # in all major browsers and getting removed from this list
        # could take several months.
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";
        add_header X-Robots-Tag none;
        add_header X-Download-Options noopen;
        add_header X-Permitted-Cross-Domain-Policies none;
        add_header Referrer-Policy no-referrer;

        root /var/www/html;

        location = /robots.txt {
            allow all;
            log_not_found off;
            access_log off;
        }

        # The following 2 rules are only needed for the user_webfinger app.
        # Uncomment it if you're planning to use this app.
        #rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
        #rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json
        # last;

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

        # set max upload size
        client_max_body_size 10G;
        fastcgi_buffers 64 4K;

        # Enable gzip but do not remove ETag headers
        gzip on;
        gzip_vary on;
        gzip_comp_level 4;
        gzip_min_length 256;
        gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
        gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

        # Uncomment if your server is build with the ngx_pagespeed module
        # This module is currently not supported.
        #pagespeed off;

        location / {
            rewrite ^ /index.php$request_uri;
        }

        location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ {
            deny all;
        }
        location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) {
            deny all;
        }

        location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\.php(?:$|/) {
            fastcgi_split_path_info ^(.+\.php)(/.*)$;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param PATH_INFO $fastcgi_path_info;
            # fastcgi_param HTTPS on;
            #Avoid sending the security headers twice
            fastcgi_param modHeadersAvailable true;
            fastcgi_param front_controller_active true;
            fastcgi_pass php-handler;
            fastcgi_intercept_errors on;
            fastcgi_request_buffering off;
        }

        location ~ ^/(?:updater|ocs-provider)(?:$|/) {
            try_files $uri/ =404;
            index index.php;
        }

        # Adding the cache control header for js and css files
        # Make sure it is BELOW the PHP block
        location ~ \.(?:css|js|woff2?|svg|gif)$ {
            try_files $uri /index.php$request_uri;
            add_header Cache-Control "public, max-age=15778463";
            # Add headers to serve security related headers (It is intended to
            # have those duplicated to the ones above)
            # Before enabling Strict-Transport-Security headers please read into
            # this topic first.
            # add_header Strict-Transport-Security "max-age=15768000;
            #  includeSubDomains; preload;";
            #
            # WARNING: Only add the preload option once you read about
            # the consequences in https://hstspreload.org/. This option
            # will add the domain to a hardcoded list that is shipped
            # in all major browsers and getting removed from this list
            # could take several months.
            add_header X-Content-Type-Options nosniff;
            add_header X-XSS-Protection "1; mode=block";
            add_header X-Robots-Tag none;
            add_header X-Download-Options noopen;
            add_header X-Permitted-Cross-Domain-Policies none;
            add_header Referrer-Policy no-referrer;

            # Optional: Don't log access to assets
            access_log off;
        }

        location ~ \.(?:png|html|ttf|ico|jpg|jpeg)$ {
            try_files $uri /index.php$request_uri;
            # Optional: Don't log access to other assets
            access_log off;
        }
    }

}

ssl.env

这个ssl最折腾人了

SSL_SUBJECT=cloud.yadomin.ml
# 你要签的域名
CA_SUBJECT=yadomin@outlook.com
# 你的邮箱
SSL_KEY=/certs/cloud.yadomin.ml.key
SSL_CSR=/certs/cloud.yadomin.ml.csr
SSL_CERT=/certs/cloud.yadomin.ml.crt
# SSL证书的位置

# 请将所有的cloud.yadomin.ml修改为自己的域名

mysql.env

MYSQL_DATABASE=nextcloud
# 数据库名
MYSQL_USER=nextcloud
# 数据库用户名
MYSQL_PASSWORD=nextcloud
# 数据库密码

app.env

NEXTCLOUD_ADMIN_USER=nextcloud
# Nextcloud Admin 用户名
NEXTCLOUD_ADMIN_PASSWORD=nextcloud
# Nextcloud Admin 密码
NEXTCLOUD_TRUSTED_DOMAINS=cloud.yadomin.ml localhost:7002
# 信任的域名

MYSQL_HOST=mariadb
# MYSQL数据库名称

REDIS_HOST=redis
REDIS_HOST_PORT=6379
# Redis 数据库名称与密码

运行容器

所有的文件全部放在同一个文件夹里

docker-compose up

就会开始自动拉取与运行镜像,等待一会后部署成功,浏览器访问你的域名,设置管理员账户,就可以正常使用了。

文件访问,文件分享也可以正常使用

数据备份迁移

备份

在备份之前,应先停止docker-compose所需要备份的镜像

docker-compose -f /path/to/nextcloud/docker-compose.yml stop

然后找到我们所需要备份的镜像,由于我们最重要的云盘文件全部存储在镜像的/var/www/html中,而该目录又与实体机的app volume中,所以我们需要做的就是将该镜像打包出来(使用docker-compose创建的容器volume会被添加前缀,所以这里其实是nextcloud_app,使用docker volume ls查看具体情况)

cat > ./backup/run.sh <EOF
#/bin/bash
if [[ $1 == "backup" ]];then
  find /data -type f -print0 |xargs -0 md5sum |sort > /backup/MD5SUM.txt
  tar -zcvf /backup/nextcloud_app.tar.gz /data
elif [[ $1 == "recover" ]];then
  tar -zxvf /backup/nextcloud_app.tar.gz -C /data/
  md5sum -c /backup/MD5SUM.txt |grep FAILED
else
  echo "You need to specific a command"
  exit 1
fi

docker run --rm -v nextcloud_app:/data -v /path/to/backup:/backup ubuntu bash /backup/run.sh backup

然后在/path/to/backup/中就可以找到nextcloud_app.tar.gz

迁移

首先将备份文件和run.sh下载到对应服务器中,scp,rsync均可。

然后创建一个中间容器用于恢复备份文件

docker create -v nextcloud_app:/data --name for_migrate ubuntu true
# 创建镜像,与原来保持一致

docker run --rm --volumes-from for_migrate -v $(pwd):/backup ubuntu /backup/run.sh recover
# 解压文件

docker rm for_migrate
# 删除中间容器

cd nextcloud && docker-compose up
# 重新部属nextcloud

校验

在上一步中已经对文件进行了校验,注意看输出中是否有FAILED

数据库定时备份

修改vim /etc/crontab,加入如下内容

*/30 * * * * docker run --rm -v nextcloud_db:/data -v /path/to/backup:/backup ubuntu tar zcvf db-$(date +%y-%m-%H-%M).tar.gz /data

即30Min备份一次数据库,保存/path/to/backup/db-日期.tar.gz

别忘了systemctl enable crond && systemctl start