С другой стороны, есть разрабатываемое Rails приложение, которое на реальной машине держит только код; Rails, gems, и все остальное покоится в Docker контейнере. Для своей работы оно использует Redis и Postgres, каждый находится в своем контейнере. Для каждого контейнера примонтирована директория, чтобы важные для приложения данные не оставались внутри.

Задача в том, чтобы Gitlab CI нормально отработал. Вроде все просто, но — он сам находится в контейнере.
services: postgres: image: postgres:9.4.5 volumes: - /var/db/test/postres:/var/lib/postgresql/data env_file: .env ports: - "54321:5432" redis: image: redis:latest volumes: - /var/db/test/redis:/data app: build: . env_file: .env volumes: - /var/www/test:/var/www/test ports: - "3000" links: - postgres - redis
В .env файле лежит например пароль к базе данных, POSTGRES_PASSWORD. Здесь можно посмотреть, какие переменные используются в контейнере postgresql. Вместо ports можно использовать expose, но для мониторинга с хоста лучше открыть окно в мир.
К Redis'у замечаний нет вообще — подключил, он сам сделал expose на 6379, и работает. Всем бы так.
Первая мысль — инсталлировать Docker в Docker'е. Я верю в человечество, и в то, что в мире есть человек, который за разумное время сделал настоящую вложенность, с установкой Docker в контейнер, которая реально работает, но лично мне это не удалось. Значит, нужен второй вариант — тестировать извне: все контейнеры должны находиться рядом с контейнером GitLab, автоматически создаваться и уничтожаться системой CI изнутри контейнера.

Для тестов нам не нужно монтировать внешние директории, все равно данные одноразовые. После тестов все контейнеры автоматически останавливаются.
Шарим Docker
Не рекомендуется давать недоверенным программам в контейнерах доступ к Docker'у на хосте. Имея таковой, можно легко запустить создание другого контейнера на хосте, который примонтирует нужную директорию или даже целый том (или devices, к примеру веб-камеру или принтер), установит нужный злоумышленнику код, и неспешно вытянет все ваши биткоины, и все это не покидая уютной песочницы. Но поскольку я тестирую свой код, предостережением можно пренебречь.
Чтобы позволить контейнеру управлять своим отцом, нужно пробросить ему родительский сокет. В идеальном мире это делается так:
docker run .. -v /var/run/docker.sock:/var/run/docker.sock
но для тех, кто не ищет в жизни легких путей, оно не работает. Нужно во-первых добавить еще и
-v /usr/bin/docker:/usr/bin/docker
потом обнаружить, что это работает только если бинарники докера на хосте статические (не спрашивайте), иначе придется позаботиться о каждой библиотеке в этой директории отдельно. Если your host's docker binary is not static (не спрашивайте!), как у меня, значит работаем дальше.
Смотрим список библиотек в /usr/bin/docker:
ldd /usr/bin/docker
linux-vdso.so.1 (0x00007fffb9ff4000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f711fe27000) libltdl.so.7 => /usr/lib/x86_64-linux-gnu/libltdl.so.7 (0x00007f711fc1d000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f711f871000) /lib64/ld-linux-x86-64.so.2 (0x000055a205b12000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f711f66d000)
Прописав в docker-compose.yml Gitlab'а (не проекта, не перепутайте) пути, которые кажутся интуитивными, и попробовав стартовать контейнер Gitlab:
volumes: ... - /var/run/docker.sock:/var/run/docker.sock - /usr/bin/docker:/usr/bin/docker - /usr/bin/docker/libltdl.so.7:/usr/bin/docker/libltdl.so.7
docker при старте разразился страшной руганню, которая была столь груба, что почти каждое слово экранировалось семижды!
ERROR: for gitlab Cannot start service gitlab: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"process_linux.go:359: container init caused \\\"rootfs_linux.go:53: mounting \\\\\\\"/usr/bin/docker/libltdl.so.7\\\\\\\" to rootfs \\\\\\\"/var/lib/docker/devicemapper/mnt/2df228e042aed186eebbb484989e44cee3126c0a3bfb42d25c8998ada5afb9bd/rootfs\\\\\\\" at \\\\\\\"/usr/bin/docker/libltdl.so.7\\\\\\\" caused \\\\\\\"stat /usr/bin/docker/libltdl.so.7: not a directory\\\\\\\"\\\"\"\n"
Решение «в лоб»:
volumes: ... - /var/run/docker.sock:/var/run/docker.sock - /usr/bin/docker:/usr/bin/docker - /usr/lib/x86_64-linux-gnu/libltdl.so.7:/usr/lib/x86_64-linux-gnu/libltdl.so.7
То есть нам нужны пути к файлам с правой части таблички с библиотеками.
gitlab: image: 'gitlab/gitlab-ce:8.14.5-ce.0' restart: unless-stopped hostname: 'git.habrahabr.ru' environment: GITLAB_OMNIBUS_CONFIG: "external_url 'http://git.habrahabr.ru'" ports: - "3456:80" # если ваш контейнер не один претендует на 80-й порт. - "52022:22" volumes: - /docker/gitlab/data/conf:/etc/gitlab - /docker/gitlab/data/logs:/var/log/gitlab - /docker/gitlab/data/data:/var/opt/gitlab - /var/run/docker.sock:/var/run/docker.sock - /usr/bin/docker:/usr/bin/docker - /usr/lib/x86_64-linux-gnu/libltdl.so.7:/usr/lib/x86_64-linux-gnu/libltdl.so.7
Создаем .gitlab-ci.yml
image: "ruby:2.3" services: - redis:latest - postgres:9.4.5 cache: paths: - vendor/ruby before_script: - apt-get update -q && apt-get install nodejs -yqq # стоит это откоментирровать сразу. Сохраните 5-10 минут жизни. - gem install bundler --no-ri --no-rdoc - bundle install -j $(nproc) --path vendor rspec: stage: test script: - bundle exec rake db:create - bundle exec rake db:migrate - bundle exec rake db:seed - rspec spec
Чтобы верно создать этот файл, лучше всего смотреть на docker-compose.yml приложения. К примеру, контейнер postgres — подключать volumes нам не нужно, env файл с паролем к базе данных не нужен (нам ведь все равно, какой пароль в базы, которая живет 10 секунд), порты наружу тоже. То есть остается только указать имя образа (image). Redis: volumes не нужны, так что только имя образа. Главный образ это ruby, в который Gitlab смонтирует код, и запустит before_script.
Вместо эпилога
На настроенный подобным образом GitLab можно навернуть что угодно, от Registry образов до Docker-in-docker решений. Например, вместо разворачивания кода в ruby контейнере средствами GitLab создать решение на основе gitlab/dind, в котором запустить docker build. Но это предмет совсем другой статьи.

