Используем Docker

Сегодня мы немного поиграем с виртуализацией на уровне операционной системы с помощью Docker.

Для начала немного информации с википедии для общего развития

Docker — программное обеспечение для автоматизации развёртывания и управления приложениями в среде виртуализации на уровне операционной системы, например LXC. Позволяет «упаковать» приложение со всем его окружением и зависимостями в контейнер, который может быть перенесён на любой Linux-системе с поддержкой cgroups в ядре, а также предоставляет среду по управлению контейнерами.

Разрабатывается и поддерживается одноимённой компанией-стартапом, распространяется как свободное программное обеспечение под лицензией Apache 2.0. Написан на языке Go.

А теперь к делу!

Установка

Для начала проверим версию установленного ядра

$ uname -r

Версия ядра должна быть не ниже 3.10, для того чтобы можно было использовать Docker.

В зависимости от используемого дистрибутива установка Docker выглядит по разному. Настоятельно рекомендую обратится к официальному гиду по установке.

В моём случае установка под Arch Linux выполняется одной командой:

$ sudo pacman -S docker

Также вы можете установить Docker используя sh скрипт. Для загрузки скрипта потребуется wget или curl, или также можно использовать браузер.

$ wget -qO- https://get.docker.com/ > docker_install.sh

Теперь запустим установку (не забыв про sudo/su).

$ sudo sh docker_install.sh

После установки желательно добавить используемого пользователя в группу docker.

$ sudo gpasswd -a <user> docker

или

$ sudo usermod -aG docker <user>

На этом установка закончена. Запустим демон docker (в моём случае через systemd)

$ sudo systemctl start docker

и проверим работоспособность следующей командой

$ docker run hello-world

Если всё хорошо, то Docker скачает и запустит данный контейнер и программа выведет следующее

Hello from **Docker**.
This message shows that your installation appears to be working correctly.
To generate this message, **Docker** took the following steps:
 1. The **Docker** client contacted the **Docker** daemon.
 2. The **Docker** daemon pulled the "hello-world" image from the **Docker** Hub.
    (Assuming it was not already locally available.)
 3. The **Docker** daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The **Docker** daemon streamed that output to the **Docker** client, which sent it
    to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash
For more examples and ideas, visit:
 http://docs.docker.com/userguide/

Основные команды

Весь список доступных команд можно получить использую флаг –help

Рассмотрим только основные из них

Использование Docker на примерах

Запуск bash

Давайте сначала запустим простой контейнер с bash интерпретатором в нём и получим доступ к нему. Скачаем образ-контейнер base/archlinux для экспериментов.

$ docker pull base/archlinux

При вызове команды images получим что-то в виде:

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
base/archlinux      latest              dce0559daa1b        10 months ago       282.9 MB

Теперь у нас есть образ для создания контейнеров. Можно перейти непосредственно к запуску bash внутри контейнера. Делается это следующей командой

$ docker run -it --entrypoint /bin/bash base/archlinux

Используемые флаги

В случае успеха Docker перенаправит нас на созданный tty внутри контейнера

[root@f64a7c5309f9 /] #

Здесь можно выполнять любые действия, а после зафиксировать изменения в контейнере. Но на текущий момент просто ограничимся запуском.

Для получения информации по запущенным контейнерам используйте флаг ps.

$ docker ps -a
CONTAINER ID        IMAGE                      COMMAND                CREATED             STATUS                   PORTS               NAMES
f64a7c5309f9        base/archlinux:latest      "/bin/bash"            2 minutes ago       Up 2 minutes                                 sleepy_bell

По умолчанию Docker сам выдаёт названиям созданным контейнерам. В нашем случае — sleepy_bell, но их также можно задавать вручную используя флаг –name. Для выхода из контейнера используйте команду exit.

Запуск сайта

Рассмотрим более интересный случай, когда нужно собрать свой контейнер и подключить каталог к нему. Будем рассматривать развёртывание простого flask приложения на python. Все действия будем производить в текущем каталоге.

Содержимое файла app/main.py

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run(host='0.0.0.0')

Для создания своего контейнера создадим файл с конфигурацией — Dockerfile, со следующим содержимым

FROM base/archlinux
MAINTAINER dr.FreeCX <email>
RUN pacman -Sy && pacman -S python-flask python-pip --noconfirm
WORKDIR /app
EXPOSE 5000
VOLUME ["/app"]
CMD ["python", "/app/main.py"]

Рассмотрим файл построчно

На третьем шаге команды объединены в одну для того чтобы не создавать лишние коммиты в создаваемом контейнере.

Теперь необходимо собрать данный контейнер, для этого используем команду build

$ docker build -t site-app .

Флаг -t отвечает за имя контейнера-образа (ну или tag) и точка в конце команды указывает на поиск файла Dockerfile в текущей директории.

Если всё прошло удачно, то увидим следующее

Sending build context to Docker daemon 43.01 kB
Sending build context to Docker daemon 
Step 0 : FROM base/archlinux
 ---> dce0559daa1b
Step 1 : MAINTAINER dr.FreeCX <email>
 ---> Using cache
 ---> 64e04e1e920f
Step 2 : RUN pacman -Sy && pacman -S python-flask python-pip --noconfirm
 ---> Using cache
 ---> daa2e8f99de7
Step 3 : WORKDIR /app
 ---> Running in 85dd63b293ee
 ---> 578e66682a73
Removing intermediate container 85dd63b293ee
Step 4 : EXPOSE 5000
 ---> Running in 125baa973662
 ---> 12a28947863a
Removing intermediate container 125baa973662
Step 5 : VOLUME /app
 ---> Running in c54bedebe79f
 ---> c4807322f855
Removing intermediate container c54bedebe79f
Step 6 : CMD python /app/main.py
 ---> Running in fd472c20b3b3
 ---> 8163426a880f
Removing intermediate container fd472c20b3b3
Successfully built 8163426a880f

Выполнив команду images убедимся, что появился новый образ

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
site-app            latest              8163426a880f        3 minutes ago       411.5 MB

Теперь осталось самое интересное — запуск нашего контейнера. Давайте же сделаем это

$ docker run -it -d -p 8080:5000 -v "$PWD/app":/app site-app
$ docker ps -a
CONTAINER ID        IMAGE                      COMMAND                CREATED             STATUS                      PORTS                    NAMES
1d3b134e4095        site-app:latest            "python /app/main.py   4 seconds ago       Up 2 seconds                0.0.0.0:8080->5000/tcp   focused_swartz

Используемые флаги

Как результат по адресу 0.0.0.0:8080 становится доступно flask-приложение.

Для остановки контейнера воспользуемся командой stop указав в качестве параметра имя либо container id

$ docker stop 1d3b134e4095
$ docker stop focused_swartz

Пара слов о Docker compose

Данный раздел находится в слишком пассивном написании!

Теперь когда мы немного разобрались с docker’ом, давайте рассмотрим более интересную штуку, такую как Docker Сompose!

Как говорит официальная документация

Compose is a tool for defining and running multi-container Docker applications.

то есть это тулза с помощью которой можно собрать систему из нескольких связанных docker-контейнеров, что мы и сделаем!

Что будем ваять?

Давайте напишем web-приложение доску, где можно будет оставить комментарий.

Для этого нам понадобится несколько компонентов:

Конечно можно обойтись без nginx, но почему бы и нет?!

Структура нашего проекта будет следующей:

.
├── docker-compose.yml
├── .env
├── nginx
│   ├── Dockerfile
│   └── sites-enabled
│       └── flask_project
└── web
    ├── config.py
    ├── create_db.py
    ├── Dockerfile
    ├── main.py
    ├── requirements.txt
    └── templates
        └── index.html

Но начнём по порядку!

Приложение

Приложение будет реализовывать две функции:

main.py

from flask import Flask, render_template, redirect, request
from werkzeug.contrib.fixers import ProxyFix
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
# загрузим конфиг из файла
app.config.from_object('config')

# нужно для корректной работы проброса через gunicorn и nginx
app.wsgi_app = ProxyFix(app.wsgi_app)

# создаём объект БД
db = SQLAlchemy(app)


# наша таблица в БД
# где у нас будет два поля с информацией
class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    # пользователь
    user = db.Column(db.String(16), nullable=False)
    # его текстовой пост
    text = db.Column(db.String(256))

    def __init__(self, user, text='<empty post>'):
        self.user = user
        self.text = text


@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'GET':
        # получаем все посты
        posts = Post.query.all()
        # рендерим страницу с этими данными
        return render_template('index.html', posts=posts)
    elif request.method == 'POST':
        # получаем данные со страницы
        user, text = request.form['user-name'], request.form['user-text']
        # создаём пост
        new_post = Post(user, text)
        # добавляем в БД
        db.session.add(new_post)
        db.session.commit()
        # редиректим на страницу с GET
        return redirect('/')
    else:
        # не будем поддерживать остальные методы
        return 'Noooooooo!', 402


if __name__ == '__main__':
    app.run()

config.py

from os import environ

SECRET_KEY = environ['SECRET_KEY']
DEBUG = environ['DEBUG']
DB_NAME = environ['DB_NAME']
DB_USER = environ['DB_USER']
DB_PASS = environ['DB_PASS']
DB_SERVICE = environ['DB_SERVICE']
DB_PORT = environ['DB_PORT']
SQLALCHEMY_DATABASE_URI = 'postgresql://{0}:{1}@{2}:{3}/{4}'.format(
    DB_USER, DB_PASS, DB_SERVICE, DB_PORT, DB_NAME
)

create_db.py

from main import db


db.create_all()
db.session.commit()

index.html

<!doctype html>
<title>simply dashboard</title>
<p>send a post</p>
<form name='add-user' method='POST' action='/'>
  <div class='user-name-input'>
    user: <input name='user-name' class='user-name' type='text' size=16>
    <input type='submit' value='send post'>
  </div>
  <div class='user-text-input'>
    text:<br><textarea name='user-text' style='width: 300px; height: 100px;'></textarea>
  </div>
</form>
<hr><p>Posts</p>
<ul>
  
</ul>

requirements.txt

flask==0.11.1
flask_sqlalchemy==2.1
gunicorn==19.6.0
psycopg2==2.6.2

Dockerfile

FROM python:3.4-onbuild

nginx

Dockerfile

FROM tutum/nginx
RUN rm /etc/nginx/sites-enabled/default
ADD sites-enabled/ /etc/nginx/sites-enabled

flask_project

server {
    listen 5000;
    charset utf-8;

    access_log  /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;

    client_max_body_size 32m;

    location / {
        proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        if (!-f $request_filename) {
            proxy_pass http://web:8000;
            break;
        }
    }
}

Конфиги Docker Compose

.env

DEBUG=True
SECRET_KEY=its-my-super-secret-key-please-dont-forget-it
DB_NAME=postgres
DB_USER=postgres
DB_PASS=postgres
DB_SERVICE=postgres
DB_PORT=5432

docker-compose.yml

web:
  restart: always
  build: ./web
  expose:
    - "8000"
  links:
    - postgres:postgres
  env_file: .env
  command: /usr/local/bin/gunicorn -w 2 -b :8000 main:app # --log-file /tmp/gunicorn.log

nginx:
  restart: always
  build: ./nginx/
  ports:
    - "5000:5000"
  volumes:
    - /www/static
  volumes_from:
    - web
  links:
    - web:web

data:
  restart: always
  image: postgres:latest
  volumes:
    - /var/lib/postgresql
  command: "true"

postgres:
  restart: always
  image: postgres:latest
  volumes_from:
    - data
  ports:
    - "5432:5432"

Финал

Собираем наши контейнеры

$ docker-compose build

Cоздаём таблицу в БД

$ docker-compose run web /usr/local/bin/python create_demo.py

И наконец поднимаем их

$ docker-compose up -d

Теперь можно открыть сам веб-сервис в браузере http://localhost:5000.

Для остановки работы контейнеров выполните следующую команду в директории с проектом

$ docker-compose stop

Исходный код всего проекта доступен в репозитории.

Вот и всё на сегодня.

Полезные ссылки

[1] Docker – официальный сайт.

[2] Docker Hub – хранилище Docker контейнеров.

[3] Docker Compose – работа с мультиконтейнерами.

[4] Flask – микрофреймворком для создания вебсайтов на языке Python.

[5] PostgreSQL – свободная объектно-реляционная система управления базами данных.

Назад