Instalando o Laravel com Docker – sem Laravel Sail

Tempo de leitura 9 minutos

Se eu aprendi uma coisa durante os últimos anos é que a tecnologia nunca para de nos surpreender, tanto de forma positiva quanto negativa e o Docker foi uma das melhores coisas no meu dia a dia.

Não que o Docker seja a solução final para o meu ambiente dev ou de produção (vou falar sobre ambientes mais pra frente), mas eu realmente acho MUITO produtivo.

Quer saber quando novos artigos forem publicados? Assine a newsletter!

O que é o Docker?

Longe de querer dedicar este artigo todo para explicar Docker do zero, já que o foco aqui e mostrar como subir um app Laravel com Docker, eu me sinto na responsabilidade de, pelo menos, dar uma introdução.

O Docker é uma ferramenta de virtualização (sim, ele é uma ferramenta de virtualização, pode não ser tradicional, mas por favor parem de dizer que ele não é) com foco em sistemas isolados (containers) que se comunicam para formar algo maior (Transformer – ironia).

“Ain, mimimi… Docker não é uma tool de virtualização… mais mimimi”.

Erik Figueiredo

Cada container no Docker executa uma tarefa isolada (rodar o PHP, servidor web, banco de dados…) que se integram e são fáceis de serem administrados separadamente.

E o Laravel com isso?

Uma das primeiras coisas que eu fiz quando descobri o Docker foi ler a documentação em busca de informações para subir o Laravel com tudo o que eu tinha direito e o que eu fiz foi beeeem…. bizarro.

Isso porque eu usava o Vagrant antes do Docker (e ainda uso) e acostumado com ele eu simplesmente criei TUDO dentro de um container só e ficou bem bacana aos olhos do Erik do passado, mas o Erik do presente tem até um pouco de vergonha disso (só um pouco, afinal de contas estou aqui contando pra você).

Isso porque subir um ambiente web completo pode acabar sendo complexo e a vantagem do Docker é você editar ou ate substituir partes da infra sem mexer no restante. Isso é mais complicado com um ambiente “tradicional”.

Uma coisa muito legal é que, uma vez, eu decidi testar o que era mais rápido, Nginx, Apache ou um mix dos dois e para subistuir um web host pelo outro é bem mais prático quando estão isolados em um container.

Talves eu esteja indo rápido de mais, vamos ver algum exemplo, mas antes um aviso:

A primeira coisa que você vai precisar é instalar o Docker, eu não ou abordar isso agora, talvez em um artigo futuro, mas isso é MUITO bem documentado web a fora, tente pesquiar “instalar o Docker no {seu sistema operacional e versão} no Google para você ver.

Docker Compose

Como eu não tenho como mensurar o nível de conhecimento de quem chega neste artigo eu vou partir do ponto em que eu estava quando comecei a aprender (mas dessa vez do jeito certo).

Pra mim foi muito mais simples entender como o Docker funciona quando eu vi os parâmetros organizados e endentados em um arquivo yml.

Não me leve a mal, eu sei como usar linha de comando do Windows e terminal no Linux, que é como o Docker é gerido em “sua forma mais pura”, mas do meu ponto de vista entender como os containers funcionam e se comunicam na linha de comando é mais complicado pra mim, simple assim.

O Docker Compose ajuda a definir containers e como eles se comunicam, ele ainda não é o Docker, mas uma forma de organizar sua execução de forma mais simplificada, veja do que eu estou falando:

version: '3.7'
services:

  app:
    build:
      context: .
      dockerfile: Dockerfile.local
    container_name: my-app
    restart: unless-stopped
    tty: true
    working_dir: /var/www/html
    volumes:
      - ./app:/var/www/html
    networks:
      - local-database
    ports:
      - "80:80"
      - "443:443"
      - "6001:6001"

  database:
    image: bitnami/mariadb:latest
    container_name: database
    restart: unless-stopped
    expose:
      - 3306
    ports:
      - "3306:3306"
    volumes:
      - ./.docker/database/data:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=mydb
      - MYSQL_USER=root
      - MYSQL_PASSWORD=root
      - MARIADB_ROOT_PASSWORD=root
    networks:
      - network

networks:
  network:
    name: network
    driver: bridge

