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

安装

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

pip3 install docker-compose

使用

术语

  • 服务(Service):一个应用容器,实际上可以运行多个相同镜像的实例。
  • 项目(Project):由一组关联的应用容器组成的一个完整业务单元。

场景

最常见的项目是 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端口看看

compose1.png

命令

build

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

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

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

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

选项包括:

  • --force-rm 删除构建过程中的临时容器。
  • --no-cache 构建镜像过程中不使用 cache(这将加长构建过程)。
  • --pull 始终尝试通过 pull 来获取更新版本的镜像。

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

选项:

  • -d 在后台运行服务容器。
  • --no-color 不使用颜色来区分不同的服务的控制台输出。
  • --no-deps 不启动服务所链接的容器。
  • --force-recreate 强制重新创建容器,不能与 --no-recreate 同时使用。
  • --no-recreate 如果容器已经存在了,则不重新创建,不能与 --force-recreate 同时使用。
  • --no-build 不自动构建缺失的服务镜像。
  • -t, --timeout TIMEOUT 停止容器时候的超时(默认为 10 秒)。

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进行部署

    • 使用fpm镜像
    • 前置代理容器(nginx)
    • 数据库容器(redis+mariadb)
    • 使用https
  • 实现数据库的定时备份和容器的健康检查(不一定能写出来)

配置文件

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

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

compose2.png

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

数据备份迁移

备份

在备份之前,应先停止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