使用 Docker 部署 Python/Django 项目 (单机/swarm集群)

2019-08-08

https://github.com/lsdlab/k8sdjango

传统部署 & 容器部署

容器部署优点:

  1. 抹平系统差异,可快速迁移部署。
  2. docker swarm 快速实现集群部署,快速实现负载均衡,无需使用其他 load blancing 模块。

容器部署缺点:

  1. 应用与状态分离,需要挂载状态文件进入容器。
  2. 日志文件需要从容器中映射到宿主机器上,这个映射存在一定的延迟。

单机

Dockerfile

使用 alpine 镜像,镜像尺寸会小一点,用了阿里云镜像源,解决 postgresql 的 driver 问题,pip 包也用了阿里云镜像源,build 速度会快一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FROM python:3.7.4-alpine3.10 as build1
ENV PYTHONUNBUFFERED 1
ENV DJANGO_SETTINGS_MODULE conf.production.settings
ENV TZ Asia/Shanghai
RUN mkdir /k8sdjango

# add china mirrors
RUN echo 'http://mirrors.aliyun.com/alpine/v3.10/community/'>/etc/apk/repositories
RUN echo 'http://mirrors.aliyun.com/alpine/v3.10/main/'>>/etc/apk/repositories

# install psycopg2-binary
RUN apk update \
&& apk add tzdata \
&& apk add --virtual build-deps gcc python3-dev musl-dev \
&& apk add postgresql-dev \
&& pip install -U pip setuptools -i https://mirrors.aliyun.com/pypi/simple/ \
&& pip install psycopg2-binary -i https://mirrors.aliyun.com/pypi/simple/ \
&& apk del build-deps

# install requirements and copy code
COPY requirements.txt /k8sdjango
RUN pip install -r /k8sdjango/requirements.txt -i https://mirrors.aliyun.com/pypi/simple/

FROM python:3.7.4-alpine3.10
COPY --from=build1 / /
COPY . /k8sdjango
WORKDIR /k8sdjango
COPY ./wait-for /bin/wait-for

docker-compose.yml

使用 docker-compose 作为编排工具,django 使用上面的 Dockerfile 作为 base image,postgresql/redis/rabbit 使用标准 image,指定版本,都是最新的稳定版,postgresql 把两个配置文件挂载进入容器,5432 端口开放远程连接,同时做了持久化,把容器的文件也映射到宿主机上。redis 只做数据持久化,不修改配置,也不开放远程连接,rabbitmq 设置用户名和密码以及 vhost,数据持久化,nginx 开放 8080 端口,代理 web 镜像的 gunicorn 8080 端口,nginx 需要挂在配置文件到容器中,同时,django 自带后台的静态文件需要映射到宿主机器中,另外 django 中自定义的日志也映射到宿主机器上。celery worker/beat 使用和 web 用一个镜像,使用同样的 image,减少 build。depends_on 配合 wait-for 能够让容器顺序启动,不会再出现 celery_worker 先于 rabbitmq 启动,连不上 broker 的报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
version: '3'
services:
db:
image: postgres:11.4
container_name: k8sdjango-postgres
restart: always
environment:
POSTGRES_USER: k8sdjango_production
POSTGRES_PASSWORD: b8n2maLRb7EUyv8c
POSTGRES_DB: k8sdjango_production
ports:
- '5432:5432'
volumes:
- ./compose/postgres_data:/var/lib/postgresql/data
- ./compose/postgresql.conf:/usr/local/etc/postgresql/11/postgresql.conf
- ./compose/pg_hba.conf:/usr/local/etc/postgresql/11/pg_hba.conf

redis:
image: redis:5.0.5
container_name: k8sdjango-redis
restart: always
ports:
- '6379:6379'
volumes:
- ./compose/redis_data:/data
depends_on:
- "db"

rabbitmq:
image: rabbitmq:3.7.17-management
container_name: k8sdjango-rabbitmq
restart: always
# environment:
# RABBITMQ_DEFAULT_VHOST: k8sdjangovhost
# RABBITMQ_DEFAULT_USER: k8sdjangovhost
# RABBITMQ_DEFAULT_PASS: b8n2maLRb7EUyv8c
ports:
- '5672:5672'
- '15672:15672'
- '15674:15674'
volumes:
- ./compose/rabbitmq_data:/var/lib/rabbitmq
- ./compose/rabbitmq_plugins:/etc/rabbitmq/enabled_plugins
depends_on:
- "redis"