Um arquivo yml é definido de acordo com os níveis de identação, de forma que os itens a baixo e com identação maior são filhos do anterior.

Com isso em mente notamos que temos uma árvore em que os itens version, services e networks estão na raiz, com app e database sendo filhos do services e network de networks.

O item version indica a versão da API do Docker Compose que vamos usar, o services são “as features” (PHP, Noe, Apache, Nginx, MySql, Postgres e etc) que queremos e o networks será como eles vão se comunicar.

Vamos focar no services.

Service APP para o Laravel

O Service app e o que vai trazer nosso aplicativo Laravel, nele eu vou configurar o PHP, suas extensões e tudo relativo ao PHP e configuração do Laravel.

Aqui o que cada parâmetro significa:

  • build: O arquivo do Docker a ser executado (vou falar dele a seguir)
  • container_name: Depois do processo de build (construção) do arquivo Docker, ele se torna um container, aqui eu dou um nome a ele para ficar mais fácil as tarefas no futuro.
  • restart: Quando o container será reiniciado, eu disse que é para ser reiniciado SEMPRE que rolar um erro crítico (quando acontece um erro o container é encerrado, aqui eu garanto que ele volte a ser executado), a menos que eu pare manualmente.
  • tty: Se vamos usar de forma interativa (tem haver com a forma como as entradas e saídas do terminal vão se comportar, material para outra hora).
  • working_dir: O diretório DENTRO do container que será o ponto de partida do container, todo os comandos executados pelo Docker tomam com base que estamos NESTE diretório.
  • volumes: Aqui eu mapeio um diretório local (do meu computador) com o do container.
  • networks: As redes, para se comunicar com outros containers ou até mesmo com minha máquina.
  • ports: As portas que estarão disponíveis no host, já que o Docker bloqueia acesso completo a tudo o que não for especificado aqui, com isso poderemos acessar o projeto Laravel no navegador através das portas 80 e 443, por exemplo.

O grande segredo deste serviço (e dos outros) é o Dockerfile (o arquivo do docker que irá gerar o container) que será executado, vou deixar ele por último.

Service de banco de dados

Eu não vou repetir as explicações de itens que comentei antes, para economizar algum tempo.

Outro ponto importante é que, embora eu esteja propondo esta estrutura com o database junto com o service do projeto, eu normalmente utilizo separado, vou falar sobre esta estratégia em outro artigo (futuro).

  • image: Em vez do parâmetro build, eu indiquei um image, é basicamente a mesma coisa, só que aqui ele busca um “Dockerfile” jé criado e pronto pra usar que foi disponibilizado no Docker Hub.
  • expose: Diferente do ports (que disponibiliza as portas apenas para os containers na mesma rede/lincados e a maquina host) o expose expoe a porta para a maquina host (ou seja, o meu computador), afinal de contas, eu quero acessar o banco de dados também, né.
  • volumes: Quando um container é finalizado ele deixa de existir com TODOS os arquivos dentro dele, inclusive o banco de dados (format c:), então defini um volume para ele salvar o banco na maquina host.
  • environment: Variáveis de ambiente que a imagem (Dockerfile) irá usar
    MYSQL_DATABASE: Nome do banco de dados
    MYSQL_USER: Usuário de acesso ao banco de dados
    MYSQL_PASSWORD: Senha de acesso ao banco de dados
    MARIADB_ROOT_PASSWORD: Usuário root do banco de dados

Agora temos 2 containers conectados na mesma rede e que podem ser ligados ou desligados com um único comando, ou até deixar lá ligados pra sempre (mesmo que reinicie o computador).

Configurando o Laravel no Docker

Para fazer o Laravel funcionar corretamente, vamos precisar de 3 arquivos, o Dockerfile, o Entrypoint e um arquivo de configuração para o Supervisor.

Crie os arquivos a partir da raiz:

  • Dockerfile.local
  • app [aqui ficam os arquivos do Laravel]
  • .docker [diretório]
    • supervisor/conf.d [diretórios]
      • laravel-worker.conf
    • entrypoint

Dockerfile.local

Se você ja trabalhou com Dockerfile antes pode ter estranhado o sufixo .local no nome do arquivo, isso porque este arquivo vai ser responsável pelo ambiente local de dev, poderemos ter um .prod no futuro, acho que fica mais claro pra mim a nomenclatura do arquivo desta forma.

# pego uma imagem já pronta com o PHP e Apache
FROM php:7.4-apache

# variáveis de ambiente do Apache
ENV APACHE_DOCUMENT_ROOT /var/www/html/public
ENV APACHE_LOG_DIR /var/log/apache2

# instala dependências de extensões, vim e supervisor
RUN apt-get update && apt-get install git libzip-dev vim supervisor -y

# instala as extensões
RUN docker-php-ext-install zip mysqli pdo pdo_mysql

# instala e configura o XDebug
RUN pecl install xdebug
RUN docker-php-ext-enable xdebug
RUN echo "xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
RUN echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
RUN echo "xdebug.client_port=9003 >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
RUN echo "xdebug.discover_client_host=1" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
RUN echo "xdebug.idekey=docker" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
EXPOSE 9003

# ativa o mod_rewrite do Apache
RUN a2enmod rewrite

# cria o diretório de logs para o Supervisor
RUN mkdir /var/log/webhook

# copia os arquivos de configuração do Supervisor
COPY ./.docker/supervisor/conf.d /etc/supervisor/conf.d

# copia o entreypoint
COPY ./.docker/entrypoint /var/www/entrypoint

# adiciona permissão de execução para o Entrypoint
RUN chmod +x /var/www/entrypoint

# instala o Composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
RUN php -r "if (hash_file('sha384', 'composer-setup.php') === '756890a4488ce9024fc62c56153228907f1545c228516cbf63f885e036d37e9a59d27d63f46af1d4d07ee0f76181c7d3') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
RUN php composer-setup.php --install-dir=/usr/bin --filename=composer
RUN php -r "unlink('composer-setup.php');"

# seta o document root configurado anteriormente nas variáveis de ambiente
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf

# copia o projeto para o Docker
COPY ./app /var/www/html

# informa o diretório raiz do Docker
WORKDIR /var/www/html

# informa o Entrypoint
ENTRYPOINT [ "/var/www/entrypoint" ]

Tem muita coisa acontecendo neste arquivo, ele é o CORE do meu ambiente Docker.

A primeira coisa que eu faço é importar o PHP 7.4 e informar algumas variáveis de ambiente para o Apache, estas variáveis estão informadas na documentação da imagem no Docker Hub, mais pra frente eu passo elas para o Apache e ativo no Entrypoint.

Na sequência eu instalei algumas dependências de extensões e o Vim (uso para editar/visualizar arquivos dentro do Docker) e o Supervisor que roda processos do Laravel, como filas, crons e websockets.

Vamos precisar de algumas extensões PHP, eu fui minimalista e inclui apenas a zip (para o Composer) e de banco de dados.

Outra extensão importante pra mim é o Xdebug, aqui eu estou adicionando a versão 3 e deixando pronto para integrar com sua IDE/Editor de texto, espero falar mais sobre o Xdebug em outro artigo.

Seguindo adiante eu copio o arquivo de configuração do Supervisor e o nosso Entrypoint (vou explicar mais pra frente o que este arquivo faz).

Instalação do Composer (vou usar no Entrypoint).

Configuro o diretório que o servidor web vai apontar (o public do Laravel) que foi setado anteriormente nas variáveis de ambiente (no começo do arquivo).

Copiar o projeto para o Docker é opcional, eu copio porque SEMPRE preciso executar alguma tarefa e ja deixo preparado, acontece que neste momento em que o Dockerfile está sendo processado os arquivos do projeto NÃO estão disponíveis ainda, então pode ser necessário copiá-los.

Informar o workdir é apenas um facilitador pra mim, para quando eu abrir o container ele já apontar para o diretório do projeto, mas também pode servir para você não precisar digitar o caminho completo até o diretório do projeto quando for rodar algum arquivo (como o artisan).

E por fim eu informo o Entrypoint, que vou explicar mais a baixo.

Supervisor

Existem algumas features do Laravel que precisam ficar rodando em segundo plano (as filas, por exemplo), eu uso o Supervisor para garantir que isso está acontecendo.

