Docker: PHP Composer и multi-stage билды Docker образов

By | 04/02/2018
 

Задача – подготовить Docker образ с PHP Composer.

Ниже рассмотрим сначала сам Composer (от PHP далёк, и с Composer дела раньше не имел, хотя сам PHP потрогать довелось), потом – пример сборки Docker контейнера и его использование под разными пользователями.

Результат можно посмотреть в Github.

PHP Composer

Composer предназначен для установки общих библиотек при создании PHP-проекта. Как ближайший аналог из “мира” NodeJS – npm, который выполняет установку различных зависимостей.

Кроме Composer для PHP имеется PEAR, но он сейчас практически не используется (хотя Composer поддерживает установку пакетов из PEAR).

Как и для npm – у Composer имеется файл, в котором перечисляются необходимые для установки зависимости и их версии, и репозитории.

Для Composer дефолтный репозиторий – https://packagist.org, кроме него в роли репозитория можно использовать любой VCS типа Github.

Установка Composer

Сначала – установим Composer локально.

На Linux выполняем:

curl -s https://getcomposer.org/installer | php
All settings correct for using Composer
Downloading...
Composer (version 1.6.3) successfully installed to: /home/setevoy/Work/composer.phar
Use it: php composer.phar

Проверяем тип файла:

file composer.phar
composer.phar: a /usr/bin/env php script executable (binary data)

Проверяем его работу:

php composer.phar
______
/ ____/___  ____ ___  ____  ____  ________  _____
/ /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
/_/
Composer version 1.6.3 2018-01-31 16:28:17
Usage:
command [options] [arguments]
Options:
-h, --help                     Display this help message
-q, --quiet                    Do not output any message
-V, --version                  Display this application version
--ansi                     Force ANSI output
...

Переносим в /usr/loca/bin:

sudo mv composer.phar /usr/local/bin/composer
sudo chmod +x /usr/local/bin/composer

Создадим тестовый проект:

mkdir ~/Scripts/PHP/ComposerTest
cd ~/Scripts/PHP/ComposerTest/

Например у нас есть зависимость от PHP фрейморка Slim – в каталоге PHP проекта создадим файл composer.json, в котором описываем зависимость:

{
    "require": {
        "slim/slim": "2.*"
    }
}

Для npm дефолтным каталог является ~/node_modules:

npm root
/home/setevoy/node_modules

А для composervendor в каталоге, из которого вызвается composer.

Запускаем установку:

composer install
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
- Installing slim/slim (2.6.3): Downloading (100%)
slim/slim suggests installing ext-mcrypt (Required for HTTP cookie encryption)
slim/slim suggests installing phpseclib/mcrypt_compat (Polyfil for mcrypt extension)
Writing lock file
Generating autoload files

Проверяем каталог vendor:

tree vendor/
vendor/
├── autoload.php
├── composer
│   ├── autoload_classmap.php
│   ├── autoload_namespaces.php
│   ├── autoload_psr4.php
│   ├── autoload_real.php
│   ├── autoload_static.php
│   ├── ClassLoader.php
│   ├── installed.json
│   └── LICENSE
└── slim
└── slim
├── composer.json
├── CONTRIBUTING.md
├── index.php
├── LICENSE
├── phpunit.xml.dist
├── README.markdown
├── Slim
│   ├── Environment.php
│   ├── Exception
...

В случае, если установку надо выполнить от другого пользователя, что бы установленные файлы имели другой UID/GID – используем, например, sudo:

sudo useradd phpcomposeruser
rm -rf vendor/
ls -l
total 12
-rw-r--r-- 1 setevoy         setevoy           51 Mar 17 11:59 composer.json
-rw-r--r-- 1 setevoy         setevoy         2192 Mar 17 12:01 composer.lock
sudo -u phpcomposeruser composer install
Cannot create cache directory /home/phpcomposeruser/.composer/cache/repo/https---packagist.org/, or directory is not writable. Proceeding without cache
Cannot create cache directory /home/phpcomposeruser/.composer/cache/files/, or directory is not writable. Proceeding without cache
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Package operations: 1 install, 0 updates, 0 removals
- Installing slim/slim (2.6.3): Downloading (100%)
slim/slim suggests installing ext-mcrypt (Required for HTTP cookie encryption)
slim/slim suggests installing phpseclib/mcrypt_compat (Polyfil for mcrypt extension)
Generating autoload files

Проверяем:

ls -l vendor/slim/
total 4
drwxr-xr-x 4 phpcomposeruser phpcomposeruser 4096 Mar 17 12:08 slim

Docker multi-stage билд

Следующей задачей будет собрать Docker образ, который будет включать в себя PHP и Composer.

