fix(dev): support root bind mounts and LAN Vite access

Keep the dev container as root for s6 init so composer can create
vendor/ on root-owned mounts, then chown writable paths to www-data.
Move init-setup into a shell script and expose VITE_HOST/PORT for
remote HMR (LAN/Tailscale) with Vite listening on 0.0.0.0.
This commit is contained in:
Andras Bacsai 2026-07-18 20:56:01 +02:00
parent 913d033c75
commit e2c2180f4f
20 changed files with 80 additions and 44 deletions

View file

@ -30,6 +30,14 @@ DB_PORT=5432
# Enable Laravel Telescope for debugging
TELESCOPE_ENABLED=false
# Enable Laravel Debugbar (disabled by default; set true when needed)
DEBUGBAR_ENABLED=false
# Vite dev server. Defaults to localhost. For phone/LAN/Tailscale access, set to
# the host machine's reachable IP (e.g. VITE_HOST=100.75.155.70), then recreate vite.
VITE_HOST=localhost
VITE_PORT=5173
# Enable Laravel Nightwatch monitoring
NIGHTWATCH_ENABLED=false
NIGHTWATCH_TOKEN=

0
bootstrap/cache/.gitignore vendored Normal file → Executable file
View file

View file

@ -96,6 +96,7 @@ services:
container_name: coolify-vite
working_dir: /var/www/html
environment:
# Set VITE_HOST in .env to a browser-reachable IP/hostname for LAN/Tailscale access
VITE_HOST: "${VITE_HOST:-localhost}"
VITE_PORT: "${VITE_PORT:-5173}"
ports:

View file

@ -96,6 +96,7 @@ services:
container_name: coolify-vite
working_dir: /var/www/html
environment:
# Set VITE_HOST in .env to a browser-reachable IP/hostname for LAN/Tailscale access
VITE_HOST: "${VITE_HOST:-localhost}"
VITE_PORT: "${VITE_PORT:-5173}"
ports:

View file

@ -98,5 +98,8 @@ RUN mkdir -p /etc/nginx/conf.d && \
COPY --from=minio-client /usr/bin/mc /usr/bin/mc
RUN chmod +x /usr/bin/mc
# Switch to non-root user
USER www-data
# Stay as root for s6 init so bind-mounted workspaces work even when the host
# tree is root-owned (CI, Jean, rootful Docker). PHP-FPM/nginx still run as
# www-data via their service configs. Do NOT set USER www-data here — that
# makes init-setup (composer install) fail with "vendor could not be created".
USER root

22
docker/development/etc/s6-overlay/s6-rc.d/init-setup/up Normal file → Executable file
View file

@ -1,22 +1,6 @@
#!/command/execlineb -P
# Use with-contenv to ensure environment variables are available
# s6 oneshots are execline pipelines (shebang shell scripts are not run as sh).
# Delegate to a real shell script for readable setup logic.
with-contenv
cd /var/www/html
foreground {
composer
install
}
foreground {
php
artisan
migrate
--step
}
foreground {
php
artisan
dev
--init
}
/etc/s6-overlay/scripts/init-setup.sh

View file

@ -0,0 +1,42 @@
#!/command/with-contenv sh
set -eu
cd /var/www/html
# When the project is bind-mounted, host ownership may be root:root while the
# app runs as www-data (UID 1000). Fix the minimum needed so composer/laravel
# can write, without a recursive chown of the whole tree.
prepare_bind_mount() {
mkdir -p \
storage/framework/cache \
storage/framework/sessions \
storage/framework/views \
storage/logs \
bootstrap/cache
if [ "$(id -u)" = "0" ]; then
# Top-level only: allows creating vendor/ when the mount is root-owned
chown www-data:www-data /var/www/html 2>/dev/null || true
chown -R www-data:www-data storage bootstrap/cache 2>/dev/null || true
git config --global --add safe.directory /var/www/html 2>/dev/null || true
fi
}
prepare_bind_mount
echo "👉 init-setup: composer install..."
# Run as root when needed so a root-owned bind mount cannot block vendor/
# creation, then hand writable paths to www-data for PHP-FPM.
composer install --no-interaction
echo "👉 init-setup: migrate..."
php artisan migrate --step --force
echo "👉 init-setup: artisan dev --init..."
php artisan dev --init
if [ "$(id -u)" = "0" ]; then
chown -R www-data:www-data vendor storage bootstrap/cache 2>/dev/null || true
fi
echo "👉 init-setup: done"

View file

@ -1,8 +1,8 @@
{
"scripts": {
"setup": "cp $JEAN_ROOT_PATH/.env . && mkdir -p .claude && cp $JEAN_ROOT_PATH/.claude/settings.local.json .claude/settings.local.json",
"setup": "cp $JEAN_ROOT_PATH/.env .",
"teardown": null,
"run": "docker rm -f coolify coolify-minio-init coolify-realtime coolify-minio coolify-testing-host coolify-redis coolify-db coolify-mail coolify-vite; spin up; spin down"
"run": "docker rm -f coolify coolify-minio-init coolify-realtime coolify-minio coolify-testing-host coolify-redis coolify-db coolify-mail coolify-vite; docker compose -f docker-compose.yml -f docker-compose.dev.yml up; docker compose -f docker-compose.yml -f docker-compose.dev.yml down"
},
"ports": [
{

View file

@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "vite build"
},
"devDependencies": {

0
storage/app/.gitignore vendored Normal file → Executable file
View file

0
storage/app/public/.gitignore vendored Normal file → Executable file
View file

0
storage/debugbar/.gitignore vendored Normal file → Executable file
View file

0
storage/framework/.gitignore vendored Normal file → Executable file
View file

0
storage/framework/cache/.gitignore vendored Normal file → Executable file
View file

0
storage/framework/sessions/.gitignore vendored Normal file → Executable file
View file

0
storage/framework/testing/.gitignore vendored Normal file → Executable file
View file

0
storage/framework/views/.gitignore vendored Normal file → Executable file
View file

0
storage/logs/.gitignore vendored Normal file → Executable file
View file

0
storage/pail/.gitignore vendored Normal file → Executable file
View file

View file

@ -2,33 +2,30 @@ import { defineConfig, loadEnv } from "vite";
import laravel from "laravel-vite-plugin";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const viteHost = env.VITE_HOST || null;
const vitePort = Number(env.VITE_PORT || 5173);
const env = loadEnv(mode, process.cwd(), "");
// Prefer process.env so Docker Compose can override without writing .env.
// Set VITE_HOST to a browser-reachable hostname/IP when accessing the app
// from another device (LAN / Tailscale), e.g. VITE_HOST=100.75.155.70
const viteHost = (process.env.VITE_HOST || env.VITE_HOST || "localhost").trim();
const vitePort = Number(process.env.VITE_PORT || env.VITE_PORT || 5173);
return {
server: {
watch: {
ignored: [
"**/dev_*_data/**",
"**/storage/**",
],
ignored: ["**/dev_*_data/**", "**/storage/**"],
},
// Listen on all interfaces so Docker / remote clients can reach the dev server
host: "0.0.0.0",
port: vitePort,
strictPort: true,
allowedHosts: true,
cors: {
origin: [
/^https?:\/\/localhost(:\d+)?$/,
/^https?:\/\/127\.0\.0\.1(:\d+)?$/,
/^https?:\/\/\[::1\](:\d+)?$/,
...(env.APP_URL ? [env.APP_URL] : []),
...(viteHost ? [`http://${viteHost}:${vitePort}`, `https://${viteHost}:${vitePort}`] : []),
],
// App (:8000) and Vite (:5173) are different origins; allow any host in dev
cors: true,
origin: `http://${viteHost}:${vitePort}`,
hmr: {
host: viteHost,
clientPort: vitePort,
},
origin: viteHost ? `http://${viteHost}:${vitePort}` : undefined,
hmr: viteHost
? { host: viteHost, clientPort: vitePort }
: true,
},
plugins: [
laravel({
@ -36,5 +33,5 @@ export default defineConfig(({ mode }) => {
refresh: true,
}),
],
}
};
});