В этой главе мы создадим новое простое приложение на Laravel, которое, по мере прохождения следующих глав, будет наращивать функциональность, пока не станет похожим на реальное приложение, на котором мы продемонстрируем лучшие практики организации приложений и CI/CD с werf.

Подготовка окружения

Подготовьте рабочее окружение согласно инструкциями главы “Подготовка окружения”, если это ещё не сделано.

Рабочее окружение работало, но перестало? Инструкции из этой главы не работают? Может помочь:

Работает ли Docker?

Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Запустим Docker:

sudo systemctl restart docker

Убедимся, что Docker запустился:

sudo systemctl status docker

Результат выполнения команды, если Docker успешно запущен:

● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2021-06-24 13:05:17 MSK; 13s ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
   Main PID: 2013888 (dockerd)
      Tasks: 36
     Memory: 100.3M
     CGroup: /system.slice/docker.service
             └─2013888 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

dockerd[2013888]: time="2021-06-24T13:05:16.936197880+03:00" level=warning msg="Your kernel does not support CPU realtime scheduler"
dockerd[2013888]: time="2021-06-24T13:05:16.936219851+03:00" level=warning msg="Your kernel does not support cgroup blkio weight"
dockerd[2013888]: time="2021-06-24T13:05:16.936224976+03:00" level=warning msg="Your kernel does not support cgroup blkio weight_device"
dockerd[2013888]: time="2021-06-24T13:05:16.936311001+03:00" level=info msg="Loading containers: start."
dockerd[2013888]: time="2021-06-24T13:05:17.119938367+03:00" level=info msg="Loading containers: done."
dockerd[2013888]: time="2021-06-24T13:05:17.134054120+03:00" level=info msg="Daemon has completed initialization"
systemd[1]: Started Docker Application Container Engine.
dockerd[2013888]: time="2021-06-24T13:05:17.148493957+03:00" level=info msg="API listen on /run/docker.sock"

Теперь проверим, что Docker доступен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Перезагружали компьютер после подготовки окружения?

Запустим кластер minikube, уже настроенный в начале главы “Подготовка окружения”:

minikube start

Выставим Namespace по умолчанию, чтобы не указывать его при каждом вызове kubectl:

kubectl config set-context minikube --namespace=werf-guide-app

Результат успешного выполнения команды:

😄  minikube v1.20.0 on Ubuntu 20.04
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🎉  minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
    ▪ Using image registry:2.7.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying registry addon...
🔎  Verifying ingress addon...
🌟  Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄  Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default

Убедитесь, что вывод команды содержит строку:

Restarting existing docker container for "minikube"

Если её нет, значит был создан новый кластер minikube вместо использования старого. В таком случае повторите установку рабочего окружения с minikube с самого начала.

Теперь запустите команду в фоновом PowerShell-терминале и не закрывайте его:

minikube tunnel --cleanup=true

Запустим кластер minikube, уже настроенный в начале главы “Подготовка окружения”:

minikube start --namespace werf-guide-app

Выставим Namespace по умолчанию, чтобы не указывать его при каждом вызове kubectl:

kubectl config set-context minikube --namespace=werf-guide-app

Результат успешного выполнения команды:

😄  minikube v1.20.0 on Ubuntu 20.04
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🎉  minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
    ▪ Using image registry:2.7.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying registry addon...
🔎  Verifying ingress addon...
🌟  Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄  Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default

Убедитесь, что вывод команды содержит строку:

Restarting existing docker container for "minikube"

Если её нет, значит был создан новый кластер minikube вместо использования старого. В таком случае повторите установку рабочего окружения с minikube с самого начала.

Случайно удаляли Namespace приложения?

Если вы непреднамеренно удалили Namespace приложения, то необходимо выполнить следующие команды, чтобы продолжить прохождение руководства:

kubectl create namespace werf-guide-app
kubectl create secret docker-registry registrysecret \
  --docker-server='https://index.docker.io/v1/' \
  --docker-username='<имя пользователя Docker Hub>' \
  --docker-password='<пароль пользователя Docker Hub>'

Результат успешного выполнения команды:

namespace/werf-guide-app created
secret/registrysecret created
Ничего не помогло, окружение или инструкции по-прежнему не работают?

Если ничего не помогло, то пройдите главу “Подготовка окружения” с начала, подготовив новое окружение с нуля. Если и это не помогло, тогда, пожалуйста, расскажите о своей проблеме в нашем Telegram или оставьте Issue на GitHub, и мы обязательно вам поможем.

Подготовка репозитория

Обновим существующий репозиторий с приложением:

Выполним следующий набор команд в PowerShell:

cd ~/werf-guide/app

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides/examples/laravel/010_basic_app/* .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Выполним следующий набор команд в Bash:

cd ~/werf-guide/app

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rf ~/werf-guide/guides/examples/laravel/010_basic_app/. .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Не работает? Попробуйте инструкции на вкладке “Начинаю проходить руководство с этой главы” выше.

Подготовим новый репозиторий с приложением:

Выполним следующий набор команд в PowerShell:

# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован
if (-not (Test-Path ~/werf-guide/guides)) {
  git clone https://github.com/werf/werf-guides $env:HOMEPATH/werf-guide/guides
}

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -Recurse -Force ~/werf-guide/app
cp -Recurse -Force ~/werf-guide/guides/examples/basic/004_modify_app ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий
cd ~/werf-guide/app
git init
git add .
git commit -m initial

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides/examples/laravel/010_basic_app/* .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Выполним следующий набор команд в Bash:

# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован
test -e ~/werf-guide/guides || git clone https://github.com/werf/werf-guides ~/werf-guide/guides

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -rf ~/werf-guide/app
cp -rf ~/werf-guide/guides/examples/basic/004_modify_app ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий
cd ~/werf-guide/app
git init
git add .
git commit -m initial

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rf ~/werf-guide/guides/examples/laravel/010_basic_app/. .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Новое приложение на Laravel

Далее мы будем использовать новое простое приложение на Laravel, функционально идентичное приложению из предыдущего раздела.

Чтобы его получить, сначала был сгенерирован скелет нового Laravel-приложения командой:

docker run --rm \
    -v $(pwd):/opt \
    -w /opt \
    laravelsail/php80-composer:latest \
    bash -c "composer create-project laravel/laravel ."

В нём была оставлена конфигурация только для production-окружения и убрано всё, что не требуется на данном этапе.

После этого был добавлен новый путь /ping для нашего API:

<?php

use App\Http\Controllers\PingController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::get('/ping', [PingController::class, 'ping']);
<?php use App\Http\Controllers\PingController; use Illuminate\Support\Facades\Route; /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return view('welcome'); }); Route::get('/ping', [PingController::class, 'ping']);

Этот путь будет обрабатываться методом ping стандартного контроллера, возвращающим текст pong:

<?php

namespace App\Http\Controllers;

class PingController extends Controller
{
    public function ping(): string
    {
        return 'pong';
    }
}
<?php namespace App\Http\Controllers; class PingController extends Controller { public function ping(): string { return 'pong'; } }

Что касается сборки и CI/CD, то основные изменения затронули сборку образа:

# Основной backend-образ с приложением и php-fpm.
FROM php:8.0-fpm-alpine as backend
WORKDIR /app

# Устанавливаем системные зависимости, необходимые php-модули и composer.
RUN set -x \
    && apk add --no-cache \
        icu-libs \
        bash \
    && apk add --no-cache --virtual .build-deps \
        icu-dev \
        autoconf \
        openssl \
        make \
        g++ \
    && docker-php-ext-install -j$(nproc) \
        sockets \
        opcache \
        pcntl \
        intl \
        1>/dev/null \
    && docker-php-source delete \
    && apk del .build-deps \
    && php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer

# Копируем в образ файлы, нужные для установки зависимостей приложения.
COPY ./composer.* /app/
# Устанавливаем зависимости приложения (`dump-autoload` запустим после копирования остальных файлов приложения).
RUN composer install -n --no-dev --no-cache --no-ansi --no-autoloader --no-scripts --prefer-dist

# Копируем в образ все остальные файлы приложения.
COPY --chown=www-data:www-data . /app/
# Перегенерируем автозагрузчик классов.
RUN composer dump-autoload -n --optimize

# Копируем конфигурацию php-fpm.
COPY .werf/zzz-php-fpm-config.conf /usr/local/etc/php-fpm.d/zzz-php-fpm-config.conf

EXPOSE 9000

#############################################################################

# NGINX-образ с публичными файлами.
FROM nginx:stable-alpine as frontend
WORKDIR /www

# Копируем публичные файлы из backend образа.
COPY --from=backend /app/public /www

# Копируем конфигурацию NGINX.
COPY .werf/nginx.conf /etc/nginx/nginx.conf

EXPOSE 8080
# Основной backend-образ с приложением и php-fpm. FROM php:8.0-fpm-alpine as backend WORKDIR /app # Устанавливаем системные зависимости, необходимые php-модули и composer. RUN set -x \ && apk add --no-cache \ icu-libs \ bash \ && apk add --no-cache --virtual .build-deps \ icu-dev \ autoconf \ openssl \ make \ g++ \ && docker-php-ext-install -j$(nproc) \ sockets \ opcache \ pcntl \ intl \ 1>/dev/null \ && docker-php-source delete \ && apk del .build-deps \ && php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer # Копируем в образ файлы, нужные для установки зависимостей приложения. COPY ./composer.* /app/ # Устанавливаем зависимости приложения (`dump-autoload` запустим после копирования остальных файлов приложения). RUN composer install -n --no-dev --no-cache --no-ansi --no-autoloader --no-scripts --prefer-dist # Копируем в образ все остальные файлы приложения. COPY --chown=www-data:www-data . /app/ # Перегенерируем автозагрузчик классов. RUN composer dump-autoload -n --optimize # Копируем конфигурацию php-fpm. COPY .werf/zzz-php-fpm-config.conf /usr/local/etc/php-fpm.d/zzz-php-fpm-config.conf EXPOSE 9000 ############################################################################# # NGINX-образ с публичными файлами. FROM nginx:stable-alpine as frontend WORKDIR /www # Копируем публичные файлы из backend образа. COPY --from=backend /app/public /www # Копируем конфигурацию NGINX. COPY .werf/nginx.conf /etc/nginx/nginx.conf EXPOSE 8080

… а CI/CD изменилось незначительно. К примеру, так выглядит новый Deployment приложения:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: werf-guide-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: werf-guide-app
  template:
    metadata:
      labels:
        app: werf-guide-app
    spec:
      imagePullSecrets:
        - name: registrysecret
      volumes:
        - name: run-php
          emptyDir: {}
      containers:
        - name: backend
          image: {{ .Values.werf.image.backend }}
          env:
            - name: APP_ENV
              value: production
            - name: APP_KEY
              value: base64:GcPVmSxMZwsOJtNOJ9eVNNeU6B5buHuln93+w0TSvfE=
          volumeMounts:
            - mountPath: /var/run/php
              name: run-php
        - name: frontend
          image: {{ .Values.werf.image.frontend }}
          ports:
            - containerPort: 8080
              name: http
          volumeMounts:
            - mountPath: /var/run/php
              name: run-php
apiVersion: apps/v1 kind: Deployment metadata: name: werf-guide-app spec: replicas: 1 selector: matchLabels: app: werf-guide-app template: metadata: labels: app: werf-guide-app spec: imagePullSecrets: - name: registrysecret volumes: - name: run-php emptyDir: {} containers: - name: backend image: {{ .Values.werf.image.backend }} env: - name: APP_ENV value: production - name: APP_KEY value: base64:GcPVmSxMZwsOJtNOJ9eVNNeU6B5buHuln93+w0TSvfE= volumeMounts: - mountPath: /var/run/php name: run-php - name: frontend image: {{ .Values.werf.image.frontend }} ports: - containerPort: 8080 name: http volumeMounts: - mountPath: /var/run/php name: run-php

Приложение — полностью stateless, не требует базы данных для работы. Мы сделаем его stateful и развернём для него базу данных в следующих главах.

Деплой нового приложения

Проверим работоспособность нового приложения, развернув его в Kubernetes:

werf converge --repo <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-guide-app

Ожидаемый результат:

...
┌ ⛵ image backend
│ ┌ Building stage backend/dockerfile
│ │ backend/dockerfile  Sending build context to Docker daemon  485.4kB
│ │ backend/dockerfile  Step 1/18 : FROM php:8.0-fpm-alpine as backend
│ │ backend/dockerfile   ---> 52c511f481c5
...
│ │ backend/dockerfile  Successfully built 75a23bca33d3
│ │ backend/dockerfile  Successfully tagged 80b5cdbb-4b00-41b2-8aa9-65fcfdd2f88a:latest
│ ├ Info
│ │      name: .../werf-guide-app:4e2d1c37b299cc5b21e71eb5f0a296e8c0fc917adba797d20dfa46fb-1633609892956
│ │        id: 75a23bca33d3
│ │   created: 2021-10-07 15:36:18 +0300 MSK
│ │      size: 48.8 MiB
│ └ Building stage backend/dockerfile (15.94 seconds)
└ ⛵ image backend (22.00 seconds)

┌ ⛵ image frontend
│ ┌ Building stage frontend/dockerfile
│ │ frontend/dockerfile  Sending build context to Docker daemon  485.4kB
│ │ frontend/dockerfile  Step 1/23 : FROM php:8.0-fpm-alpine as backend
│ │ frontend/dockerfile   ---> 52c511f481c5
...
│ │ frontend/dockerfile  Successfully built 8f4a95930a1d
│ │ frontend/dockerfile  Successfully tagged 19ddb0ae-a79a-4496-a920-1e1e44900b09:latest
│ ├ Info
│ │      name: .../werf-guide-app:a16f913d838b6e697159b09acd22af3f798404c050af61792946733c-1633609892635
│ │        id: 8f4a95930a1d
│ │   created: 2021-10-07 15:36:18 +0300 MSK
│ │      size: 9.4 MiB
│ └ Building stage frontend/dockerfile (12.62 seconds)
└ ⛵ image frontend (18.51 seconds)

┌ Waiting for release resources to become ready
│ ┌ Status progress
│ │ DEPLOYMENT                                                                                      REPLICAS          AVAILABLE           UP-TO-DATE
│ │ werf-guide-app                                                                                  2->1/1            1                   1
│ │ │   POD                                 READY        RESTARTS          STATUS
│ │ ├── guide-app-5478b4cfbb-cpkf8          2/2          0                 ContainerCreating ->
│ │ │                                                                      Running
│ │ └── guide-app-59c465b577-5xd44          2/2          0                 Running -> Terminating
│ └ Status progress
└ Waiting for release resources to become ready (8.55 seconds)

Release "werf-guide-app" has been upgraded. Happy Helming!
NAME: werf-guide-app
LAST DEPLOYED: Thu Oct  7 15:36:36 2021
NAMESPACE: werf-guide-app
STATUS: deployed
REVISION: 2
TEST SUITE: None
Running time 39.04 seconds
... ┌ ⛵ image backend │ ┌ Building stage backend/dockerfile │ │ backend/dockerfile Sending build context to Docker daemon 485.4kB │ │ backend/dockerfile Step 1/18 : FROM php:8.0-fpm-alpine as backend │ │ backend/dockerfile ---> 52c511f481c5 ... │ │ backend/dockerfile Successfully built 75a23bca33d3 │ │ backend/dockerfile Successfully tagged 80b5cdbb-4b00-41b2-8aa9-65fcfdd2f88a:latest │ ├ Info │ │ name: .../werf-guide-app:4e2d1c37b299cc5b21e71eb5f0a296e8c0fc917adba797d20dfa46fb-1633609892956 │ │ id: 75a23bca33d3 │ │ created: 2021-10-07 15:36:18 +0300 MSK │ │ size: 48.8 MiB │ └ Building stage backend/dockerfile (15.94 seconds) └ ⛵ image backend (22.00 seconds) ┌ ⛵ image frontend │ ┌ Building stage frontend/dockerfile │ │ frontend/dockerfile Sending build context to Docker daemon 485.4kB │ │ frontend/dockerfile Step 1/23 : FROM php:8.0-fpm-alpine as backend │ │ frontend/dockerfile ---> 52c511f481c5 ... │ │ frontend/dockerfile Successfully built 8f4a95930a1d │ │ frontend/dockerfile Successfully tagged 19ddb0ae-a79a-4496-a920-1e1e44900b09:latest │ ├ Info │ │ name: .../werf-guide-app:a16f913d838b6e697159b09acd22af3f798404c050af61792946733c-1633609892635 │ │ id: 8f4a95930a1d │ │ created: 2021-10-07 15:36:18 +0300 MSK │ │ size: 9.4 MiB │ └ Building stage frontend/dockerfile (12.62 seconds) └ ⛵ image frontend (18.51 seconds) ┌ Waiting for release resources to become ready │ ┌ Status progress │ │ DEPLOYMENT REPLICAS AVAILABLE UP-TO-DATE │ │ werf-guide-app 2->1/1 1 1 │ │ │ POD READY RESTARTS STATUS │ │ ├── guide-app-5478b4cfbb-cpkf8 2/2 0 ContainerCreating -> │ │ │ Running │ │ └── guide-app-59c465b577-5xd44 2/2 0 Running -> Terminating │ └ Status progress └ Waiting for release resources to become ready (8.55 seconds) Release "werf-guide-app" has been upgraded. Happy Helming! NAME: werf-guide-app LAST DEPLOYED: Thu Oct 7 15:36:36 2021 NAMESPACE: werf-guide-app STATUS: deployed REVISION: 2 TEST SUITE: None Running time 39.04 seconds

Проверим, действительно ли наше приложение работает:

curl http://werf-guide-app/ping

Ожидаемый ответ:

pong