nginx:
image: nginx:1.16
container_name: k8sdjango-nginx
restart: always
ports:
- "8080:8080"
volumes:
- ./compose/nginx:/etc/nginx/conf.d
- ./compose/static:/k8sdjango/static
- ./compose/nginx_log:/var/log/nginx
depends_on:
- "web"

web:
build: .
image: django-docker-web
container_name: k8sdjango-web
command: sh -c "wait-for db:5432 && python manage.py collectstatic --no-input && python manage.py migrate && gunicorn k8sdjango.wsgi --workers 5 --bind 0.0.0.0:8080"
environment:
DJANGO_SETTINGS_MODULE: conf.production.settings
TZ: Asia/Shanghai
volumes:
- ./compose/static:/k8sdjango/static
depends_on:
- "db"
expose:
- "8080"

celery_worker:
image: django-docker-web
container_name: k8sdjango-celery_worker
command: sh -c "wait-for rabbitmq:5672 && celery -A apps.celeryconfig worker --loglevel=info --autoscale=4,2"
restart: always
environment:
DJANGO_SETTINGS_MODULE: conf.production.settings
TZ: Asia/Shanghai
depends_on:
- "db"
- "redis"
- "rabbitmq"
- "web"
- "nginx"

celery_beat:
image: django-docker-web
container_name: k8sdjango-celery_beat
command: sh -c "wait-for rabbitmq:5672 && celery -A apps.celeryconfig beat --loglevel=info"
restart: always
environment:
DJANGO_SETTINGS_MODULE: conf.production.settings
TZ: Asia/Shanghai
depends_on:
- "db"
- "redis"
- "rabbitmq"
- "web"
- "nginx"
- "celery_worker"

运行

1
2
3
cd k8sdjango
docker-compose build
docker-compose up

build 完成后会获得以下镜像

Xnip2019-08-13_09-07-56.jpg

优化点

1
FROM python:3.7.4-alpine3.10 as build1

使用 python:3.7.4-alpine3.10 作为 build1

1
2
3
# install requirements and copy code
COPY requirements.txt /k8sdjango
RUN pip install -r /k8sdjango/requirements.txt -i https://mirrors.aliyun.com/pypi/simple/

先把 pip 依赖复制进去,安装依赖

1
2
3
4
FROM python:3.7.4-alpine3.10
COPY --from=build1 / /
COPY . /k8sdjango
WORKDIR /k8sdjango

把第一步的 build1 复制出来,整个项目的代码复制到文件夹中,更改工作目录,这一步,这么做会让每次 build 镜像速度快很多,如果你没有更改 requirements 以及 Dockerfile docker-compose.yml 的话,基本上再次 build 镜像就是复制代码到容器中,其余都是用的缓存。

swarm

docker swarm 是最简单的集群部署方式,docker 内置的 overlay 网络模式能够提供负载均衡,任意服务的暴露的 端口均能在任意节点访问,由 swarm overlay 网络进行 routing,最终会访问到某一个容器,同时每个服务的容器数量能够 scale up 或者 scale down。

使用三台机器,一个 manager,两个 worker,使用 swarm 集群部署配置文件和单机部署区别不大,主要是设置 replicas 数量以及 placement 容器运行制定镜像,把三个数据库都制定运行在第二台机器上,web 以及 nginx 各运行三个 replicas,swarm 会自动分配,swarm这里的 node.hostname == SmartMP02 是机器 /etc/hosts 中的 hostname。

docker-compose-stack.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
version: '3'
services:
db:
image: postgres:11.4
deploy:
replicas: 1
restart_policy:
condition: on-failure
placement:
constraints: [node.hostname == SmartMP02]
environment:
POSTGRES_USER: k8sdjango_production
POSTGRES_PASSWORD: b8n2maLRb7EUyv8c
POSTGRES_DB: k8sdjango_production
ports:
- '5432:5432'
volumes:
- ./compose/postgres_data:/var/lib/postgresql/data
- ./compose/postgresql.conf:/usr/local/etc/postgresql/11/postgresql.conf
- ./compose/pg_hba.conf:/usr/local/etc/postgresql/11/pg_hba.conf

