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

Автор: | 02/04/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 выполняем:

[simterm]

$ 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

[/simterm]

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

[simterm]

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

[/simterm]

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

[simterm]

$ 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
...

[/simterm]

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

[simterm]

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

[/simterm]

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

[simterm]

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

[/simterm]

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

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

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

[simterm]

$ npm root
/home/setevoy/node_modules

[/simterm]

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

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

[simterm]

$ 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

[/simterm]

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

[simterm]

$ 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
...

[/simterm]

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

[simterm]

$ 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

[/simterm]

Проверяем:

[simterm]

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

[/simterm]

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.

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

[simterm]

$ 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

[/simterm]

Проверяем:

[simterm]

$ 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

[/simterm]

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

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

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

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

{
    "require": {
        "geoip2/geoip2": "~2.0"
    },  
    "repositories": [{
        "type": "vcs",
        "url": "[email protected]: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:

[simterm]

$ 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

[/simterm]

Проверяем:

[simterm]

$ 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

[/simterm]

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

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

[simterm]

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

[/simterm]

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

[simterm]

$ 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

[/simterm]

Отлично! Всё работает. Вот только 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

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

[simterm]

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

[/simterm]

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

[simterm]

$ 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

[/simterm]

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

Composer user

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

[simterm]

$ 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

[/simterm]

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

Собираем:

[simterm]

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

[/simterm]

А мы хотим, что бы каталог 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

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

[simterm]

$ 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

[/simterm]

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

[simterm]

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

[/simterm]

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

[simterm]

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

[/simterm]

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

[simterm]

$ 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

[/simterm]

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

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


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

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

[simterm]

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

[/simterm]

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

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

[simterm]

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

[/simterm]

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

[simterm]

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

[/simterm]

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:

[simterm]

$ docker build -t php-composer:3.3 .

[/simterm]

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

[simterm]

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

[/simterm]

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

[simterm]

$ 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.

[/simterm]

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

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

[simterm]

$ 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

[/simterm]

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

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

[simterm]

$ sudo chown phpcomposeruser composer.json

[/simterm]

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

[simterm]

$ 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

[/simterm]

Проверяем:

[simterm]

$ 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

[/simterm]

Готово.