В этой статье я опишу настройку автоматического развёртывания веб-приложения на стеке Django + uWSGI + PostgreSQL + Nginx из репозитория на сервисе GitLab.com. Изложенное также применимо к кастомной инсталляции GitLab. Предполагается, что читатель располагает опытом в создании веб-приложений на Django, а так же опытом администрирования Linux-систем.


Развёртывание реализуем с помощью Fabric, Docker и docker-compose, а осуществлять его будет сервис непрерывной интеграции, встроенный в GitLab, под названием GitLab CI.


Механизм автоматического развёртывания


Развёртывание будет происходить следующим образом:


  1. При push'e новых коммитов в репозиторий будет автоматически запускаться GitLab CI.
  2. GitLab CI будет собирать Docker-образ с готовым к запуску Django-приложением.
  3. Затем GitLab CI отправит (push) собранный Docker-образ в GitLab container registry. Обратите внимание, настройки приватности в registry те же, что и у репозитория, т.е. для публичных репозиториев GitLab registry открыт для всех.
  4. Gitlab CI запустит юнит-тесты.
  5. В случае, если коммиты или merge request'ы производились в главную ветку (master), то после успешной сборки и тестирования Gitlab CI с помощью Fabric развернёт собранный Docker-образ на сервер с указанным нами IP-адресом.

Приватные данные, необходимые для развёртывания — закрытые ключи, SECRET_KEY для Django, токены сторонних сервисов и т.д. — хранить открытым текстом в репозитории определённо не стоит, поэтому для их хранения воспользуемся механизмом GitLab Secret Variables:


image


При таком подходе конфиденциальные данные доступны открытым текстом лишь в двух местах: в настройках проекта на GitLab.com и на сервере, на который осуществляется развёртывание. В свою очередь, на сервере конфиденциальные данные будут храниться в переменных окружения (читай: будут видны любому, кто может на него зайти по SSH).


Следующие переменные необходимы для работы механизма развёртывания:


  • DEPLOY_KEY — приватная часть SSH-ключа, который используется для входа на сервер;
  • DEPLOY_ADDR — его IP-адрес;
  • SECRET_KEYсоответствующая настройка Django.

Кроме того, в файле settings.py Django-проекта определим SECRET_KEY следующим образом:


SECRET_KEY = os.getenv('SECRET_KEY') or sys.exit('SECRET_KEY environment variable is not set.')

Шаг 1: Docker


В первую очередь, создадим Dockerfile для запуска Django и uWSGI на основе легковесного образа Alpine Linux:


web/Dockerfile
FROM python:3.5-alpine

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

RUN apk add --no-cache --virtual .build-deps gcc musl-dev linux-headers pkgconf     autoconf automake libtool make postgresql-dev postgresql-client openssl-dev &&     apk add postgresql-libs postgresql-client &&     # Предотвращаем неудачную компиляцию uWSGI внутри Docker, см. https://git.io/v1ve3
    (while true; do pip --no-cache-dir install uwsgi==2.0.14 && break; done)

COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r requirements.txt

COPY . /usr/src/app
RUN SECRET_KEY=build ./manage.py collectstatic --noinput &&     ./manage.py makemessages &&     apk del .build-deps

Предполагается, что зависимости нашего веб-приложения, как это принято в мире Python, хранятся в файле requirements.txt.


Шаг 2: docker-compose


Далее, для оркестрации Docker-контейнеров стека нам понадобится docker-compose.
Теоретически, можно было бы обойтись и без него, но тогда файл с инструкциями для CI стал бы раздутым и нечитаемым (см. для примера здесь).


Итак, в корневой директории репозитория создадим файл docker-compose.yml следующего содержания:


docker-compose.yml
version: '2'
services:
    web:
        # TODO: Смените username и project на подходящие вам значения.
        image: registry.gitlab.com/username/project:${CI_BUILD_REF_NAME}
        build: ./web
        environment:
            # переменные окружения, значения которых пробрасываются
            #  в контейнер из сервера
            - SECRET_KEY
        command: uwsgi /usr/src/app/uwsgi.ini
        volumes:
            - static:/srv/static
        restart: unless-stopped

    test:
        # TODO: Смените username и project на подходящие вам значения.
        image: registry.gitlab.com/username/project:${CI_BUILD_REF_NAME}
        command: python manage.py test
        restart: "no"

    postgres:
        image: postgres:9.6
        environment:
            # переменные окружения: пользователь и база данных
            - POSTGRES_USER=root
            - POSTGRES_DB=database
        volumes:
            # хранилище данных
            - data:/var/lib/postgresql/data
        restart: unless-stopped

    nginx:
        image: nginx:mainline
        ports:
            # открытые наружу порты
            - "80:80"
            - "443:443"
        volumes:
            # хранилища конфигов и статических файлов
            - ./nginx:/etc/nginx:ro
            - static:/srv/static:ro
        depends_on:
            - web
        restart: unless-stopped