redis:
image: redis:5.0.5
deploy:
replicas: 1
restart_policy:
condition: on-failure
placement:
constraints: [node.hostname == SmartMP02]
ports:
- '6379:6379'
volumes:
- ./compose/redis_data:/data
depends_on:
- "db"

rabbitmq:
image: rabbitmq:3.7.17-management
deploy:
replicas: 1
restart_policy:
condition: on-failure
placement:
constraints: [node.hostname == SmartMP02]
# environment:
# RABBITMQ_DEFAULT_VHOST: k8sdjangovhost
# RABBITMQ_DEFAULT_USER: k8sdjangovhost
# RABBITMQ_DEFAULT_PASS: b8n2maLRb7EUyv8c
ports:
- '5672:5672'
- '15672:15672'
- '15674:15674'
volumes:
- ./compose/rabbitmq_data:/var/lib/rabbitmq
depends_on:
- "redis"

nginx:
image: nginx:1.16
deploy:
replicas: 4
ports:
- "8080:8080"
volumes:
- ./compose/nginx:/etc/nginx/conf.d
- ./compose/static:/k8sdjango/static
- ./compose/nginx_log:/var/log/nginx
depends_on:
- "web"

web:
build: .
image: django-docker-web
command: sh -c "wait-for db:5432 && python manage.py collectstatic --no-input && python manage.py migrate && gunicorn k8sdjango.wsgi --workers 5 --bind 0.0.0.0:8080"
deploy:
replicas: 4
environment:
DJANGO_SETTINGS_MODULE: conf.production.settings
TZ: Asia/Shanghai
volumes:
- ./compose/static:/k8sdjango/static
depends_on:
- "db"

celery_worker:
image: django-docker-web
command: sh -c "wait-for rabbitmq:5672 && celery -A apps.celeryconfig worker --loglevel=info --autoscale=4,2"
deploy:
replicas: 1
restart_policy:
condition: on-failure
placement:
constraints: [node.hostname == SmartMP03]
environment:
DJANGO_SETTINGS_MODULE: conf.production.settings
TZ: Asia/Shanghai
depends_on:
- "db"
- "redis"
- "rabbitmq"
- "web"
- "nginx"

celery_beat:
image: django-docker-web
command: sh -c "wait-for rabbitmq:5672 && celery -A apps.celeryconfig beat --loglevel=info"
deploy:
replicas: 1
restart_policy:
condition: on-failure
placement:
constraints: [node.hostname == SmartMP03]
environment:
DJANGO_SETTINGS_MODULE: conf.production.settings
TZ: Asia/Shanghai
depends_on:
- "db"
- "redis"
- "rabbitmq"
- "web"
- "nginx"
- "celery_worker"

问题

**
celery_worker/beat 使用和 django web 同一个镜像,指定放在第三台机器上,所以仍然需要在三台机器上同一个位置都部署代码,build 好镜像,否则 celery_worker/beat 都不能启动。

运行

1
2
3
4
5
6
7
8
9
cd k8sdjango
docker-compose build
docker stack deploy -c docker-compose-stack.yml k8sdjango # 启动服务
docker stack down k8sdjango #关闭所有服务

# 创建django自带后台的超级用户
# 先获取 django-docker-web 的 container id 任意一个
docker ps
docker exec -it --env DJANGO_SETTINGS_MODULE=conf.production.settings f0ef1ad3c0bb python manage.py createsuperuser

此时访问三台机器中的任意一台的 :8080/admin/,都能进入 django 自带的后台页面。

获取服务列表

1
docker service ls

Xnip2019-08-13_10-01-59.jpg

https://www.portainer.io/ 是一个 Docker management UI,提供了一些功能,同时也能 将 swarm cluster 可视化,这个示例用 swarm 部署后可视化的结果如下。

Xnip2019-08-13_13-27-47.jpg

serivces list 服务列表如下

Xnip2019-08-13_15-56-24.jpg

优化点

  • 代码同步脚本