
Neste artigo, falarei sobre minha experiência de "agrupar" um aplicativo Laravel em um contêiner Docker para que os desenvolvedores de front-end e back-end possam trabalhar localmente com ele, e iniciá-lo na produção foi o mais simples possível. Além disso, o CI executará automaticamente analisadores de código estático, testes de phpunit
e criará imagens.
"E o que, de fato, é complexidade?" - você pode dizer e estará parcialmente certo. O fato é que muitas discussões nas comunidades de língua russa e de língua inglesa são dedicadas a esse tópico, e eu dividiria condicionalmente quase todos os tópicos estudados nas seguintes categorias:
- "Estou usando o docker para desenvolvimento local. Coloquei o laradock e não conheço os problemas". Legal, mas e o lançamento da automação e produção?
- "Eu coleciono um container (monólito) baseado no
fedora:latest
(~ 230 Mb), coloco todos os serviços (nginx, db, cache, etc) nele, executo tudo dentro do supervisor." Também excelente, fácil de iniciar, mas e a ideologia de "um contêiner - um processo"? E quanto ao balanceamento e gerenciamento de processos? Qual é o tamanho da imagem? - "Aqui estão algumas configurações, tempere com trechos de sh-scripts, adicione env-values mágicos, use-os." Obrigado, mas e quanto a pelo menos um exemplo vivo que eu poderia usar e tocar totalmente?
Tudo o que você lê abaixo é uma experiência subjetiva que não finge ser a verdade suprema. Se você tiver adições ou indicações de imprecisões - bem-vindo aos comentários.
Para os impacientes - um link para o repositório , clone o qual você pode iniciar o aplicativo Laravel com um comando. Também não é difícil executá-lo no mesmo fazendeiro , "vinculando" corretamente os contêineres ou usar a versão do supermercado docker-compose.yml
como ponto de partida.
Parte teórica
Quais ferramentas usaremos em nosso trabalho e em que focaremos? Primeiro de tudo, precisamos instalar no host:
docker
- no momento da redação, usei a versão 18.06.1-ce
docker-compose
- lida com a conexão de contêineres e o armazenamento dos valores ambientais necessários; versão 1.22.0
make
- você pode se surpreender, mas se encaixa perfeitamente no contexto de trabalhar com o docker
Você pode curl -fsSL get.docker.com | sudo sh
docker
em sistemas do tipo debian
com o comando curl -fsSL get.docker.com | sudo sh
curl -fsSL get.docker.com | sudo sh
, mas o docker-compose
melhor para instalar usando o pip
, pois as versões mais recentes estão em seus repositórios (o apt
muito atrasado, em regra).
Isso completa a lista de dependências. O que você usará para trabalhar com o código-fonte - phpstorm
, netbeans
ou dead vim
- depende de você.
A seguir, um controle de qualidade improvisado no contexto do design da imagem (não tenho medo dessa palavra) :
P: Imagem básica - qual é a melhor escolha?
A: O que é "mais fino", sem frescuras. Com base no alpine
(~ 5 Mb), você pode coletar o que seu coração desejar, mas provavelmente terá que jogar com a montagem de serviços da fonte. Como alternativa - jessie-slim
(~ 30 Mb) . Ou use o que é mais usado em seus projetos.
P: Por que o peso da imagem é importante?
R: Diminuição do volume de tráfego, diminuição da probabilidade de erro ao baixar (menos dados - menos probabilidade), diminuição no local consumido. A regra "A gravidade é confiável" (© "Snatch") não funciona muito bem aqui.
P: Mas meu amigo %friend_name%
diz que uma imagem "monolítica" com todas as dependências é a melhor maneira.
A: Vamos apenas contar. O aplicativo possui 3 dependências - PG, Redis, PHP. E você queria testar como ele se comportaria em pacotes de versões diferentes dessas dependências. PG - versões 9.6 e 10, Redis - 3.2 e 4.0, PHP - 7.0 e 7.2. Caso cada dependência seja uma imagem separada - você precisa de 6 delas, que nem precisa coletar - tudo está pronto e fica no hub.docker.com
. Se, por razões ideológicas, todas as dependências forem "empacotadas" em um contêiner, você precisará montá-lo com canetas ... 8 vezes? Agora adicione a condição que você ainda deseja jogar com o opcache
. No caso de decomposição, isso é simplesmente uma alteração nas tags das imagens usadas. Um monólito é mais fácil de executar e manter, mas é o caminho para lugar nenhum.
P: Por que o supervisor no contêiner é mau?
A: Porque o PID 1
. Se você não deseja muitos problemas com os processos zumbis e tem a capacidade de "adicionar capacidade" de forma flexível, quando necessário - tente executar um processo por contêiner. Uma exceção peculiar é o nginx
com seus trabalhadores e o php-fpm
, que têm a capacidade de produzir processos, mas precisam tolerar isso (além disso, eles não são ruins em reagir ao SIGTERM
, "matando" corretamente seus trabalhadores). Ao lançar todos os demônios como supervisor, você quase certamente está se condenando a problemas. Embora, em alguns casos, seja difícil ficar sem isso, mas essas já são exceções.
Tendo decidido sobre as principais abordagens, vamos para o nosso aplicativo. Deve ser capaz de:
web|api
- dê estática com nginx
e gere conteúdo dinâmico com fpm
scheduler
- execute o agendador de tarefas nativoqueue
- processa trabalhos de filas
Um conjunto básico que pode ser expandido, se necessário. Agora, vamos às imagens que precisamos coletar para que nosso aplicativo "decole" (seus nomes de código são dados entre colchetes):
PHP + PHP-FPM
( app ) - o ambiente em que nosso código será executado. Como as versões do PHP e do FPM serão as mesmas para nós - nós as coletamos em uma imagem. Portanto, é mais fácil gerenciar com as configurações e a composição dos pacotes será idêntica. Obviamente - os processos de aplicação e FPM serão executados em diferentes contêineresnginx
( nginx ) - que não se incomodaria com a entrega de configurações e módulos opcionais para o nginx
- coletaremos uma imagem separada com ele. Por ser um serviço separado, ele possui seu próprio arquivo docker e seu contexto- Fontes do aplicativo ( fontes ) - a fonte será entregue usando uma imagem separada, montando o
volume
com elas em um contêiner com o aplicativo. A imagem base é alpine
; no interior, existem apenas fontes com dependências instaladas e coletadas usando os recursos do webpack (criar artefatos)
Outros serviços de desenvolvimento são lançados em contêineres, retirando-os do hub.docker.com
; na produção, por outro lado - eles estão sendo executados em servidores separados, agrupados em cluster. Tudo o que resta para nós é dizer ao aplicativo (através do ambiente) em quais endereços / portas e com quais detalhes é necessário bater neles. Ainda mais legal é usar a descoberta de serviços para esse fim, mas não nesse momento.
Tendo decidido a parte teórica, proponho passar para a próxima parte.
A parte prática
Sugiro organizar arquivos no repositório da seguinte maneira:
. ├── docker # - │ ├── app │ │ ├── Dockerfile │ │ └── ... │ ├── nginx │ │ ├── Dockerfile │ │ └── ... │ └── sources │ ├── Dockerfile │ └── ... ├── src # │ ├── app │ ├── bootstrap │ ├── config │ ├── artisan │ └── ... ├── docker-compose.yml # Compose- ├── Makefile ├── CHANGELOG.md └── README.md
Você pode se familiarizar com a estrutura e os arquivos clicando neste link .
Para criar um serviço, você pode usar o comando:
$ docker build \ --tag %local_image_name% \ -f ./docker/%service_directory%/Dockerfile ./docker/%service_directory%
A única diferença será a montagem da imagem com as fontes - para isso, o contexto da montagem (argumento extremo) ./src
ser definido como ./src
.
As regras para nomear imagens no registro local recomendam o uso das que o docker-compose
usa por padrão, a saber: %root_directory_name%_%service_name%
. Se o diretório do projeto for chamado my-awesome-project
e o serviço for chamado redis
, o nome da imagem (local) será melhor para escolher my-awesome-project_redis
respectivamente.
Para acelerar o processo de construção, você pode dizer ao docker para usar o cache da imagem montada anteriormente e, para isso, a --cache-from %full_registry_name%
inicialização --cache-from %full_registry_name%
. Portanto, o daemon do docker procurará antes de iniciar uma instrução específica no Dockerfile - ele mudou? E se não (o hash converge) - ele pulará a instrução, usando a camada já preparada da imagem, que será solicitada como cache. Como isso não é ruim, ele reconstruirá o processo, especialmente se nada mudou :)
Preste atenção aos scripts ENTRYPOINT
para iniciar contêineres de aplicativos.
A imagem do ambiente para iniciar o aplicativo (aplicativo) foi coletada levando em consideração o fato de que ele funcionará não apenas na produção, mas também localmente, os desenvolvedores precisam interagir efetivamente com ele. A instalação e remoção de dependências do composer
, execução de testes de unit
, registros de tail
e uso de aliases familiares ( php /app/artisan
→ art
, composer
→ c
) não devem causar nenhum desconforto. Além disso - também será usado para executar testes de unit
e analisadores de código estático ( phpstan
no nosso caso) no CI. É por isso que o Dockerfile, por exemplo, contém a xdebug
instalação do xdebug
, mas o próprio módulo não está ativado (ele é ativado apenas usando o CI).
Também para o composer
o pacote hirak/prestissimo
é hirak/prestissimo
, o que aumenta muito a instalação de todas as dependências.
Na produção, montamos o conteúdo do diretório /src
partir da imagem com as fontes (fontes) dentro dele no diretório /app
. Para desenvolvimento, “rolamos” o diretório local com fontes de aplicativo ( -v "$(pwd)/src:/app:rw"
).
E aqui está uma complexidade - esses são os direitos de acesso aos arquivos criados a partir do contêiner. O fato é que, por padrão, os processos em execução no contêiner iniciam a partir da raiz ( root:root
), os arquivos criados por esses processos (cache, logs, sessões etc.) - também e, como resultado - você não possui nada "localmente" com eles você pode fazer isso sem executar o sudo chown -R $(id -u):$(id -g) /path/to/sources
.
Como uma solução, use fixuid , mas essa solução é direta. Pareceu-me a melhor maneira de USER_ID
local e seu GROUP_ID
dentro do contêiner e iniciar processos com esses valores . Por padrão, a substituição dos valores 1000:1000
(os valores padrão do primeiro usuário local) eliminou a chamada $(id -u):$(id -g)
e, se necessário, você sempre pode substituí-los ( $ USER_ID=666 docker-compose up -d
) ou coloque o arquivo de composição de encaixe no arquivo .env
.
Além disso, quando o php-fpm
iniciado localmente php-fpm
não se esqueça de desativar o opcache
- caso contrário, haverá muito "sim, que diabos!" você será fornecido.
Para uma conexão "direta" com o redis e o postgres, joguei portas adicionais "fora" ( 15432
e 15432
respectivamente), para que não haja problemas em "conectar e ver o que e como realmente é" em princípio.
Eu mantenho o contêiner com o app
codinome em execução ( --command keep-alive.sh
) com a finalidade de acesso conveniente ao aplicativo.
Aqui estão alguns exemplos de solução de problemas diários com o docker-compose
:
Operação | Comando em execução |
---|
Instale o pacote do composer | $ docker-compose exec app composer require package/name |
Executando phpunit | $ docker-compose exec app php ./vendor/bin/phpunit --no-coverage |
Instale todas as dependências do nó | $ docker-compose run --rm node npm install |
Instalar pacote de nós | $ docker-compose run --rm node npm i package_name |
Iniciando uma reconstrução ao vivo de ativos | $ docker-compose run --rm node npm run watch |
Você pode encontrar todos os detalhes de inicialização no arquivo docker-compose.yml .
Choi make
vivo!
Digitar os mesmos comandos todas as vezes se torna entediante após a segunda vez, e como os programadores são criaturas preguiçosas por natureza, vamos entrar em sua "automação". Manter um conjunto de scripts sh
é uma opção, mas não tão atraente quanto um único Makefile
, especialmente porque sua aplicabilidade no desenvolvimento moderno é muito subestimada.
O manual completo em russo pode ser encontrado neste link .
Vamos ver como a make
run fica na raiz do repositório:
[user@host ~/projects/app] $ make help Show this help app-pull Application - pull latest Docker image (from remote registry) app Application - build Docker image locally app-push Application - tag and push Docker image into remote registry sources-pull Sources - pull latest Docker image (from remote registry) sources Sources - build Docker image locally sources-push Sources - tag and push Docker image into remote registry nginx-pull Nginx - pull latest Docker image (from remote registry) nginx Nginx - build Docker image locally nginx-push Nginx - tag and push Docker image into remote registry pull Pull all Docker images (from remote registry) build Build all Docker images push Tag and push all Docker images into remote registry login Log in to a remote Docker registry clean Remove images from local registry --------------- --------------- up Start all containers (in background) for development down Stop all started for development containers restart Restart all started for development containers shell Start shell into application container install Install application dependencies into application container watch Start watching assets for changes (node) init Make full application initialization (install, seed, build assets) test Execute application tests Allowed for overriding next properties: PULL_TAG - Tag for pulling images before building own ('latest' by default) PUBLISH_TAGS - Tags list for building and pushing into remote registry (delimiter - single space, 'latest' by default) Usage example: make PULL_TAG='v1.2.3' PUBLISH_TAGS='latest v1.2.3 test-tag' app-push
Ele é muito bom em objetivos viciantes. Por exemplo, para iniciar o watch
( docker-compose run --rm node npm run watch
), você precisa que o aplicativo seja "aumentado" - você só precisa especificar o destino docker-compose run --rm node npm run watch
como dependente - e não precisa se preocupar em esquecer de fazer isso antes de chamar o watch
- make
si mesmo fará tudo por você. O mesmo se aplica à execução de testes e analisadores estáticos, por exemplo, antes de realizar alterações - make test
um make test
e toda a mágica acontecerá com você!
Escusado será dizer que você não precisa se preocupar em montar imagens, fazer download delas, especificar --cache-from
e quase tudo?
Você pode ver o conteúdo do Makefile
neste link .
Peça de automóvel
Vamos para a parte final deste artigo - essa é a automação do processo de atualização de imagens no Docker Registry. Embora no meu exemplo o GitLab CI seja usado - para transferir a idéia para outro serviço de integração, acho que será bem possível.
Primeiro, determinaremos o nome das tags de imagem usadas:
Nome da tag | Destino |
---|
latest | Imagens coletadas do ramo master . O estado do código é o mais recente, mas ainda não está pronto para entrar no lançamento |
some-branch-name | Imagens coletadas no brunch some-branch-name . Assim, podemos "implantar" as alterações em qualquer ambiente que foram implementadas somente dentro da estrutura de um brunch específico, mesmo antes de mesclá-las com o master -light - basta "esticar" as imagens com essa tag. E - sim, as alterações podem estar relacionadas ao código e às imagens de todos os serviços em geral! |
vX.XX | Na verdade, o lançamento do aplicativo (use para implantar uma versão específica) |
stable | Alias, para a tag com a versão mais recente (use para implantar a versão estável mais recente) |
O lançamento ocorre publicando uma tag no vX.XX
formato vX.XX
Para acelerar a construção, o cache dos diretórios ./src/vendor
e ./src/node_modules
+ --cache-from
para docker build
e consiste nos seguintes estágios:
Nome do estágio | Destino |
---|
prepare | A fase preparatória - a montagem de imagens de todos os serviços, exceto a imagem com a fonte |
test | Testando o aplicativo (executando phpunit , analisadores de código estático) usando imagens coletadas no estágio de preparação |
build | Instalando todas as dependências do composer ( --no-dev ), montando assets webpack e webpack imagem com o código-fonte, incluindo artefatos recebidos ( vendor/* , app.js , app.css ) |

A montagem no ramo master
produzindo push
com as tags master
e latest
Em média, todas as etapas da montagem levam 4 minutos , o que é um resultado muito bom (a execução paralela de tarefas é tudo).
Você pode se familiarizar com o conteúdo da configuração ( .gitlab-ci.yml
) do coletor neste link .
Em vez de uma conclusão
Como você pode ver, organizar o trabalho com um aplicativo php (usando o Laravel
como exemplo) usando o Docker não é tão difícil. Como teste, você pode bifurcar o repositório e substituir todas as ocorrências de tarampampam/laravel-in-docker
por você - tente tudo "ao vivo" por conta própria.
Para inicialização local - execute apenas 2 comandos:
$ git clone https://gitlab.com/tarampampam/laravel-in-docker.git ./laravel-in-docker && cd $_ $ make init
Em seguida, abra http://127.0.0.1:9999
no seu navegador favorito.
... aproveitando a oportunidade
No momento, estou trabalhando no projeto TL "autocode" e estamos procurando talentosos desenvolvedores de php e administradores de sistema (o escritório de desenvolvimento está localizado em Yekaterinburg). Se você se considera o primeiro ou o segundo - escreva nossa carta de RH com o texto "Quero ser uma equipe de desenvolvimento, retome:% link_on_summary%" para o e-mail hr@avtocod.ru
, ajudamos na realocação.