Настройка 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.

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

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

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

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

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

Сергей
Гость
Сергей
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 ничего

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

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

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

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

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

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

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

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

Если на винде работаешь – то нужно чтобы проект лежал в 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
19 дней назад

Народ, кто юзает 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