Jak uruchomić aplikację w Laravelu wewnątrz kontenera Dockera

Problem wielu projektów na komputerze developera.

W większości projektów web-devowych programiści stawiają lokalną kopię aplikacji nad którą pracują, aby mieć możliwość szybkiego testowania zmian bez konieczności aktualizacji plików na serwerze. Często jednak okazuje się, że różne projekty wymagają różnych zależności a nawet różnych wersji tych samych bibliotek. Zmieniając środowisko pod kątem jednego projektu zmieniamy konfigurację dla wszystkich innych projektów, ciężko jest zatem utrzymać wszystkie projekty działające, co jest niezmiernie uciążliwe gdy często przełączamy się między projektami. W takiej konfiguracji nie mamy również możliwości stworzenia dla każdej aplikacji środowiska lokalnego odzwierciedlającego środowisko produkcyjne.

Z pomocą przychodzą nam narzędzia typu Docker, które umożliwiają konteneryzację, czyli zamknięcie wielu różnych środowisk w swoich kontenerach. Dzięki temu każda aplikacja działa niezależnie w swoim własnym środowisku.

Laravel i Docker

W artykule tym omówimy sposób tworzenia kontenerów dla aplikacji laravelowych. Każda aplikacja będzie miała przypisany host, a konfiguracja będzie na tyle elastyczna, że łatwo będzie ją można dostosować do różnych projektów.

Czego będziemy potrzebować?

Jeśli jeszcze nie posiadasz na swoim komputerze dockera, jest to dobry moment, żeby go zainstalować. Będziemy potrzebowali dwóch narzędzi: `docker` i `docker-compose`. 

Opis instalacji obu narzędzi w systemie Ubuntu 18.04 / 16.04 znajduje się na stronie https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=94798094

docker-compose.yml i .env

Naszą konfigurację zaczniemy od utworzenia nowego folderu o nazwie `laravel-docker` a w nim następujących plików:

docker-compose.yml

.env

Nasz plik docker-compose.yml będzie wyglądał następująco:

version: '3'
services:
    database:
        image: mysql:5.7
        container_name: ${APP_NAME}_mysql
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
            MYSQL_DATABASE: ${MYSQL_DATABASE}
            MYSQL_USER: ${MYSQL_USER}
            MYSQL_PASSWORD: ${MYSQL_PASSWORD}
        ports:
            - ${PORT_DATABASE}:3306
        volumes:
            - "./data/db/mysql:/var/lib/mysql"
            - "./etc/mysql:/etc/mysql/conf.d"
    mailcatcher:
        image: schickling/mailcatcher
        container_name: ${APP_NAME}_mailcatcher
        ports:
            - ${PORT_MAILCATCHER}:1080
    redis:
        container_name: ${APP_NAME}_redis
        image: redis
        ports:
          - "${PORT_REDIS}:6379"
        volumes:
          - "./data/redis:/data"
        restart: always
    myadmin:
        image: phpmyadmin/phpmyadmin
        container_name: ${APP_NAME}_phpmyadmin
        ports:
            - ${PORT_PHPMYADMIN}:80
        restart: always
        links:
            - database:db
        depends_on:
            - database
    php:
        build:
          context: ./etc/php
          args:
            - INSTALL_NODE=${INSTALL_NODE}
            - INSTALL_GULP=${INSTALL_GULP}
            - INSTALL_BOWER=${INSTALL_BOWER}
            - INSTALL_POSTGRESQL=${INSTALL_POSTGRESQL}
            - INSTALL_MYSQL=${INSTALL_MYSQL}
            - INSTALL_GD=${INSTALL_GD}
            - ADD_ALIASES=${ADD_ALIASES}
            - INSTALL_XDEBUG=${INSTALL_XDEBUG}
        container_name: ${APP_NAME}_php
        entrypoint: sh /bin/entrypoint.sh php-fpm
        links:
            - database:mysqldb
        restart: always
        volumes:
            - "./etc/php/php.ini:/usr/local/etc/php/conf.d/php.ini"
            - ${APP_PATH}:/var/www/html
            - './etc/log/nginx:/var/log/nginx'
            - ./etc/php/entrypoint.sh:/bin/entrypoint.sh
    web:
        build: ./etc/nginx
        container_name: ${APP_NAME}_nginx
        ports:
            - ${PORT_HTTP}:80
            - ${PORT_HTTPS}:443
        restart: always
        volumes:
            - "./etc/nginx/nginx.conf:/etc/nginx/nginx.conf"
            - "./etc/nginx/app.conf:/etc/nginx/sites-available/application.conf"
            - "./etc/nginx/app.conf:/etc/nginx/sites-enabled/application"
            - "./etc/ssl:/etc/ssl"
            - './etc/log/nginx:/var/log/nginx'
            - ${APP_PATH}:/var/www/html
        depends_on:
            - php
            - database

Jak łatwo zauważyć konfiguracja zawiera: 

  • bazę danych mysql
  • Mailcatcher – czyli narzędzie do przechwytywania maili wysyłanych z aplikcji
  • Redis
  • phpMyAdmin
  • Php
  • Nginx

 

W konfiguracji znajduje się sporo zmiennych oznaczonych ${NAZWA_ZMIENNEJ}. Wartości tych zmiennych ustawimy w pliku .env. Jego zawartość będzie następująca:

#!/usr/bin/env bash

MYSQL_DATABASE=my_database
MYSQL_ROOT_USER=root
MYSQL_ROOT_PASSWORD=root
MYSQL_USER=dev
MYSQL_PASSWORD=dev

# App settings
APP_NAME=MyApplication
APP_PATH=./project

# PHP Image settings
INSTALL_NODE=true
INSTALL_GULP=false
INSTALL_BOWER=false
INSTALL_POSTGRESQL=false
INSTALL_MYSQL=true
INSTALL_GD=false
ADD_ALIASES=true
INSTALL_XDEBUG=false

# Port Mappings
PORT_DATABASE=3300
PORT_MAILCATCHER=1080
PORT_PHPMYADMIN=8081
PORT_HTTP=8000

PORT_HTTPS=8100
PORT_REDIS=6379

Zalecamy dodanie pliku .env do .gitignore, a do repozytorium wrzucenie pliku .env.example z który nie zawiera żadnych wrażliwych informacji, np. Haseł.

Jak widać, konfiguracja będzie nam pozwalała na włączanie/wyłączanie różnych opcji z poziomu pliku .env, zatem jeśli nasz projekt będzie potrzebował na przykład bazy danych postgres zamiast mysql, możemy to łatwo zmienić przy pomocy opcji INSTALL_MYSQL oraz INSTALL_POSTGRESQL.

Plik .env umożliwia również konfigurację portów na których będą działały usługi, więc jeśli np. port bazy danych 3300 jest zajęty, możemy go tutaj zmienić na inny, który jest wolny.

Konfiguracje w katalogu ./etc/

Kolejnym krokiem będzie utworzenie konfiguracji poszczególnych kontenerów w podkatalogu ./etc/. Na początek wystarczą nam konfiguracje dla nginx i php.

Utwórzmy katalog ./etc/nginx a wewnątrz niego pliki: Dockerfile, nginx.conf i app.conf. 

Zawartość pliku ./etc/nginx/Dockerfile:

FROM debian:jessie

RUN printf "deb http://archive.debian.org/debian/ jessie main\ndeb-src http://archive.debian.org/debian/ jessie main\ndeb http://security.debian.org jessie/updates main\ndeb-src http://security.debian.org jessie/updates main" > /etc/apt/sources.list

RUN apt-get update && apt-get install -y \

    nginx

RUN rm /etc/nginx/sites-enabled/default

RUN echo "upstream php-upstream { server php:9000; }" > /etc/nginx/conf.d/upstream.conf

RUN usermod -u 1000 www-data

CMD ["nginx"]

EXPOSE 80

EXPOSE 443

Zawartość pliku ./etc/nginx/nginx.conf:

user www-data;
worker_processes 4;
pid /run/nginx.pid;

events {
  worker_connections  2048;
  multi_accept on;
  use epoll;
}

http {
  server_tokens off;
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 15;
  types_hash_max_size 2048;
  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  access_log off;
  error_log off;
  gzip on;
  gzip_disable "msie6";
  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-available/*;
  open_file_cache max=100;
  client_max_body_size 12M;
}

daemon off;

Zawartość pliku ./etc/nginx/app.conf:

server {
    server_name myapp.test;
    root /var/www/html/public;
    
    location / {
        try_files $uri /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass php-upstream;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

    error_log /var/log/nginx/laravel_error.log;
    access_log /var/log/nginx/laravel_access.log;
}

# server {
#     server_name myapp.test;
# 
#     listen 443 ssl;
#     fastcgi_param HTTPS on;
# 
#     ssl_certificate /etc/ssl/server.pem;
#     ssl_certificate_key /etc/ssl/server.key;
#     ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
# 
#     root /var/www/html/public;
# 
#     location / {
#         try_files $uri /index.php?$query_string;
#     }
# 
#     location ~ \.php$ {
#         fastcgi_split_path_info ^(.+\.php)(/.+)$;
#         fastcgi_pass php-upstream;
#         fastcgi_index index.php;
#         include fastcgi_params;
#         fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
#         fastcgi_param PATH_INFO $fastcgi_path_info;
#     }
# 
#     error_log /var/log/nginx/laravel_error.log;
#     access_log /var/log/nginx/laravel_access.log;
# }

 

Ostatni plik zawiera sporo zakomentowanych linii, będą one przydatne jeśli będziemy chcieli uruchomić SSL dla aplikacji. Opowiemy o tym w dalszej części artykułu.

Istotnym parametrem konfiguracji jest `server_name`, jest to adres na którym będziemy serwować lokalnie naszą aplikację (myapp.test). 

Uwaga: dla lokalnych nazw hosta należy użyć jednej z domen:

.test

.example

.invalid

.localhost

Inne domeny nie zadziałają w przeglądarce Google Chrome.

Utwórzmy teraz konfigurację dla php w katalogu ./etc/php. Będziemy tutaj mieć trzy pliki: Dockerfile, entrypoint.sh i php.ini

Zawartość pliku ./etc/php/entrypoint.sh:

#!/bin/sh

echo ' START PHP POST INSTALL SCRIPTS'
echo 'setting write access for www-data'
setfacl -dR -m u:www-data:rwX -m u:docker:rwX var
setfacl -R -m u:www-data:rwX -m u:docker:rwX var

echo ' END PHP POST INSTALL SCRIPTS'
docker-php-entrypoint $@

Zawartośc pliku ./etc/php/php.ini:

;PHP config

display_errors = On
display_startup_errors = On
error_reporting = E_ALL
memory_limit = 1024M
upload_max_filesize = 12M
post_max_size = 24M
date.timezone = Europe/Warsaw

Zawartośc pliku ./etc/php/Dockerfile:

FROM php:7.2-fpm

RUN apt-get update > /dev/null && apt-get install -y \
    git \
    unzip \
    libjpeg-dev \
    libxpm-dev \
    libwebp-dev \
    libfreetype6-dev \
    libjpeg62-turbo-dev \
    libmcrypt-dev \
    libpng-dev \
    zlib1g-dev \
    libicu-dev \
    jpegoptim \
    g++ \
    libxrender1 \
    libfontconfig \
    nano \
    cron

RUN docker-php-ext-install intl > /dev/null \
    && docker-php-ext-install zip > /dev/null \
    && docker-php-ext-install bcmath > /dev/null

RUN pecl install mcrypt-1.0.2\
    docker-php-ext-enable mcrypt

#--------------------------------------------------------------------------

# Optional Software's Installation

#--------------------------------------------------------------------------

ARG INSTALL_NODE=true

RUN if [ ${INSTALL_NODE} = true ]; then \
    # Install NodeJS using NVM
    curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.5/install.sh | bash > /dev/null && \
    export NVM_DIR="$HOME/.nvm" > /dev/null && \
    [ -s "$NVM_DIR/nvm.sh" ] > /dev/null && . "$NVM_DIR/nvm.sh" > /dev/null && \
    nvm install 11 && \
    nvm use node \
    nvm install node-sass; \
    npm rebuild node-sass \
;fi

ARG INSTALL_GULP=false
RUN if [ ${INSTALL_GULP} = true ]; then \
    # Install globaly gulp
    npm install -g gulp > /dev/null \
;fi

ARG INSTALL_BOWER=false

RUN if [ ${INSTALL_BOWER} = true ]; then \
    # Install globaly bower
    npm install -g bower > /dev/null \
;fi

# Install Composer

RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer > /dev/null

ARG INSTALL_MYSQL=false

RUN if [ ${INSTALL_MYSQL} = true ]; then \
    # Install MySQL PDO
    docker-php-ext-install pdo pdo_mysql > /dev/null \
;fi

ARG INSTALL_POSTGRESQL=false

RUN if [ ${ISNTALL_POSTGRESQL} = true ]; then \
    # Install PostgreSQL PDO
    docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql > /dev/null && \
    docker-php-ext-install pgsql pdo_pgsql > /dev/null \
;fi

ARG INSTALL_GD=false

RUN if [ ${INSTALL_GD} = true ]; then \
    # Install GD library
    docker-php-ext-configure gd \
        --with-freetype-dir=/usr/include/ \
        --with-jpeg-dir=/usr/include/ \
        --with-png-dir=/usr/include/ \
        --with-xpm-dir=/usr/include/ \
        --with-webp-dir=/usr/include/ > /dev/null && \
    docker-php-ext-install gd > /dev/null \
;fi

ARG INSTALL_XDEBUG=false

RUN if [ ${INSTALL_XDEBUG} = true ]; then \
    # Install XDebug extention PDO
    pecl install xdebug > /dev/null && \
    docker-php-ext-enable xdebug > /dev/null \
;fi

ARG ADD_ALIASES=false

RUN if [ ${ADD_ALIASES} = true ]; then \
    # Install GD library
    echo 'alias sf="php app/console"' >> ~/.bashrc  && \
    echo 'alias sf3="php bin/console"' >> ~/.bashrc && \
    echo 'alias lv="php artisan"' >> ~/.bashrc \
;fi

WORKDIR /var/www/html

Jak widać plik Dockerfile zawiera polecenia instalacji niezbędnego oprogramowania a także oprogramowania opcjonalnego, którego instalacja jest uzależniona od wartości parametrów w pliku .env, który utworzyliśmy wcześniej.

Na ten moment nasza struktura plików wygląda następująco:

laravel-docker/
├── docker-compose.yml
└── etc
    ├── nginx
    │   ├── app.conf
    │   ├── Dockerfile
    │   └── nginx.conf
    └── php
        ├── Dockerfile
        ├── entrypoint.sh
        └── php.ini

Podłączamy projekt w Laravelu

Ostatnim krokiem konfiguracji jest wskazanie, gdzie znajduje się nasz projekt. Robimy to poprzez dowiązanie symboliczne o nazwie `project` (nazwa dowiązania odpowiada wartości APP_PATH w .env).

ln -s ../my_great_project project

Powyższe polecenie tworzy dowiązanie do katalogu `my_great_project` znajdującego się w katalogu nadrzędnym, możemy również używać ścieżek bezwzględnych np.:

ln -s /home/user/projects/my_great_project project

Nasza konfiguracja ostatecznie wygląda tak:

laravel-docker/
├── docker-compose.yml
├── etc
│   ├── nginx
│   │ ├── app.conf
│   │ ├── Dockerfile
│   │ └── nginx.conf
│   └── php
│       ├── Dockerfile
│       ├── entrypoint.sh
│       └── php.ini
└── project -> /home/user/projects/my_great_project/

 

Katalog na który wskazuje dowiązanie `./project` może być katalogiem z istniejącym projektem w laravelu, może to być również nowy projekt utworzony przy pomocy polecenia `laravel new`. Ważne jest, żeby był to katalog główny projektu. Pliki w tym katalogu możesz edytować bezpośrednio z poziomu swojego IDE.

Budujemy kontenery

W tym miejscu możemy już wykonać polecenie budujące nasze kontenery:

docker-compose up –build

Jeśli nie wystąpiły żadne problemy w konfiguracji (np. błędna składnia) to rozpocznie się budowanie kontenerów. Proces ten wymaga pobrania sporej ilości danych z internetu oraz kompilacji potrzebnych narzędzi, dlatego może on potrwać dość długo, o postępach będziemy informowani na bieżąco w konsoli w której wykonaliśmy polecenie.

Ustawienie hosta

Jeśli kontenery zbudowały się poprawnie, jesteśmy o krok od zobaczenia naszej aplikacji w przeglądarce. Ostatnim krokiem jest przypisanie nazwy hosta w pliku /etc/hosts (Uwaga! Jest to plik w systemie komputera, nie w kontenerze dockera).

Przykład:

172.20.0.7 myapp.test

Nazwa hosta musi odpowiadać tej, którą ustawiliśmy w pliku ./etc/nginx/app.conf, natomiast adres IP to adres kontenera web, który możemy znaleźć przy pomocy polecenia `docker inspect <Id kontenera>`. Id kontenera znajdziemy uruchamiając polecenie `docker ps`. W naszym przypadku kontener ma nazwę `MyApplication_nginx`.

Dla ułatwienia znajdowania adersów IP kontenerów możemy użyć następującego skryptu o którym pisałem w artykule Docker – Adresy IP kontenerów.

Konfiguracja gotowa

Od tego momentu aplikacja będzie dostępna pod adresem `myapp.test`. 

Uwaga: w przypadku systemu MacOS dostęp przez nazwę hosta nie zadziała, należy wówczas wywoływać adres: `localhost:<PORT>` gdzie <PORT> to port HTTP ustawiony w pliku .env

Jeśli jeszcze nie utworzyłeś projektu w laravelu, możesz stworzyć plik ./project/public/index.php i w nim umieścić jakiś testowy ciąg znaków, np. “Testing docker”. Po uruchomieniu aplikacji w przeglądarce powinieneś ten ciąg zobaczyć. 

Jeśli natomiast podpiąłeś już projekt w laravelu, będziesz musiał dostosować jego konfigurację.

Konfiguracja Laravela

Należy ustawić adres hosta w pliku .env projektu laravelowego, w naszym przypadku będzie to APP_URL=http://myapp.test

Ustawiamy również bazę danych zgodnie z ustawieniami z dockera:

DB_CONNECTION=mysql
DB_HOST=MyApplication_mysql
DB_PORT=3306
DB_DATABASE=my_database
DB_USERNAME=root
DB_PASSWORD=root

Jeśli używamy redisa to również ustawiamy jego konfigurację:

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

Warto również ustawić w .env laravela konfigurację poczty tak, aby wskazywała na mailcatchera:

MAIL_DRIVER=smtp
MAIL_HOST=MyApplication_mailcatcher
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

Dostęp do kontenera przez ssh

Należy pamiętać, że komendy artisana muszą być uruchamiane w kontenerze dockera, nie na komputerze-hoście. Aby dostać się do powłoki kontenera, będąc w katalogu z naszą konfiguracją dockera wykonujemy polecenie:

docker-compose exec php bash

Przywita nas wówczas znak zachęty:

root@862287329e3e:/var/www/html#

Katalog w którym się znajdziemy to główny katalog projektu, gdzie możemy np. uruchamiać komendy artisana. Z tego poziomu możemy również uruchomić `composer install` oraz przyznawać uprawnienia do plików/katalogów, na pewno będziemy potrzebować następujących uprawnień:

chown www-data:www-data storage/logs/

chown www-data:www-data storage/framework/sessions/

chown www-data:www-data storage/framework/views

Należy wspomnieć, że użytkownik w sesji ssh kontenera to `root`, natomiast użytkownik na którym działa serwer www to `www-data`. Może się więc zdarzyć, że wykonamy komendę artisana, która utworzy plik log i ten plik będzie miał uprawnienia roota, serwer www nie będzie mógł do niego zapisywać. Aby tego uniknąć otwórz plik config/logging.php w aplikacji laravel i w sekcji channels -> daily ustaw:

‚path’ => storage_path(‚logs/’ . php_sapi_name() . ‚/laravel.log’),

Dzięki temu pliki .log będą zapisywane w osobnych katalogach dla konsoli i dla serwera:

storage/logs/
|-- cli
|   `-- laravel-2019-05-09.log
`-- fpm-fcgi
    `-- laravel-2019-05-09.log

Zamiast zmieniać kanał ‘daily’, możesz również utworzyć osobny kanał logowania i ustawić go w swoim .env, żeby zmienić sposób logowania tylko w Twojej instancji deweloperskiej.

Cron

W tym momencie możemy dodać wpis do crona. Z poziomu powłoki kontenera uruchamiamy polecenie:

crontab -e

I na końcu pliku dodajemy linię:

* * * * * cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1

Https

Jeśli chcemy mieć dostęp do naszej aplikacji poprzez protokół https:// musimy w pliku ./etc/nginx/app.conf odkomentować wszystkie zakomentowane linie, a następnie zrestartować serwer. Aby zrestartować serwer, logujemy się do kontenera `web`:

docker-compose exec web bash

I wykonujemy polecenie:

service nginx restart

Jeśli zajrzymy teraz w logi kontenera, to znajdziemy tam błędy:

No such file or directory:fopen(‚/etc/ssl/server.pem’,’r’)

Musimy zatem utworzyć certyfikat ssl. Aby tego dokonać, uruchom polecenie z poziomu swojego systemu (nie w kontenerze) w katalogu z konfiguracją dockera:

sudo docker run –rm -v $(pwd)/etc/ssl:/certificates -e „SERVER=myapp.test” jacoelho/generate-certificate

Wartość `myapp.test` musi odpowiadać nazwie hosta z pliku ./etc/nginx/app.conf.

Polecenie wygeneruje potrzebne certyfikaty w katalogu ./etc/ssl:

etc/ssl/
├── cacert.pem
├── server.key
└── server.pem

Aplikacja będzie działać pod adresem https://myapp.test/. Oczywiście przeglądarka będzie zgłaszać nieważny certyfikat, ale po jego zaakceptowaniu strona będzie działać.

Mailcatcher

Mamy skonfigurowane narzędzie mailcatcher, aby uruchomić jego panel otwórz w przeglądarce adres http://localhost:1080 (port odpowiada wartości PORT_MAILCATCHER z pliku .env dockera). Wszelkie maile wysyłane z aplikacji zobaczysz w tym panelu. Jest to bardzo przydatne, jeśli chcemy testować wysyłkę maili lokalnie, bez wypuszczania jakichkolwiek e-maili w świat.

Dostęp do aplikacji z sieci lokalnej

Jeśli chcesz zobaczyć jak aplikacja działa na innym urządzeniu w sieci lokalnej, np. na telefonie komórkowym, możesz to zrobić stawiając proxy przy pomocy narzędzia squid, informacje jak to zrobić znajdziesz w artykule Dostęp do kontenera dockera z telefonu w sieci lokalnej.

 

Udostępnij

Skomentuj