Adicione um arquivo em .docker/supervisor/conf.d/laravel-worker.conf com o seguinte conteúdo:

[supervisord]

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work
autostart=true
autorestart=true
user=root
numprocs=4
redirect_stderr=true
stderr_logfile=/var/log/webhook/laravel-worker.err.log
stdout_logfile=/var/log/webhook/laravel-worker.out.log
stopwaitsecs=3600

Bem simples.

Se algum erro ocorrer você pode conferir os logs dentro do container nos diretórios que informei em stderr_logfile e stdout_logfile.

Também é possível configurar a quantidade de filas que você quer em numprocs.

Recomendo pesquisar por Supervisor e entender cada um dos parâmetros enquanto eu não posto um artigo sobre isso aqui no Blog.

Entrypoint

O último arquivo é o entrypoint do Docker (ou ponto de entrada) é o arquivo que é executado após o build do container, eu vou usá-lo para configurar/executar algumas tarefas do Laravel.

Um detalhe importante é que o Entrypoint roda DEPOIS do build, ou seja, por alguns segundos (ou minutos) o projeto vai continuar não abrindo no navegador, isso é normal, só precisa do tempo até executar tudo.

Crie um arquivo em .docker/entrypoint como seguinte conteúdo.

#!/bin/sh

chown -R www-data:www-data /var/www/html

composer install

/usr/bin/supervisord -n > /dev/null 2>&1 &
/usr/sbin/apache2ctl -D FOREGROUND

A primeira linha (#!/bin/sh) informa que este é um script para o bash do Linux.

A segunda linha da permissão para usuário e grupo www-data acessarem os arquivos do Laravel e evita que o erro 403 do Apache apareça no navegador.

O composer install é para a primeira execução do Laravel, ele vai instalar as dependências do Framework.

Por fim eu executo o Supervisor com o worker que configurei nos passos anteriores (note que eu adicionei um -n > /dev/null 2>&1 & após o comando do supervisor, é para ele ser executado em segundo plano, caso contrário o próximo comando NUNCA seria executado), e dou start no Apache com o novo document root já configurado.

Testando

Após todos os arquivos serem criados, vamos precisar executar o Docker Compose para subir tudo, é bem simples, na raiz do projeto, junto ao docker-compose.yml, execute:

docker-compose up -d

A primeira vez que você executar este comando o Docker vai baixar a imagem do PHP e executar todos os passos que definimos no nosso Dockerfile, isso demora um pouco, mas das próximas vezes será quase instantâneo.

Outro ponto legal é que você pode desligar o computador e religar que o Docker continuará com estes serviços ativos, você não precisa rodar o comando toda hora.

Assim que terminar de configurar tudo, abra o navegador e rode http://localhost e veja se a cara do Laravel aparece, como dito no artigo, pode ser necessário aguardar o entrypoint setar as permissões e instalar as dependências do Composer, isso deve demorar alguns poucos minutos, mas se você identificar que não deu certo tente comparar com os arquivos de exemplo que eu vou deixar ou perguntar nos comentários.

Última dica desta sessão é, se você não quiser rodar o Docker Compose em segundo plano, apenas omita o -d.

docker-compose up

Outros comandos legais

Aqui alguns comandos úteis, rode no mesmo diretório do docker-compose.yml.

Para parar os serviços que criamos no Docker:

docker-compose stop

Para parar e remover os serviços do Docker que criamos:

docker-compose down

Para listar TODOS os serviços ativos do Docker:

docker ps

Para acessar o Container que criamos:

docker exec -it my-app /bin/bash

Conclusão

É isso, depois de você escrever os arquivos de configuração, adicionar isso a um novo projeto é bem simples e rápido, você vai conseguir subir um servidor completo dentro de poucos minutos e sem precisar configurar nada novamente.

Prático de mais.

E se você quiser ser avisado sobre novos artigos, assine a newsletter do blog.

Autor: Erik Figueiredo

Músico, gamer amador, tutor de programação, desenvolvedor freelancer full cycle, com foco em PHP (Laravel e CakePHP), Javascript (Front e Node.js), Dart (Front e Flutter) e infra.

2 comentários em “Instalando o Laravel com Docker – sem Laravel Sail”

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *