Ранее здесь уже описывалась настройка docker для приложения на базе Laravel и Vue с использование поддомена. В том материале backend с Laravel размещался на поддомене, а frontend с Vue – на основном домене. Однако такая конфигурация может иметь свои недостатки (например, проблемы с CORS). В данной статье попробуем настроить для локальной работы такое же окружение, но на одном домене. Чтобы у Laravel работали одни маршруты, а у Vue – другие.
Итоговый код примера можно взять в репозитории на GitHub. Ниже в тексте статьи будут описаны все необходимые шаги для нового проекта.
Содержание статьи
- Настройка docker compose
- Установка Laravel
- Настройка Vue
- Настройка маршрутов Vue-Router
- Добавление маршрута для backend API
- Сборка frontend для production
Настройка 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-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.
Если по данной теме остались какие-либо вопросы, то отправляйте их, пожалуйста, через форму обратной связи на сайте. Возможно статья будет дополнена или Ваша тема может стать материалом следующей статьи.
Сделал сначала по гайду, не получилось, склонил репу себе, установил пакеты все, выходит та же ошибка GET http://[::]:5173/@vite/client net::ERR_ADDRESS_INVALID
Мне помогло удаление файла hot в папке public
Сергей, можете уточнить после запуска какой команды и где эта ошибка? Ошибка в браузере, верно? После клонирования какие команды выполняете? Все остальное (команды) без ошибок работает? В логе контейнера node нет ошибок связанных с запуском vite?
в логах ноды ошибок нет, ошибка в браузере, после клонирования захожу в контейнер 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
ну единственное что я сделал, только домен локальный оставил localhost, чтобы не менять в /etc/hosts ничего
если менять домен, то будет ошибка
app.css:1 Failed to load resource: net::ERR_ADDRESS_INVALID
app.js:1 Failed to load resource: net::ERR_ADDRESS_INVALID
Попробуем разобраться.
Предлагаю поднять на примере моего репозитория.
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” как минимум.
как я понял, основная проблема было в том, что я не очень понимаю как прописать домен новый для докера, так как я везде где можно установил vue3-laravel-app.local, но не получалось, потом вместо него везде в проекте поставил localhost, а так же в конфиге vite добавил
server: {
hmr: {
host: ‘localhost’
}
},
работать начало, но есть две проблемы:
1) очень долго всё грузит (возможно неправильно работаю с докером)
2) нет быстрой перезагрузки, приходится заново контейнер запускать
я увидел, что консультации проводите, готов даже заплатить, хочу разобрать в этом всём и развернуть свой проект нормально и работать
Должно по инструкции из репозитория работать с озвученным доменом быстро и с горячей перезагрузкой.
Менять его в /etc/hosts хост-машины в .env и конфигурационном файле nginx. Но на localhost его смысла нет менять. Удобно когда каждый проект на своем отдельном домене.
Так как дискуссия разрастается, то напишите мне на почту или через форму обратной связи. Попробую помочь.
Если на винде работаешь – то нужно чтобы проект лежал в WSL системе.
Например по пути – \\wsl.localhost\Ubuntu-20.04\home\username\projects
почему 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
Помогло в настройках 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
Интересно было бы узнать какое значение данного заголовка выдавалось у Вас до этого изменения.
Народ, кто юзает 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