Docker with PHP and Vite HMR
June 21, 2025
How to containerize a Laravel app with Docker, enable Vite's HMR, and create a seamless local development workflow that doesn't fight you.
What we're building
Modern Laravel apps benefit from Vite for frontend bundling and Docker for isolated development environments. The challenge is making Vite's Hot Module Reloading work across the container boundary — the browser connects to a Vite dev server running inside Docker, and without the right configuration, the WebSocket connection that powers HMR won't reach it.
This guide sets up:
- PHP 8.3 (Apache) for Laravel
- Node.js 20 in a separate container for Vite
- MariaDB and phpMyAdmin
- HMR that actually works on
localhost:5173
Project structure
/yourapp
├── docker
│ ├── node
│ │ └── Dockerfile
│ └── php
│ ├── Dockerfile
│ ├── php.ini
│ └── opcache.ini
├── docker-compose.yml
├── package.json
└── vite.config.js
Node container for Vite
A lightweight node:20 image runs the Vite dev server:
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
PHP (Laravel) container
FROM php:8.3-rc-apache-buster AS base
ENV DEBIAN_FRONTEND noninteractive
ENV TZ=UTC
ENV npm_config_cache=/tmp/.npm
ARG WWWUSER
ARG NODE_VERSION=20
RUN apt-get update && apt-get install -y \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
libtiff-dev \
libonig-dev \
libzip-dev \
libicu-dev \
unzip \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
opcache mysqli pdo_mysql gd bcmath zip intl exif \
&& curl -sL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
&& apt-get install -y nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN sed -i 's#/var/www/html#/var/www/html/public#g' /etc/apache2/sites-available/000-default.conf
COPY ./docker/php/php.ini /usr/local/etc/php/
COPY ./docker/php/opcache.ini /usr/local/etc/php/conf.d/20-opcache.ini
RUN a2enmod rewrite
FROM base AS builder
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
&& php composer-setup.php --install-dir=/usr/local/bin --filename=composer \
&& php -r "unlink('composer-setup.php');"
WORKDIR /var/www/html
ARG WWWUSER
RUN usermod -u ${WWWUSER} www-data \
&& groupmod -g ${WWWUSER} www-data
COPY --chown=www-data:www-data . .
USER www-data
Docker Compose
services:
yourapp.develop:
build:
context: .
dockerfile: ./docker/php/Dockerfile
args:
WWWUSER: ${WWWUSER}
ports:
- "${APP_PORT:-80}:80"
container_name: yourapp-app
environment:
DOCKER_BUILDKIT: 1
volumes:
- ".:/var/www/html"
- "./docker/php/php.ini:/usr/local/etc/php/php.ini"
- "./docker/php/opcache.ini:/usr/local/etc/php/conf.d/docker-php-ext-opcache.ini"
networks:
- yourapp
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- db
db:
image: mariadb:10.6.18
ports:
- "${FORWARD_DB_PORT:-3306}:3306"
environment:
MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}"
MYSQL_DATABASE: "${DB_DATABASE}"
MYSQL_USER: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
volumes:
- "yourappdb:/var/lib/mysql"
networks:
- yourapp
phpmyadmin:
image: phpmyadmin/phpmyadmin
container_name: phpmyadmin
environment:
PMA_HOST: db
PMA_PORT: 3306
MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}"
ports:
- "8080:80"
networks:
- yourapp
node:
build:
context: .
dockerfile: ./docker/node/Dockerfile
volumes:
- .:/app
working_dir: /app
ports:
- "5173:5173"
command: ["npm", "run", "dev"]
networks:
yourapp:
driver: bridge
volumes:
yourappdb:
driver: local
Running everything
- Copy
.env.exampleto.envand configure DB credentials. - Run:
docker compose up --build
Laravel is at localhost, Vite HMR at localhost:5173, phpMyAdmin at localhost:8080.
Vite configuration for HMR inside Docker
The two things Vite needs to work across the container boundary: bind to 0.0.0.0 so the container accepts connections from outside, and configure the HMR host to localhost so the browser's WebSocket connects to your host machine, not the container's internal hostname.
import { defineConfig } from "vite"
import laravel from "laravel-vite-plugin"
export default defineConfig({
server: {
host: "0.0.0.0",
port: 5173,
strictPort: true,
hmr: {
host: "localhost",
port: 5173,
protocol: "ws",
},
},
plugins: [
laravel({
input: ["resources/css/app.css", "resources/js/app.js"],
refresh: true,
}),
],
resolve: {
alias: {
"@": "/resources/js",
},
},
})
And in .env:
APP_URL=http://localhost
VITE_DEV_SERVER_URL=http://localhost:5173
With this in place, Blade views served by Laravel load JavaScript from the Vite dev server, and changes to frontend files appear in the browser without a full reload.
Final notes
- Laravel templates must use
@vitedirectives, not manual<script>tags, for HMR injection to work. - Set
APP_ENV=localandAPP_DEBUG=truein.envfor development. - The Node container only handles JS/CSS — it has no knowledge of PHP routes or application state.
References
Hi, I'm Martin Duchev. You can find more about my projects on my GitHub.