Приведённый файл отвечает следующей структуре проекта:


repository
+-- nginx
¦   +-- mime.types
¦   +-- nginx.conf
¦   +-- ssl_params
¦   L-- uwsgi_params
+-- web
¦   +-- project
¦   ¦   +-- __init__.py
¦   ¦   +-- settings.py
¦   ¦   +-- urls.py
¦   ¦   L-- wsgi.py
¦   +-- app
¦   ¦   +-- migrations
¦   ¦   ¦   L-- ...
¦   ¦   +-- __init__.py
¦   ¦   +-- admin.py
¦   ¦   +-- apps.py
¦   ¦   +-- models.py
¦   ¦   +-- tests.py
¦   ¦   L-- views.py
¦   +-- Dockerfile
¦   +-- manage.py
¦   +-- requirements.txt
¦   L-- uwsgi.ini
+-- docker-compose.yml
L-- fabfile.py

Теперь весь стек запускается одной командой docker-compose up, а внутри Docker-контейнеров стека доступ к другим запущенным контейнерам происходит по DNS-именам, соответствующим записям в файле docker-compose.yml, кроме того, все открытые порты контейнеров доступны друг другу, так как они находятся в одной внутренней сети Docker'а. Так, релевантная часть конфига Nginx будет выглядеть следующим образом:


nginx.conf
upstream django {
    server web:8000;
}

… а настройки доступа Django к БД — следующим:


settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'database',
        'HOST': 'postgres',
    }
}

Благодаря настройке restart: unless-stopped при перезагрузке сервера все контейнеры в нашем стеке автоматически перезапускаются с теми параметрами, с которыми они были запущены изначально, т.е. никаких дополнительных действий при перезапуске сервера совершать не требуется.


Шаг 3: GitLab CI


Создадим в корне репозитория файл .gitlab-ci.yml с инструкциями для GitLab CI:


.gitlab-ci.yml
# Сообщаем Gitlab CI, что мы будем использовать Docker при сборке.
image: docker:latest
services:
    - docker:dind

# Описываем, из каких ступеней будет состоять наша непрерывная интеграция:
#  - сборка Docker-образа,
#  - прогон тестов Django,
#  - выкат на боевой сервер.
stages:
    - build
    - test
    - deploy

# Описываем инициализационные команды, которые необходимо запускать
#  перед запуском каждой ступени.
# Изменения, внесённые на каждой ступени, не переносятся на другие, так как запуск
#  ступеней осуществляется в чистом Docker-контейнере, который пересоздаётся каждый раз.
before_script:
    # установка pip
    - apk add --no-cache py-pip
    # установка docker-compose
    - pip install docker-compose==1.9.0
    # логин в Gitlab Docker registry
    - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY

# Сборка Docker-образа
build:
    stage: build
    script:
        # собственно сборка
        - docker-compose build
        # отправка собранного в registry
        - docker-compose push

# Прогон тестов
test:
    stage: test
    script:
        # вместо повторной сборки, забираем собранный на предыдущей ступени
        #  готовый образ из registry
        - docker-compose pull test
        # запускаем тесты
        - docker-compose run test

# Выкат на сервер
deploy:
    stage: deploy
    # выкатываем только ветку master
    only:
        - master
    # для этой ступени другие команды инициализации
    before_script:
        # устанавливаем зависимости Fabric, bash и rsync
        - apk add --no-cache openssh-client py-pip py-crypto bash rsync
        # устанавливаем Fabric
        - pip install fabric==1.12.0
        # добавляем приватный ключ для выката
        - eval $(ssh-agent -s)
        - bash -c 'ssh-add <(echo "$DEPLOY_KEY")'
        - mkdir -p ~/.ssh
        - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
    script:
        - fab -H $DEPLOY_ADDR deploy

Стоит отметить, что Docker-runner'ы GitLab CI, которые мы используем, в качестве основы используют всё тот же образ Alpine Linux, что создаёт ряд трудностей — из коробки нет bash, непривычный пакетный менеджер apk, непривычная стандартная библиотека musl-libc и др. Трудности компенсируются тем, что образы на основе Apline Linux получаются действительно легковесными; так, официальный образ python:3.5.2-alpine весит всего 27.6 MB.


Шаг 4: Fabric


Для выката приложения на сервер нужно в корневой же директории репозитория создать файл fabfile.py, как минимум содержащий следующее:


fabfile.py
#!/usr/bin/env python2

from fabric.api import hide, env, settings, abort, run, cd, shell_env
from fabric.colors import magenta, red
from fabric.contrib.files import append
from fabric.contrib.project import rsync_project
import os

env.user = 'root'
env.abort_on_prompts = True
# TODO: Смените на путь на сервере, по которому будут скопированы файлы приложения
PATH = '/srv/mywebapp'
ENV_FILE = '/etc/profile.d/variables.sh'
VARIABLES = ('SECRET_KEY', )

def deploy():
    def rsync():
        exclusions = ('.git*', '.env', '*.sock*', '*.lock', '*.pyc', '*cache*',
                      '*.log',  'log/', 'id_rsa*', 'maintenance')
        rsync_project(PATH, './', exclude=exclusions, delete=True)

    def docker_compose(command):
        with cd(PATH):
            with shell_env(CI_BUILD_REF_NAME=os.getenv(
                    'CI_BUILD_REF_NAME', 'master')):
                # прячем прогресс-бар, см. https://git.io/vXH8a
                run('set -o pipefail; docker-compose %s | tee' % command)

    # Сохраняем переменные на сервере
    variables_set = True
    for var in VARIABLES + ('CI_BUILD_TOKEN', ):
        if os.getenv(var) is None:
            variables_set = False
            print(red('ERROR: environment variable ' + var + ' is not set.'))
    if not variables_set:
        abort('Missing required parameters')
    with hide('commands'):
        run('rm -f "%s"' % ENV_FILE)
        append(ENV_FILE,
               ['export %s="%s"' % (var, val) for var, val in zip(
                   VARIABLES, map(os.getenv, VARIABLES))])
    # Fabric перечитывает переменные из профиля при каждом вызове run(),
    #  поэтому нет смысла делать это явно. см. http://stackoverflow.com/q/38024726/1336774

    # Логинимся в registry
    run('docker login -u %s -p %s %s' % (os.getenv('REGISTRY_USER',
                                                   'gitlab-ci-token'),
                                         os.getenv('CI_BUILD_TOKEN'),
                                         os.getenv('CI_REGISTRY',
                                                   'registry.gitlab.com')))

    # Выполняем начальную установку, если нужно
    with settings(warn_only=True):
        with hide('warnings'):
            need_bootstrap = run('docker ps | grep -q web').return_code != 0
    if need_bootstrap:
        print(magenta('No previous installation found, bootstrapping'))
        rsync()
        docker_compose('up -d')

    # Включаем заглушку "технические работы", см. https://habr.ru/post/139968
    run('touch %s/nginx/maintenance && docker kill -s HUP nginx_1' % PATH)
    rsync()
    docker_compose('pull')
    docker_compose('up -d')
    # Убираем заглушку
    run('rm -f %s/nginx/maintenance && docker kill -s HUP nginx_1' % PATH)

Вообще говоря, копировать rsync'ом весь репозиторий необязательно, для запуска было бы достаточно файла docker-compose.yml и содержимого директории nginx.
Код приложения хранится на сервере на случай, если вдруг понадобится внести срочные изменения "наживую". На бесплатных аккаунтах gitlab.com для запуска CI используется сравнительно слабое виртуализированное железо, поэтому сборка, тесты и выкат, как правило, происходят за 5-10 минут.


image
(правда, бывает, что они до этого ещё в очереди торчат целую вечность)


Однако бывают случаи, когда каждая секунда на счету — для таких случаев мы и оставляем лазейку в виде полных исходников приложения. Для применения изменений, внесённых "наживую", достаточно перейти в директорию /srv/mywebapp и сказать в консоли


docker-compose build
docker-compose up -d

Заключение


Таким образом, мы реализовали непрерывную интеграцию веб-приложения с помощью сервиса GitLab.


image


Теперь все изменения будут прогоняться через батарею автоматических тестов (которые, разумеется, тоже нужно написать), а изменения в главной ветке будут автоматически разворачиваться на боевой сервер с околонулевым временем простоя.


За рамками статьи остались следующие вопросы:


  • настройка ротации журналов Nginx;
  • настройка бэкапов PostgreSQL;
  • настройка docker-gc для предотвращения неловкой ситуации "No space left on device".

Оставим их пытливому читателю в качестве самостоятельного упражнения.


Ссылки


» GitLab CI: Учимся деплоить
» GitLab CI Quick Start
» GitLab Container Registry
» Django на production. uWSGI + nginx. Подробное руководство
» Fabric documentation

Поделиться с друзьями
-->

Комментарии (33)


  1. darken99
    25.11.2016 15:27
    +1

    А что и зачем вы копируете rsync'ом если у вас код в образе докера?


    1. prefrontalCortex
      25.11.2016 15:54

      rsync'ом я копирую всё содержимое репозитория, чтобы можно было его в случае чего подредактировать на сервере, не заходя внутрь докера. Как мне кажется, это проще, чем вспоминать ключи команды docker exec, а потом ещё посылать HUP всем затронутым процессам.


      1. darken99
        25.11.2016 16:13

        А что мешает сделать профиль для девелопмента в docker-compose и менять себе на здоровье локально перед коммитом?
        Может для тестовой среды такое еще и можно делать, но если это production...


        1. prefrontalCortex
          25.11.2016 16:27

          Насчёт отладочного профиля в docker-compose вы правы, так и нужно делать.
          Но без rsync'а все равно не обойтись — конфиги для Nginx (и, потенциально, для других частей стека) хранятся в репозитории.


          1. darken99
            25.11.2016 16:34
            +1

            Вопрос конфига для nginx решается через переменные, шаблоны и dockerize.
            Ну и как бы конфигурация должна храниться отдельно.


            А в целом спасибо за статью, у меня как раз в процессе перевод django приложения в Docker, возьму статью за основу :)


            1. prefrontalCortex
              25.11.2016 16:37

              dockerize

              Ух ты, не знал про такую штуку, спасибо, изучу :)


            1. renskiy
              25.11.2016 18:45
              +1

              у меня как раз в процессе перевод django приложения в Docker

              Мы тоже решали эту задачу. Решили на наш взгляд довольно лаконично. Плюс: нулевой простой, применение и откат миграций и другие радости Docker.

              prefrontalCortex, кстати, тоже на Fabric


              1. prefrontalCortex
                25.11.2016 19:02

                Вы, я так понимаю, автор того самого fabricio? Я в процессе построения описанного в статье решения на него натыкался, но решил, что быстрее напишу свой quick-and-dirty fabfile.


                У меня всё-таки больше упор на инфраструктуру Gitlab, в котором даже закрытый Docker registry дают на халяву и не нужно самому его поднимать.


                1. renskiy
                  25.11.2016 19:33
                  +1

                  Fabricio поддерживает любые registry. Например, если вы хотите использовать gitlab, то можно использовать следующий конфиг:

                  from fabricio import tasks
                  from fabricio.apps.python.django import DjangoContainer
                  
                  my_app = tasks.ImageBuildDockerTasks(
                      container=DjangoContainer(
                          name='my_app',
                          image='user/my_image',
                          options=dict(
                              ports='8000:8000',
                          ),
                      ),
                      registry='registry.gitlab.com',
                      hosts=['user@example.com'],
                  )
                  

                  Fabricio только по-умолчанию пытался работать с локальным registry (сейчас этот механизм deprecated, чтобы не сбивать больше пользователей с толку, теперь надо всегда задавать registry вручную, если нужно использовать репозитарий, отличный от hub.docker.com).


                  1. renskiy
                    25.11.2016 19:40

                    Можно даже опустить отдельное указание registry и прописать его в самом названии образа:

                    image='registry.gitlab.com/user/my_image',
                    


                    1. prefrontalCortex
                      25.11.2016 19:48
                      +1

                      Спасибо, попробую.


            1. Sovetnikov
              26.11.2016 11:24
              +1

              «через переменные, шаблоны и dockerize» — немного непонятно.
              Если мне надо деплоить вместе с кодом проекта конфиг nginx сайта проекта, то как оно должно быть устроено правильнее?
              И где должна храниться конфигурация?


              1. p0g0rel0v55
                27.11.2016 18:53

                я через envsubst конфиг nginx собираю прямо в контейнере, те туда темплейт конфига а переменные уже при запуске подставляю но это не dockerize… а только переменные и шаблоны


                1. Sovetnikov
                  28.11.2016 13:20

                  Это логично и понятно, а про что darken99 писал я не понял…


  1. EgorLyutov
    25.11.2016 22:49
    +1

    А как правильно разделить, если пуш не в мастер, то собирать контейнер в конфигурации для разработки, ну и соответсвенно, если в мастер, то для продакта?


    1. prefrontalCortex
      26.11.2016 11:39

      Хороший вопрос. Я бы попробовал из .gitlab-ci.yml вызывать шелл-скрипт, который на основе переменной CI_BUILD_REF_NAME будет производить нужные действия, как-то в таком духе


      build.sh
      if [ "$CI_BUILD_REF_NAME" = "master" ]; then
          docker-compose build
      else
          docker-compose -f debug.yml build
      fi


    1. Sovetnikov
      26.11.2016 22:35

      В шагах .gitlab-ci.yml можно выбирать, при коммите в какой бранч выполнять шаг.
      И соответственно разные шаги для разных бранчей с разными скриптами.

      Вот тут подробно https://habrahabr.ru/company/softmart/blog/310502/
      Вот тут официальная документация https://docs.gitlab.com/ce/ci/yaml/README.html#only-and-except


  1. Sovetnikov
    26.11.2016 22:30

    До zero downtime развертывания не планируете довести? :)
    Было бы тоже интересно.


    1. prefrontalCortex
      26.11.2016 22:43

      Пока нет, но тема интересная и требующая подробного изучения :)


    1. renskiy
      27.11.2016 11:17
      +1

      с контейнерами можно осуществить только near-zero downtime. Полноценный zero downtime гарантируется только при использовании кластерных решений (Swarm, Kubernetes, Mesos).

      PS: можно конечно постараться осуществить нулевой простой и без помощи кластера контейнеров. Например, играясь с сетевой маршрутизацией между двумя контейнерами по аналогии с сине-зеленым деплоем. Но в этом случае вы скорее всего сделаете свой довольно сложный велосипед, тогда как перечисленные выше решения уже дают необходимое свойство.


      1. Sovetnikov
        27.11.2016 11:57

        Тут варианты:
        1. nginx+uwsgi в одном контейнере — да, тут кластер или маршрутизацией управлять.

        2. nginx отдельно (отдельный контейнер или непосредственно на сервере) от uwgi — в этом случае можно ведь настроить nginx failover с proxy_next_upstream и нулевым timeout?
        Спрашиваю, потому что на практике не делал, но думаю что работает.

        3. Исходные коды храняться в data volume (а не в инстансе как в пп.1 и 2), который цепляется в инстанс uwsgi — тут можно использовать возможность uwsgi последовательно перезагружать воркеры, у них довольно подробная инструкция есть на эту тему.

        Всё это конечно с допусками:
        — Нет изменений в БД, которые не позволяют использовать предыдущую кодовую базу с новой версией БД.
        — Время простоя на перезагрузку конфигурации nginx считаем незначительной и отдельный балансировщик делать не будем :)

        Или что-то не так?


        1. renskiy
          27.11.2016 13:53

          1. В одном контейнере лучше не запускать более одного сервиса. А маршрутизацию можно осуществлять при помощи iptables (Docker так и делает).

          2. Этот вариант в общем-то работает, но не дает zero downtime как такового, ибо nginx в данном случае пытается перенаправить запрос на следующий сервер, только если первый вернул ошибку либо отвалился по таймауту. В случае не идемпотентных запросов такое поведение опасно.

          3. Этот вариант немного костыльный (зачем, например, нужны контейнеры, если исходный код сервиса все равно находится снаружи, а если так и нужна просто изоляция, то есть virtualenv и другие инструменты). И уж точно этот вариант не универсальный, то есть будет работать только при определенных условиях (как вы сами заметили) и только в случае с uwsgi.


          1. Sovetnikov
            27.11.2016 14:26

            1. Это понятно

            2. nginx ведь не вернет ошибку, он внутри обработает запрос на доступный uwsgi. Клиент увидит только результат того uwsgi, который работает. Ошибки не увидит.
            Попытка коннекта на закрытый порт это мне кажется всего несколько миллисекунд, а то и меньше.

            А в случае не идемпотентных запросов, что может произойти?
            Два инстанса сразу не должны быть доступны для запросов от клиента.
            Но получается, что всёравно надо будет портами играться… надо ждать пока новый инстанс запустится, гасить порт старого, открывать порт нового.
            В общем понятно.

            3. Хранение исходных кодов в data volume (или в host директории) или в контейнере это разные стратегии применяемые в разных условиях (проект, команда, организация, куда идёт развертывание и т.п.).
            virtualenv vs docker? Да ну его этот virtualenv…
            uwsgi прекрасно работает, зачем гонять образы, если можно заменить только код и пользоваться его возможностями.


            1. renskiy
              27.11.2016 17:06

              А в случае не идемпотентных запросов, что может произойти?

              повторять не идемпотентные запросы нельзя, потому что если первый сервер ответил ошибкой на такой запрос, то вполне вероятно, что он мог успеть обновить данные в БД. Если nginx потом перенаправит этот запрос на резервный сервер, то данные могут обновиться дважды.


              1. Sovetnikov
                27.11.2016 18:24

                Я и пишу, что «Два инстанса сразу не должны быть доступны для запросов от клиента.»


  1. renskiy
    27.11.2016 17:03

    удалено


  1. p0g0rel0v55
    27.11.2016 17:08

    А зачем копировать исходники проекта в контейнер с окружением? почему б не сделать 2 контейнера и не пересобирать окружение каждый раз? можно его тогда вообще в открытом репозитарии хранить будет и использовать например для запуска контейнера с celery.


    1. prefrontalCortex
      27.11.2016 18:04

      почему б не сделать 2 контейнера

      Проще уж тогда вообще один контейнер сделать и к нему подключать volume'ом директорию с кодом. В принципе, такой вариант тоже имеет право на существование.


      Контейнеры с celery я, например, вообще запускал ровно из того же образа, в котором жили Django с uwsgi — это удобно, так как демону celery в любом случае понадобятся настройки Django, как минимум настройки доступа к БД.


      1. p0g0rel0v55
        27.11.2016 18:50
        +1

        согласен, у меня так в одном проекте и сделано в elastic beanstalk
        1 контейнер с uwsgi
        2 от него унаследован образ для celery, изза особеностей EB так удобней стартовый скрипт менять
        3 контейнер nginx
        4 контейнер logentries который пишет логи собирая их из docker api

        скелет приложения в открытом репозитарие не совсем допилен но более менее понятна структура


        1. prefrontalCortex
          27.11.2016 19:20

          контейнер logentries который пишет логи собирая их из docker api

          Теоретически, можно было обойтись без отдельного контейнера, докер много куда изкоробки умеет логи писать, в том числе и в syslog, откуда практически любой сервис по работе с логами умеет их забирать (например, loggly).


          1. p0g0rel0v55
            27.11.2016 20:50

            да, я даже делал такие конфигурации, но с отдельным контейнером получилось универсальней, не надо ничего писать в конфиг хоста на котором докер запущен… те сейчас везде где докер работает можно запустить этот конфиг у разработчика без дополнительных конфигов


  1. M_Muzafarov
    28.11.2016 10:57
    +1

    Спасибо за статью, может наконец сподвигнусь еще раз попробовать контейнирозвать проект.


    Есть вопросы по решению, хотелось бы обсудить:


    1. Забавное решение с uwsgi (while true). Правда ничего лучше так и нет?
    2. Зачем в docker-compose выставлять наружу каждый порт? Они ж друг-другу итак доступны за счет network. Это как биндить всё на 0.0.0.0, когда на самом деле все эти порты (кроме nginx конечно), нужны только внутри.


    1. prefrontalCortex
      28.11.2016 12:22

      1. Можно взять за основу образ alpine:edge (ну, или 3.5, когда выйдет), там uwsgi есть пакетом. Ну или вообще Ubuntu какую-нибудь.
      2. А ведь ваша правда. Я-то у себя на продакшене через unix domain socket'ы сделал. Спасибо, поправил статью.