Настройка docker для Laravel и Vue на одном домене

Ранее здесь уже описывалась настройка docker для приложения на базе Laravel и Vue с использование поддомена. В том материале backend с Laravel размещался на поддомене, а frontend с Vue – на основном домене. Однако такая конфигурация может иметь свои недостатки (например, проблемы с CORS). В данной статье попробуем настроить для локальной работы такое же окружение, но на одном домене. Чтобы у Laravel работали одни маршруты, а у Vue – другие.

Итоговый код примера можно взять в репозитории на GitHub. Ниже в тексте статьи будут описаны все необходимые шаги для нового проекта.

Содержание статьи

Настройка docker compose

Для нашего тестового приложения будем использовать домен vue3-laravel-app.local. Добавим в файл /etc/hosts строку:

127.0.0.1 vue3-laravel-app.local

В корне проекта создаем файл docker-compose.yml и папку docker с отдельными файлами конфигурации:

version: '3.8'
services:

  # Backend контейнер
  backend:
    # Для установки нужных пакетов используем не чистый образ, а инструкции из конкретного файла Dockerfile
    build:
      context: .
      dockerfile: ./docker/backend/Dockerfile
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped
    tty: true
    working_dir: /var/www
    volumes:
      - .:/var/www # Монтируем локальную папку в контейнер как /var/www
      - ./docker/backend/php.ini:/usr/local/etc/php/php.ini
    depends_on:
      - db

  # Nginx контейнер для вебсервера
  nginx:
    # Используем готовый образ для nginx контейнера
    image: nginx:alpine
    restart: unless-stopped
    tty: true
    ports:
      - "80:80" # Внутренний порт контейнера пробрасываем на host машину
    volumes:
      - .:/var/www # Монтируем локальную папку в контейнер как /var/www
      - ./docker/nginx/conf.d/:/etc/nginx/conf.d/ # Передаем в контейнер конфигурационные файлы nginx
    depends_on:
      - backend

  # MySQL контейнер
  db:
    image: mysql:5.7.22
    restart: unless-stopped
    tty: true
    ports:
      - "3306:3306"
    command: ['--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci','--default-authentication-plugin=mysql_native_password']
    environment:
      # Желаемые настройки для СУБД MySQL
      MYSQL_DATABASE: laravel
      MYSQL_ROOT_PASSWORD: 'root'
      MYSQL_USER: 'appuser'
      MYSQL_PASSWORD: 'appuser'
    volumes:
      - dbdata:/var/lib/mysql # Используем именованный том из блока volumes
      - ./docker/mysql/my.cnf:/etc/mysql/my.cnf # Передаем в контейнер конфигурационный файл
    healthcheck:
      test: [ "CMD", "mysqladmin", "ping" ]

  # Для сборки js
  node:
    build:
        context: .
        dockerfile: ./docker/nodejs/Dockerfile
    tty: true
    ports:
        - "5173:5173"
    working_dir: /var/www
    volumes:
        - .:/var/www # Монтируем локальную папку в контейнер как /var/www

# Тома
volumes:
  # Чтобы данные БД не пропадали после выключения создаем именованный том
  dbdata:
    driver: local

В папке docker/backend файлы для настройки контейнера с php:

FROM php:8.0-fpm

# Install dependencies
RUN apt-get update && apt-get install -y \
    build-essential \
    libpng-dev \
    libjpeg62-turbo-dev \
    libfreetype6-dev \
    locales \
    libonig-dev \
    zip \
    libzip-dev \
    jpegoptim optipng pngquant gifsicle \
    vim \
    unzip \
    git \
    curl && \
    pecl install xdebug-3.0.1 && \
    docker-php-ext-enable xdebug && \
    docker-php-ext-install pdo_mysql mbstring zip exif pcntl && \
    docker-php-ext-configure gd --with-freetype --with-jpeg && \
    docker-php-ext-install gd && \
    curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \
    # Clear cache
    apt-get clean && rm -rf /var/lib/apt/lists/*

# Add user for laravel application
RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www

# Change current user to www
USER www

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]
xdebug.mode = debug
xdebug.client_host = host.docker.internal
xdebug.discover_client_host = 1
xdebug.start_with_request = yes
xdebug.log_level = 0

cgi.fix_pathinfo=0

post_max_size = 256M
upload_max_filesize = 256M

max_execution_time = 1000
max_input_time = 1000

В папке docker/nginx/conf.d/app.conf настройка виртуального хоста для nginx:

server {
    listen 80;
    server_name vue3-laravel-app.local;
    root /var/www/public;

    index index.php index.html;
    error_log  /var/log/nginx/backend.error.log;
    access_log /var/log/nginx/backend.access.log;

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass backend:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
    location / {
        try_files $uri $uri/ /index.php?$query_string;
        gzip_static on;
    }

}

Немного настроек для mysql:

[mysqld]
general_log = 1
general_log_file = /var/lib/mysql/general.log

И, наконец, в папке docker/nodejs настройки контейнера для node:

FROM node:16

# Install dependencies
RUN apt-get update && apt-get install -y \
    vim \
    zip \
    unzip \
    curl

# Change current user
USER node

EXPOSE 5173

# https://github.com/vitejs/vite/discussions/3396
CMD ["sh", "-c", "npm install && npm run dev -- --host"]

Также нам нужно создать в папке public файл index.php с любым простым php кодом:

<?php
phpinfo();

Теперь в корне проекта запустим наше docker окружение, но пока без контейнера node:

docker compose up nginx -d

[+] Running 3/3
 ⠿ Container vue3-laravel-docker-db-1       Started
 ⠿ Container vue3-laravel-docker-backend-1  Started
 ⠿ Container vue3-laravel-docker-nginx-1    Started    

Здесь мы просто указываем запуск контейнера nginx, а контейнеры backend и db включатся за счет того, что они являются зависимостями, описанными в docker-compose.yml. После этого шага команда docker compose ps должна отображать 3 запущенных контейнера, а в браузере будет открываться сайт http://vue3-laravel-app.local со страницей phpinfo.

Установка Laravel

Теперь зайдем в оболочку bash в контейнере backend.

docker compose exec backend /bin/bash

www@302629478a1e:/var/www$ 

В нем удалим папку public и с помощью composer создадим проект Laravel в поддиректории tmp, а затем скопируем в корневую папку содержимое tmp:

www@2307ac0601e5:/var/www$ rm -R public/
www@2307ac0601e5:/var/www$ composer create-project --prefer-dist laravel/laravel:^9.0 tmp
www@2307ac0601e5:/var/www$ mv tmp/* ./
www@2307ac0601e5:/var/www$ mv tmp/.e* ./
www@2307ac0601e5:/var/www$ mv tmp/.git* ./
www@2307ac0601e5:/var/www$ exit

К промежуточной папке tmp пришлось прибегнуть, так как composer не создаст проект в папке, где уже есть файлы. А у нас там папки и файлы связанные с docker.

Теперь по адресу с нашил локальным сайтом уже открывается страница Laravel.

Настройка Vue

Теперь выключим наши контейнеры, и запустим снова. Но уже все, которые прописаны в docker-compose.yml:

docker compose stop
docker compose up -d

Команда docker compose ps теперь должна показывать, что запущены 4 контейнера. Добавился контейнер node, который после установки Laravel успешно будет запущен, так как есть все необходимое для успешного выполнения строки CMD ["sh", "-c", "npm install && npm run dev -- --host"] из docker/nodejs/Dockerfile . Ведь теперь имеется и composer.json и vite.config.js с нужными настройками.

Установим Vue:

docker-compose exec node /bin/bash -lc 'npm install -D vue@next'

Установим плагин Vite для поддержки Vue:

docker-compose exec node /bin/bash -lc 'npm install -D @vitejs/plugin-vue'

Vite позволит как создавать js сборку с помощью Rollup, так и запускать локальную отладочную версию вашего frontend с использованием горячей перезагрузки.

Надо будет внести небольшие изменения в vite.config.js

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    /*server: {
        host: '192.168.128.5'
    },*/
    plugins: [
        laravel(['resources/js/app.js', 'resources/css/app.css']),
        vue({
            template: {
                transformAssetUrls: {
                    // The Vue plugin will re-write asset URLs, when referenced
                    // in Single File Components, to point to the Laravel web
                    // server. Setting this to `null` allows the Laravel plugin
                    // to instead re-write asset URLs to point to the Vite
                    // server instead.
                    base: null,

                    // The Vue plugin will parse absolute URLs and treat them
                    // as absolute paths to files on disk. Setting this to
                    // `false` will leave absolute URLs un-touched so they can
                    // reference assets in the public directory as expected.
                    includeAbsolute: false,
                },
            },
        }),
    ],
});

