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

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

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

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

Настройка docker compose

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

/etc/hosts
127.0.0.1 vue3-laravel-app.local

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

docker-compose.yml
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:

docker/backend/Dockerfile
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:

docker/nginx/conf.d/app.conf
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:

docker/nodejs/Dockerfile
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

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:

resources/js/app.js
import './bootstrap'; import {createApp} from 'vue' import App from './App.vue' createApp(App).mount("#app")
resources/js/App.vue
<template> Vue 3 Component </template>

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

app.blade.php
<!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>

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

routes/web.php
<?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'

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

resources/js/router.js
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;

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

resources/js/views/Home.vue
<template> <h1>Home Page</h1> </template>
resources/js/views/About.vue
<template> <h1>About Page</h1> </template>
resources/js/views/Error404.vue
<template> <h1>Error 404. Page not found</h1> </template>

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

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") // <- И здесь

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

resources/js/App.vue
<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 будет получать какие-либо данные асинхронно. На главной странице у нас будет кнопка, по нажатию на которую произойдет запрос к серверу и отображение полученных данных.

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

resources/js/views/Home.vue
<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:

routes/api.php
<?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-странице были строки:

Подключение js и css
... <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 вариантом. Если мы хотим посмотреть как будет работать приложение с собранными файлами нам необходимо выполнить следующие команды:

Переключаемся на production версию frontend
docker-compose stop node rm public/hot

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

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

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

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

Запуск контейнера node
docker-compose start node

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

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

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

5 1 голос
Рейтинг статьи
Подписаться
Уведомить о
guest


15 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Сергей
Гость
Сергей
2 лет назад

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

Vladimir
Гость
Vladimir
2 лет назад
Ответить на  Сергей

Мне помогло удаление файла hot в папке public

Сергей
Гость
Сергей
2 лет назад
Ответить на  Игорь Тельменко

в логах ноды ошибок нет, ошибка в браузере, после клонирования захожу в контейнер 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

Сергей
Гость
Сергей
2 лет назад
Ответить на  Сергей

ну единственное что я сделал, только домен локальный оставил 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

Сергей
Гость
Сергей
2 лет назад
Ответить на  Игорь Тельменко

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

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

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

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

Grigo
Гость
2 лет назад
Ответить на  Сергей

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

Азамат
Гость
Азамат
1 год назад

почему Vite перегружает страницу каждую секунду?
С чем это связано?

Александр
Гость
Александр
1 год назад

от ошибки
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

Alex
Гость
Alex
6 месяцев назад

Народ, кто юзает WSL2 под Windows. Чтобы был виден код из контейнера Node Js нужно в vite.config.js добавить ключ server. Как-то так:

export default defineConfig({
  plugins: [

    laravel({
      input: [‘resources/css/app.css’, ‘resources/js/app.js’],
      refresh: true,
    }),
    vue({
      template: {
        transformAssetUrls: {
              base: null,
           includeAbsolute: false,
        }
      }
    }),

  ],
  server: { // [tl! add:start]
    hmr: {
      host: ‘localhost’,
    },
  }, // [tl! add:end]
});
Все это описано в официальной документации по Laravel https://laravel.com/docs/11.x/vite#configuring-hmr-in-sail-on-wsl2