Тут можно использовать multi-stage билды, которые появились в Docker в версии 17.05.

Идея заключается в том, что мы запускаем контейнер с Composer, затем – контейнер с желаемой версией PHP, копируем из контейнера с Composer его исполняемый phar-файл, и создаём новый контейнер, из которого собираем свой образ.

Dockerfile будет выглядеть так:

FROM composer:latest AS composer
FROM php:7.2.3
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN composer --version && php --v

В первой строке, в инструкции FROM, используем оператор AS, что бы задать имя запускаемому во время билда контейнеру, а затем – ссылаемся на это имя в COPY, что бы из него скопировать необходимый файл, в данном случае – /usr/bin/composer.

Собираем образ:

docker build -t php-composer:1.0 .
Sending build context to Docker daemon  502.8kB
Step 1/4 : FROM composer:latest AS composer
---> 2135a91c923b
Step 2/4 : FROM php:7.2.3
---> c8d1a5f14eb7
Step 3/4 : COPY --from=composer /usr/bin/composer /usr/bin/composer
---> Using cache
---> c6833892ea80
Step 4/4 : ENTRYPOINT ["/usr/bin/composer"]
---> Running in 0169f3c917ae
Removing intermediate container 0169f3c917ae
---> 10b6c61224b9
Successfully built 10b6c61224b9
Successfully tagged php-composer:1.0

Проверяем:

docker run -ti php-composer:1.0 --version
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Composer version 1.6.3 2018-01-31 16:28:17

ОК – всё работает.

Репозитории в composer.json

Попробуем установить библиотеку geoip2, используя собранный образ.

Для этого потребуется указать репозиторий, из которого библиотеку можно скачать – используем repositories, обновляем composer.json:

{
    "require": {
        "geoip2/geoip2": "~2.0"
    },  
    "repositories": [{
        "type": "vcs",
        "url": "git@github.com:antimattr/GoogleBundle.git"}
    ]
}

Обновим Dockerfile – добавим WORKDIR для указания каталога, в котором Composer будет искать composer.json:

FROM composer:latest AS composer
FROM php:7.2.3
COPY --from=composer /usr/bin/composer /usr/bin/composer
WORKDIR /app

Убираем ENTRYPOINTcomposer будем вызывать явно, при запуске контейнера.

Собираем v2:

docker build -t php-composer:2.0 .
Sending build context to Docker daemon   7.68kB
Step 1/4 : FROM composer:latest AS composer
---> 2135a91c923b
Step 2/4 : FROM php:7.2.3
---> c8d1a5f14eb7
Step 3/4 : COPY --from=composer /usr/bin/composer /usr/bin/composer
---> Using cache
---> c6833892ea80
Step 4/4 : WORKDIR /app
---> Using cache
---> 68ece553d2ef
Successfully built 68ece553d2ef
Successfully tagged php-composer:2.0

Проверяем:

docker run -ti --volume $(pwd)/:/app php-composer:2.0 composer --version
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Composer version 1.6.3 2018-01-31 16:28:17

ОК, работает.

Удаляем composer.lock, который остался от установки Slim в начале:

cat composer.lock
...
"packages": [
{
"name": "slim/slim",
"version": "2.6.3",
"source": {
"type": "git",
"url": "https://github.com/slimphp/Slim.git",
...
rm composer.lock

И запускаем контейнер, передавая composer install:

docker run -ti --volume $(pwd)/:/app php-composer:2.0 composer install
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 4 installs, 0 updates, 0 removals
Failed to download composer/ca-bundle from dist: The zip extension and unzip command are both missing, skipping.
A php.ini file does not exist. You will have to create one.
Now trying to download from source
- Installing composer/ca-bundle (1.1.0): Cloning 943b2c4fca
[RuntimeException]
Failed to clone https://github.com/composer/ca-bundle.git, git was not found, check that it is installed and in your PATH env.
sh: 1: git: not found

Отлично! Всё работает. Вот только git-а нет 🙂

Т.к. просто скопировать исполняемый файл git – не вариант из-за зависимостей от системных библиотек, добавим его установку.

Обновляем Dockerfile:

FROM composer:latest AS composer
FROM php:7.2.3
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN apt update && apt install -y git
WORKDIR /app

Собираем образ:

docker build -t php-composer:3.0 .
...
Successfully tagged php-composer:3.0

И запускаем composer install:

docker run -ti --volume $(pwd)/:/app php-composer:3.0 composer install
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Loading composer repositories with package information
Reading composer.json of antimattr/google-bundle (v2.0.0)
Updating dependencies (including require-dev)
Package operations: 4 installs, 0 updates, 0 removals
Failed to download composer/ca-bundle from dist: The zip extension and unzip command are both missing, skipping.
A php.ini file does not exist. You will have to create one.
Now trying to download from source
- Installing composer/ca-bundle (1.1.0): Cloning 943b2c4fca from cache
Failed to download maxmind/web-service-common from dist: The zip extension and unzip command are both missing, skipping.
A php.ini file does not exist. You will have to create one.
Now trying to download from source
- Installing maxmind/web-service-common (v0.5.0): Cloning 61a9836fa3 from cache
Failed to download maxmind-db/reader from dist: The zip extension and unzip command are both missing, skipping.
A php.ini file does not exist. You will have to create one.
Now trying to download from source
- Installing maxmind-db/reader (v1.3.0): Cloning e042b4f8a2 from cache
Failed to download geoip2/geoip2 from dist: The zip extension and unzip command are both missing, skipping.
A php.ini file does not exist. You will have to create one.
Now trying to download from source
- Installing geoip2/geoip2 (v2.8.0): Cloning 63b0d87d47 from cache
maxmind-db/reader suggests installing ext-bcmath (bcmath or gmp is required for decoding larger integers with the pure PHP decoder)
maxmind-db/reader suggests installing ext-gmp (bcmath or gmp is required for decoding larger integers with the pure PHP decoder)
maxmind-db/reader suggests installing ext-maxminddb (A C-based database decoder that provides significantly faster lookups)
Writing lock file
Generating autoload files

ОК, теперь всё работает.

Composer user

Последняя проблема, которую осталось решить – это пользователь:

ls -l
total 24
-rw-r--r-- 1 setevoy setevoy  142 Mar 17 14:18 composer.json
-rw-r--r-- 1 root    root    8286 Mar 17 14:18 composer.lock
-rw-r--r-- 1 setevoy setevoy  155 Mar 17 14:14 Dockerfile
drwxr-xr-x 6 root    root    4096 Mar 17 14:18 vendor

vendor и composer.lock создаются от рута, т.к. в самом контейнере используется пользователь root, если не указано другое.

Проверим – добавляем RUN whoami в Dockerfile:

FROM composer:latest AS composer
FROM php:7.2.3
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN apt update && apt install -y git
RUN whoami
WORKDIR /app

Собираем:

docker build -t php-composer:3.1 .
...
Step 5/6 : RUN whoami
---> Running in 8707eba18a3b
root
...
Successfully tagged php-composer:3.1

А мы хотим, что бы каталог vendor на хосте оставался за пользователем phpcomposeruser, которого мы создали в начале.

Тут есть два варианта.

Dockerfile USER

Первый – добавить пользователя phpcomposeruser во время сборки контейнера, и использовать инструкцию USER.

Обновляем Dockerfile:

FROM composer:latest AS composer
FROM php:7.2.3
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN apt update && apt install -y git
RUN adduser phpcomposeruser
USER phpcomposeruser
RUN whoami
RUN id
WORKDIR /app

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

docker build -t php-composer:3.2 .
...
Step 5/9 : RUN useradd phpcomposeruser
---> Using cache
---> f7003b3a0afb
Step 6/9 : USER phpcomposeruser
---> Running in c4d318683899
Removing intermediate container c4d318683899
---> ca2ce1f6854c
Step 7/9 : RUN whoami
---> Running in 9f8b4339ff66
phpcomposeruser
Removing intermediate container 9f8b4339ff66
---> 9c2670aba71f
Step 8/9 : RUN id
---> Running in 288eab068311
uid=1000(phpcomposeruser) gid=1000(phpcomposeruser) groups=1000(phpcomposeruser)
...
Successfully tagged php-composer:3.2

Удаляем каталог vendor и composer.lock:

sudo rm -rf vendor/ && sudo rm composer.lock

Собираем проект, используя версию 3.2:

docker run -ti --volume $(pwd)/:/app php-composer:3.2 composer install

Проверяем файлы:

ls -l
total 24
-rw-r--r-- 1 setevoy setevoy  142 Mar 17 14:37 composer.json
-rw-r--r-- 1 setevoy setevoy 8286 Mar 17 14:37 composer.lock
-rw-r--r-- 1 setevoy setevoy  222 Mar 17 14:34 Dockerfile
drwxr-xr-x 6 setevoy setevoy 4096 Mar 17 14:37 vendor

Упс! Откуда взялся setevoy, если мы указывали phpcomposeruser?

Вернёмся к логу сборки образа:


Step 8/9 : RUN id
—> Running in 288eab068311
uid=1000(phpcomposeruser) gid=1000(phpcomposeruser) groups=1000(phpcomposeruser)

А теперь проверим – кому принадлежит UID 1000 на хосте, с которого мы билдим:

getent passwd 1000
setevoy:x:1000:1000::/home/setevoy:/bin/bash

Отлично… Пользователь с UID 1000 в образе – это phpcomposeruser (первый созданный в системе пользователь, после рута), а на хосте – это пользователь setevoy.

Соответственно, на хосте, с которого запускаем билд, у пользователя phpcomposeruser == UID 1001:

id phpcomposeruser
uid=1001(phpcomposeruser) gid=1002(phpcomposeruser) groups=1002(phpcomposeruser)

И особых прав на каталог vendor у него нет:

sudo -u phpcomposeruser touch vendor/file
touch: cannot touch 'vendor/file': Permission denied

docker run --user

Другой вариант решить проблему – запускать контейнер от определённого пользователя, используя --user.

Убираем USER из Dockefile, возвращаем его к виду:

FROM composer:latest AS composer
FROM php:7.2.3
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN apt update && apt install -y git
WORKDIR /app

Собираем версию 3.3:

docker build -t php-composer:3.3 .

Удаляем каталог и файл:

!685
sudo rm -rf vendor/ && sudo rm composer.lock

И пробуем собрать проект, передав --user phpcomposeruser:

docker run --user phpcomposeruser -ti --volume $(pwd)/:/app php-composer:3.3 composer install
docker: Error response from daemon: linux spec user: unable to find user phpcomposeruser: no matching entries in passwd file.

Снова “упс”? 🙂

Логично – мы ведь не создавали пользователя phpcomposeruser в образе php-composer:3.3:

docker run -ti php-composer:3.3 cut -d: -f1 /etc/passwd
root
daemon
bin
sys
sync
games
man
lp
mail
news
uucp
proxy
www-data
backup
list
irc
gnats
nobody
_apt

Поэтому как вариант решения – мы можем передать в контейнер UID и GID вместо имени пользователя – используем id -u (print only the effective user ID) и -g (print only the effective group ID).

Меняем владельца composer.json на phpcomposeruser, который будет владельцем проекта:

sudo chown phpcomposeruser composer.json

Собираем проект:

docker run --user $(id -u phpcomposeruser):$(id -g phpcomposeruser) -ti --volume $(pwd)/:/app php-composer:3.3 composer install
Cannot create cache directory /.composer/cache/repo/https---packagist.org/, or directory is not writable. Proceeding without cache
Cannot create cache directory /.composer/cache/files/, or directory is not writable. Proceeding without cache
Loading composer repositories with package information
Cannot create cache directory /.composer/cache/repo/github.com/antimattr/GoogleBundle/, or directory is not writable. Proceeding without cache
Updating dependencies (including require-dev)
Package operations: 4 installs, 0 updates, 0 removals
Failed to download composer/ca-bundle from dist: The zip extension and unzip command are both missing, skipping.
A php.ini file does not exist. You will have to create one.
Now trying to download from source
- Installing composer/ca-bundle (1.1.0): Cloning 943b2c4fca
Failed to download maxmind/web-service-common from dist: The zip extension and unzip command are both missing, skipping.
A php.ini file does not exist. You will have to create one.
Now trying to download from source
- Installing maxmind/web-service-common (v0.5.0): Cloning 61a9836fa3
Failed to download maxmind-db/reader from dist: The zip extension and unzip command are both missing, skipping.
A php.ini file does not exist. You will have to create one.
Now trying to download from source
- Installing maxmind-db/reader (v1.3.0): Cloning e042b4f8a2
Failed to download geoip2/geoip2 from dist: The zip extension and unzip command are both missing, skipping.
A php.ini file does not exist. You will have to create one.
Now trying to download from source
- Installing geoip2/geoip2 (v2.8.0): Cloning 63b0d87d47
maxmind-db/reader suggests installing ext-bcmath (bcmath or gmp is required for decoding larger integers with the pure PHP decoder)
maxmind-db/reader suggests installing ext-gmp (bcmath or gmp is required for decoding larger integers with the pure PHP decoder)
maxmind-db/reader suggests installing ext-maxminddb (A C-based database decoder that provides significantly faster lookups)
Writing lock file
Generating autoload files

Проверяем:

ls -l
total 24
-rw-r--r-- 1 phpcomposeruser setevoy          142 Mar 17 14:54 composer.json
-rw-r--r-- 1 phpcomposeruser phpcomposeruser 8286 Mar 17 14:59 composer.lock
-rw-r--r-- 1 setevoy         setevoy          155 Mar 17 14:44 Dockerfile
drwxr-xr-x 6 phpcomposeruser phpcomposeruser 4096 Mar 17 14:59 vendor

Готово.