И создать немного кода на Vue:

import './bootstrap';

import {createApp} from 'vue'
import App from './App.vue'

createApp(App).mount("#app")
<template>
    Vue 3 Component
</template>

Немного изменений на стороне Laravel. Подготовим шаблон blade:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Install Vue 3 in Laravel 9 with Vite</title>

    @vite('resources/css/app.css')
</head>
<body>
<div id="app"></div>

@vite('resources/js/app.js')
</body>
</html>

Уберем маршрут по-умолчанию и создадим свой:

<?php

use Illuminate\Support\Facades\Route;

Route::get('{all}', function () {
    return view('app');
})->where('all', '.*');

Тоесть для любых маршрутов, которые не будут объявлены до этой инструкции будет отображаться наше представление с app.blade.php, где подключен Vite с Vue.

И пропишем APP_URL в .env:

APP_URL=http://vue3-laravel-app.local

Теперь в браузере у нас отобразится страница с надписью Vue 3 Component. А эта надпись у нас содержится как раз во Vue компоненте.

Главная страница сайта, на которой находится vue компонент

Настройка маршрутов Vue-Router

Теперь настало время настроить маршруты на стороне frontend. Установим пакет vue-router:

docker compose exec node /bin/bash -lc 'npm install -D vue-router@4'

Добавим файл с маршрутами:

import { createWebHistory, createRouter } from "vue-router";
import Home from "@/views/Home.vue";
import About from "@/views/About.vue";
import Error404 from "@/views/Error404.vue";

const routes = [
    {
        path: "/",
        name: "Home",
        component: Home,
    },
    {
        path: "/about",
        name: "About",
        component: About,
    },
    {
        path: '/:pathMatch(.*)*',
        name: 'Error404',
        component: Error404,
    },
];

const router = createRouter({
    history: createWebHistory(),
    routes,
});

export default router;

А также создадим необходимые страницы:

<template>
    <h1>Home Page</h1>
</template>
<template>
    <h1>About Page</h1>
</template>
<template>
    <h1>Error 404. Page not found</h1>
</template>

И внесем изменения в resources/js/app.js для использования маршрутов:

import './bootstrap';

import {createApp} from 'vue'
import App from './App.vue'
import router from './router' // <- Здесь

createApp(App).use(router).mount("#app") // <- И здесь

Отредактируем основной компонент:

<template>
    <div id="nav">
        <router-link to="/">Home</router-link> |
        <router-link to="/about">About</router-link>
    </div>
    <router-view />
</template>

Теперь на главной странице будет отображаться заголовок Home Page, по адресу http://vue3-laravel-app.local/about будет страница с заголовком About Page, а при попытке зайти на несуществующий URL (например, http://vue3-laravel-app.local/somehere) пользователь увидит Error 404. Page not found:

Обратите внимание, что при перезагрузке страницы маршруты обрабатываются корректно, так же как и при кликах по ссылкам. Если у backend есть маршрут, то загрузится он, если нет, то – будет показана страница с Vue компонентом и маршрут будет обработан через Vue-Router.

Добавление маршрута для backend API

Сейчас для полноты картины хотелось бы добавить маршрут на backend, по которому frontend будет получать какие-либо данные асинхронно. На главной странице у нас будет кнопка, по нажатию на которую произойдет запрос к серверу и отображение полученных данных.

Изменим компонент главной страницы:

<template>
    <h1>Home Page</h1>

    <button @click="load" v-if="migrations.length === 0">Load data from server</button>

    <div class="server-response" v-if="migrations.length > 0">
        <h2>Data from server:</h2>
        <ul>
            <li v-for="item in migrations" :key="item.id">{{ item.migration }}</li>
        </ul>
    </div>

</template>

<script>
import axios from 'axios'
axios.defaults.withCredentials = true

import {ref} from "vue";

export default {
    name: 'Hello',

    setup() {

        const migrations = ref([])

        function onSuccess(response) {
            migrations.value = response.data;
        }

        function load() {
            axios.get('/api/migration')
                .then(onSuccess)
                .catch((error) => { alert(`Error ${error.message}`) })
        }

        return {
            load,
            migrations
        }
    }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
    h3 {
        margin: 40px 0 0;
    }
    a {
        color: #42b983;
    }
    ul {
        text-align: start;
    }
    button {
        font-size: 1.1rem;
        cursor: pointer;
    }
    .server-response {
        display: inline-block;
    }
    .server-response h2 {
        text-align: start;
        margin-top: 0;
    }
</style>

Добавим маршрут на backend:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;

Route::get('migration', function () {
    return DB::table('migrations')->get();
});

Скорректируем соединение с БД в .env:

# ...
DB_CONNECTION=mysql
# Имя контейнера с БД
DB_HOST=db
DB_PORT=3306
# Имя БД и данные пользователя из docker-compose.yml
DB_DATABASE=laravel
DB_USERNAME=appuser 
DB_PASSWORD=appuser
# ...

Запустим выполнение миграций в контейнере backend:

docker compose exec backend /bin/bash -lc 'php artisan migrate'


 INFO  Preparing database.  

  Creating migration table ................................ 43ms DONE

   INFO  Running migrations.  

  2014_10_12_000000_create_users_table .................... 84ms DONE
  2014_10_12_100000_create_password_resets_table ......... 113ms DONE
  2019_08_19_000000_create_failed_jobs_table .............. 85ms DONE
  2019_12_14_000001_create_personal_access_tokens_table .. 119ms DONE

По адресу http://vue3-laravel-app.local/api/migration должен отдаваться JSON:

[{"id":1,"migration":"2014_10_12_000000_create_users_table","batch":1},{"id":2,"migration":"2014_10_12_100000_create_password_resets_table","batch":1},{"id":3,"migration":"2019_08_19_000000_create_failed_jobs_table","batch":1},{"id":4,"migration":"2019_12_14_000001_create_personal_access_tokens_table","batch":1}]

На главной странице теперь у нас есть кнопка, по нажатию на которую на страницу загружается список миграций БД:

Сборка frontend для production

До сих пор была показана работа с development вариантом frontend. Благодаря интеграции Laravel с Vite специальные инструкции в blade шаблоне делают так, чтобы в html-странице были строки:

...
<script type="module" src="http://[::]:5173/@vite/client"></script><link rel="stylesheet" href="http://[::]:5173/resources/css/app.css" />
...
<script type="module" src="http://[::]:5173/@vite/client"></script><script type="module" src="http://[::]:5173/resources/js/app.js"></script>
...

Благодаря настройкам docker-compose.yml порт 5173 из контейнера пробрасывается на host и сборки JS и CSS подключаются на нашу страницу.

Для получения готовых файлов frontend для production необходимо выполнить команду

docker-compose exec node /bin/bash -lc 'npm run build'

> build
> vite build

vite v4.0.4 building for production...
✓ 76 modules transformed.
public/build/manifest.json              0.41 kB
public/build/assets/app-49070b02.css    0.29 kB │ gzip:  0.17 kB
public/build/assets/app-4ed993c7.js     0.00 kB │ gzip:  0.02 kB
public/build/assets/app-21985ffa.js   179.59 kB │ gzip: 68.51 kB

Далее в рамках процедуры развертывания (deploy) эти файлы вместе с остальными файлами проекта должны попасть на production серверы. Настройка процедуры развертывания не рассматривается в статье.

Но наш локальный сайт все еще обращается к порту 5173 с development вариантом. Если мы хотим посмотреть как будет работать приложение с собранными файлами нам необходимо выполнить следующие команды:

docker-compose stop node
rm public/hot

Важно, чтобы не стало файла public/hot. Видимо, по этому файлу Laravel-приложение понимает какую версию frontend загружать.

Теперь, если перезагрузить страницу в браузере, то можно увидеть, что JS и CSS подключены уже другие:

Уже нет обращения к порту 5173, а загрузка JS и CSS происходит с использованием папки build.

Чтобы после этих операций снова работать с Vite запускаем контейнер node:

docker-compose start node

После чего снова на страницу будет подключена development версия JS и CSS.

В результате мы получили приложение на базе Laravel и Vue развернутое в docker контейнерах имеющее на одном домене как маршруты Laravel так и Vue-Router.

Если по данной теме остались какие-либо вопросы, то отправляйте их, пожалуйста, через форму обратной связи на сайте. Возможно статья будет дополнена или Ваша тема может стать материалом следующей статьи.

14 комментариев

  1. Сделал сначала по гайду, не получилось, склонил репу себе, установил пакеты все, выходит та же ошибка GET http://[::]:5173/@vite/client net::ERR_ADDRESS_INVALID

  2. Сергей, можете уточнить после запуска какой команды и где эта ошибка? Ошибка в браузере, верно? После клонирования какие команды выполняете? Все остальное (команды) без ошибок работает? В логе контейнера node нет ошибок связанных с запуском vite?

    1. в логах ноды ошибок нет, ошибка в браузере, после клонирования захожу в контейнер php и пишу composer install, потом качаю пакеты для vue и vite, генерирую ключ ларавель, работает http://localhost:5173/ где содержится информация про vite и урл приложения, но при заходе на локал хост, скрипт который должен подтягивать vite, выдает ошибки
      GET http://[::]:5173/@vite/client net::ERR_ADDRESS_INVALID
      GET http://[::]:5173/resources/css/app.css net::ERR_ADDRESS_INVALID
      GET http://[::]:5173/resources/js/app.js net::ERR_ADDRESS_INVALID

      1. ну единственное что я сделал, только домен локальный оставил localhost, чтобы не менять в /etc/hosts ничего

    2. если менять домен, то будет ошибка
      app.css:1 Failed to load resource: net::ERR_ADDRESS_INVALID
      app.js:1 Failed to load resource: net::ERR_ADDRESS_INVALID

  3. Попробуем разобраться.

    Предлагаю поднять на примере моего репозитория.

    1. Нужно для чистоты эксперимента удалить контейнеры, образы и тома с прошлых попыток
    2. Убедиться, что на 80м порту ничего иного не висит
    3. Поднять проект следуя один-в-один инструкции из readme.md проекта с примером (https://github.com/itelmenko/myblog/tree/main/examples/vue3-laravel-docker). Инструкцию сегодня обновил пройдясь еще раз с нуля.

    Часть “Сборка frontend для production” пока выполнять не нужно. Убедимся сначала, что всё до этого работает

    Если вдруг не заведется, то нужно будет побольше данных.
    Выводы команд “docker compose ps” и “docker compose logs node” как минимум.

    1. как я понял, основная проблема было в том, что я не очень понимаю как прописать домен новый для докера, так как я везде где можно установил vue3-laravel-app.local, но не получалось, потом вместо него везде в проекте поставил localhost, а так же в конфиге vite добавил
      server: {
      hmr: {
      host: ‘localhost’
      }
      },

      работать начало, но есть две проблемы:

      1) очень долго всё грузит (возможно неправильно работаю с докером)
      2) нет быстрой перезагрузки, приходится заново контейнер запускать

      я увидел, что консультации проводите, готов даже заплатить, хочу разобрать в этом всём и развернуть свой проект нормально и работать

      1. Должно по инструкции из репозитория работать с озвученным доменом быстро и с горячей перезагрузкой.

        Менять его в /etc/hosts хост-машины в .env и конфигурационном файле nginx. Но на localhost его смысла нет менять. Удобно когда каждый проект на своем отдельном домене.

        Так как дискуссия разрастается, то напишите мне на почту или через форму обратной связи. Попробую помочь.

      2. Если на винде работаешь – то нужно чтобы проект лежал в WSL системе.
        Например по пути – \\wsl.localhost\Ubuntu-20.04\home\username\projects

  4. Это прям воспроизводится на демонстрационном проекте из статьи? Полноценный редирект? Или просто рендеринг?

    Такой проблемы не замечал. Страница при работе с Vite не перезагружается, и рендеринга повторного не происходит. Только если поменять какой-либо кусочек в коде, то мгновенно перерисовывает измененную часть страницы.

  5. от ошибки
    GET http://[::]:5173/@vite/client net::ERR_ADDRESS_INVALID
    GET http://[::]:5173/resources/css/app.css net::ERR_ADDRESS_INVALID
    GET http://[::]:5173/resources/js/app.js net::ERR_ADDRESS_INVALID
    Помогло в настройках nginx прописать
    add_header ‘Access-Control-Allow-Origin’ ‘http://vue3-laravel-app.local’ always;
    https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSMissingAllowOrigin

    1. Интересно было бы узнать какое значение данного заголовка выдавалось у Вас до этого изменения.

Добавить комментарий для Сергей Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *