Run development on the production stack with a Vite dev server

docker-compose.dev.yml extends docker-compose.yml, so development runs the
scheduler too, and takes its settings from .env, which selects the file
through COMPOSE_FILE. The dev image is a stage of the Dockerfile and shares
the production image's PHP extensions; the app listens on port 80 as in
production.

A vite service runs the dev server with hot reload. No ports are published:
OrbStack serves https://app.sealshare.orb.local and
https://vite.sealshare.orb.local, with the ports pinned by label. Without
OrbStack, docker-compose.ports.yml publishes APP_PORT and VITE_PORT on
127.0.0.1. vite.config.js takes the dev server's address from
VITE_DEV_SERVER_URL and listens on IPv4 and IPv6, as OrbStack's proxy
connects over either.

Each start installs Composer packages, clears caches, migrates and links
storage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
surtic86
2026-09-18 22:17:36 +02:00
co-authored by Claude Opus 5
parent 9bb144577b
commit 026912f98a
9 changed files with 203 additions and 91 deletions
+13 -1
View File
@@ -72,5 +72,17 @@ VITE_APP_NAME="${APP_NAME}"
# Uploads: each encrypted chunk the browser sends, in MB
# UPLOAD_CHUNK_SIZE_MB=16
# Docker (used only when deploying with docker-compose.yml)
# Docker development: `docker compose up` runs this file. On OrbStack, also set
# APP_URL=https://app.sealshare.orb.local and VITE_DEV_SERVER_URL=https://vite.sealshare.orb.local.
# Without OrbStack, append :docker-compose.ports.yml and set APP_URL=http://localhost:8000.
COMPOSE_FILE=docker-compose.dev.yml
# Where the browser reaches the Vite dev server, when not on http://localhost
# VITE_DEV_SERVER_URL=https://vite.sealshare.orb.local
# Ports on this machine: the Vite dev server's, and the app's with docker-compose.ports.yml
# VITE_PORT=5173
# APP_PORT=8000
# Docker production (docker compose -f docker-compose.yml), with AUTO_HTTPS=true
# SERVER_NAME=share.example.com
+1
View File
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- A share that reached its download limit is closed at once, but deleted by the hourly cleanup 24 hours after its last download instead of immediately, so downloads still running can finish. Until then its files still count towards the storage quota.
- The download page of a share with a download limit says how many downloads are left, or how long the recipient can still download. The admin dashboard shows downloads as "2 of 3 downloads", marks shares at their limit as "Download limit reached" and no longer counts them as active.
- The sort dropdown on the admin dashboard spans the full width of the shares card.
- Development: `docker-compose.dev.yml` now extends `docker-compose.yml`, so the dev stack runs the scheduler and the production image's PHP extensions, and takes its settings from `.env` (which selects the file through `COMPOSE_FILE`). A Vite dev server with hot reload runs beside the app. No ports are published unless `docker-compose.ports.yml` is added; with OrbStack the app is at `https://app.sealshare.orb.local`. `docker/dev.Dockerfile` became the `dev` stage of the `Dockerfile`.
## [2.1.0] - 2026-09-16
+32 -10
View File
@@ -39,20 +39,44 @@ COPY --from=vendor /app/vendor/nonameweb ./vendor/nonameweb
RUN npm run build
# ============================================
# Stage 3: Production image (FrankenPHP/Octane)
# Stage 3: PHP runtime, shared by development and production
# ============================================
FROM dunglas/frankenphp:php8.5-alpine AS production
FROM dunglas/frankenphp:php8.5-alpine AS base
LABEL maintainer="surtic86"
LABEL org.opencontainers.image.source="https://gitea.nonameweb.ch/noNameWEB/SealShare"
LABEL org.opencontainers.image.description="Self-hosted encrypted file sharing"
# Install required PHP extensions
RUN install-php-extensions \
intl \
pcntl \
zip
WORKDIR /app
# ============================================
# Stage 4: Development image (docker-compose.dev.yml)
# ============================================
# Holds only the tools: the checkout is mounted at /app, and its entrypoint runs from there.
FROM base AS dev
# For the dev packages: Pest's browser plugin needs sockets
RUN install-php-extensions sockets
# Node.js for the Vite dev server
RUN apk add --no-cache nodejs npm
# Composer: the entrypoint installs the packages on every start
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
ENTRYPOINT ["docker/dev-entrypoint.sh"]
# ============================================
# Stage 5: Production image (FrankenPHP/Octane)
# ============================================
# The last stage, so a build without --target builds this one.
FROM base AS production
LABEL maintainer="surtic86"
LABEL org.opencontainers.image.source="https://gitea.nonameweb.ch/noNameWEB/SealShare"
LABEL org.opencontainers.image.description="Self-hosted encrypted file sharing"
# Laravel environment defaults
ENV APP_NAME="SealShare" \
APP_ENV="production" \
@@ -69,8 +93,6 @@ ENV APP_NAME="SealShare" \
BCRYPT_ROUNDS="12" \
OCTANE_SERVER="frankenphp"
WORKDIR /app
# Copy PHP ini for upload limits
COPY docker/php/uploads.ini /usr/local/etc/php/conf.d/99-uploads.ini
@@ -84,7 +106,7 @@ COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
# Remove dev/build files and stale cache not needed in production
RUN rm -rf node_modules tests .gitea docker/dev.Dockerfile docker/dev-entrypoint.sh .env .env.example \
RUN rm -rf node_modules tests .gitea docker/dev-entrypoint.sh .env .env.example \
bootstrap/cache/*.php \
&& mkdir -p storage/app/shares storage/app/public storage/framework/cache \
storage/framework/sessions storage/framework/testing storage/framework/views \
+26 -6
View File
@@ -52,15 +52,35 @@ A simple, self-hosted file sharing solution built with Laravel. Upload files, ge
### Docker (recommended)
```bash
# Build and start the dev container
docker compose -f docker-compose.dev.yml up -d --build
`docker-compose.dev.yml` extends the production stack (`docker-compose.yml`, app and scheduler): the checkout mounted at `/app`, Octane reloading on PHP changes, and a Vite dev server with HMR. `.env` selects it through `COMPOSE_FILE`, so plain `docker compose` commands work.
# View logs (including Vite output)
docker compose -f docker-compose.dev.yml logs -f
```bash
cp .env.example .env
# Set APP_KEY (composer setup generates one) and the values for your setup (below)
# Build and start the app, the scheduler and Vite
docker compose up -d --build
# View logs
docker compose logs -f
```
The app is available at `http://localhost:8000` with Vite HMR on port `5173`.
With [OrbStack](https://orbstack.dev), no ports are published: set these in `.env` and open `https://app.sealshare.orb.local`. Uploads need HTTPS or `localhost`, because browsers only encrypt files there.
```dotenv
COMPOSE_FILE=docker-compose.dev.yml
APP_URL=https://app.sealshare.orb.local
VITE_DEV_SERVER_URL=https://vite.sealshare.orb.local
```
Without OrbStack, publish the ports on `127.0.0.1` and open `http://localhost:8000` (change the ports with `APP_PORT` and `VITE_PORT`):
```dotenv
COMPOSE_FILE=docker-compose.dev.yml:docker-compose.ports.yml
APP_URL=http://localhost:8000
```
The containers read `.env` when they are created: run `docker compose up -d` again after changing it.
## Installation — Production
+68 -27
View File
@@ -1,37 +1,78 @@
# ============================================
# SealShare - Development (extends docker-compose.yml)
# ============================================
#
# The app and the scheduler extend the production services and change only what development needs.
# Select this file in .env, then use plain `docker compose` commands:
# COMPOSE_FILE=docker-compose.dev.yml
#
# The checkout is mounted at /app, so changes apply without a rebuild: Octane reloads on PHP changes,
# the Vite dev server hot-reloads CSS and JavaScript and reloads the page on Blade changes. Every other
# value comes from .env through docker-compose.yml.
#
# No ports are published: OrbStack serves https://app.sealshare.orb.local and
# https://vite.sealshare.orb.local. Elsewhere, add docker-compose.ports.yml to COMPOSE_FILE.
#
# ============================================
# Only what differs between the host and the container: the host reads .env too.
x-container-environment: &container-environment
# Compiled views stay in the container. The host shares storage/ through the mount, and
# compiled Livewire components hold absolute paths (/app/… here, the checkout's path there).
VIEW_COMPILED_PATH: /tmp/views
# The database file the host uses, not the production volume's path
DB_DATABASE: /app/database/database.sqlite
services:
app:
extends:
file: docker-compose.yml
service: app
# Its own name, so a development build never tags the published image
image: sealshare-dev
build:
target: dev
ports: !reset []
volumes: !override
- .:/app
environment: *container-environment
labels:
# OrbStack's port for https://app.sealshare.orb.local, instead of detecting it (it can keep a stale one)
dev.orbstack.http-port: "80"
healthcheck:
# The first start installs Composer packages
start_period: 5m
start_interval: 2s
scheduler:
extends:
file: docker-compose.yml
service: scheduler
image: sealshare-dev
build:
target: dev
volumes: !override
- .:/app
environment: *container-environment
vite:
image: sealshare-dev
build:
context: .
dockerfile: docker/dev.Dockerfile
ports:
- "8000:8000"
- "5173:5173"
dockerfile: Dockerfile
target: dev
entrypoint: ["sh", "-c", "npm install --no-audit --no-fund && exec node_modules/.bin/vite"]
volumes:
- .:/app
# Its own node_modules: npm installs the build tools' native binaries for Linux here and for
# the host's platform there, and a shared folder only ever holds one of them.
- /app/node_modules
environment:
APP_KEY: ${APP_KEY:-}
APP_URL: http://localhost:8000
APP_ENV: local
# Compiled views stay in the container. The host shares storage/ through the mount, and
# compiled Livewire components hold absolute paths (/app/… here, the checkout's path there).
VIEW_COMPILED_PATH: /tmp/views
APP_DEBUG: "true"
SERVER_NAME: ":8000"
DB_CONNECTION: sqlite
LOG_CHANNEL: stack
LOG_LEVEL: debug
OCTANE_MAX_EXECUTION_TIME: "300"
PHP_UPLOAD_MAX_FILESIZE: "64M"
PHP_POST_MAX_SIZE: "64M"
PHP_MAX_EXECUTION_TIME: "300"
PHP_MAX_INPUT_TIME: "300"
PHP_MEMORY_LIMIT: "512M"
labels:
dev.orbstack.http-port: "${VITE_PORT:-5173}"
# The image's healthcheck asks the web server, which only the app service runs
healthcheck:
test: ["CMD", "curl", "--silent", "--fail", "http://localhost:8000/up"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 3
disable: true
depends_on:
# The stylesheet imports Livewire Material from vendor/, which the app's first start installs
app:
condition: service_healthy
+20
View File
@@ -0,0 +1,20 @@
# ============================================
# SealShare - Development ports (layered on docker-compose.dev.yml)
# ============================================
#
# Publishes the app and the Vite dev server on this machine, for Docker without OrbStack's domains:
# COMPOSE_FILE=docker-compose.dev.yml:docker-compose.ports.yml
# APP_URL=http://localhost:8000
#
# Bound to 127.0.0.1: a debug build does not belong on the network.
#
# ============================================
services:
app:
ports:
- "127.0.0.1:${APP_PORT:-8000}:80"
vite:
ports:
- "127.0.0.1:${VITE_PORT:-5173}:${VITE_PORT:-5173}"
+8 -10
View File
@@ -13,16 +13,13 @@ max_input_time = ${PHP_MAX_INPUT_TIME:-300}
memory_limit = ${PHP_MEMORY_LIMIT:-512M}
EOF
if [ ! -f vendor/autoload.php ]; then
echo "[dev] Installing PHP dependencies..."
composer install --no-interaction 2>&1
fi
# Every start, so a pull with new packages needs no extra step; with nothing new it takes a second
echo "[dev] Installing PHP dependencies..."
composer install --no-interaction 2>&1
echo "[dev] Installing Node dependencies..."
npm install 2>&1
echo "[dev] Building frontend assets..."
npm run build 2>&1
# A config or route cache left by `php artisan optimize` would hide changes to the checkout
echo "[dev] Clearing caches..."
php artisan optimize:clear
echo "[dev] Running database migrations..."
php artisan migrate --force
@@ -30,5 +27,6 @@ php artisan migrate --force
echo "[dev] Creating storage link..."
php artisan storage:link --force
# Port 80 as in production, so the same healthcheck applies. The vite service serves the assets.
echo "[dev] Starting Octane (FrankenPHP) with --watch..."
exec php artisan octane:frankenphp --host=0.0.0.0 --port=8000 --watch --workers=1 --max-requests=1
exec php artisan octane:frankenphp --host=0.0.0.0 --port=80 --watch --workers=1 --max-requests=1
-19
View File
@@ -1,19 +0,0 @@
FROM dunglas/frankenphp:php8.5-alpine
# Install required PHP extensions
RUN install-php-extensions \
intl \
pcntl
# Install Node.js for Vite / frontend asset building
RUN apk add --no-cache nodejs npm
# Composer, for a checkout without vendor/: the assets import Livewire Material from it
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY docker/dev-entrypoint.sh /usr/local/bin/dev-entrypoint.sh
RUN chmod +x /usr/local/bin/dev-entrypoint.sh
ENTRYPOINT ["dev-entrypoint.sh"]
+35 -18
View File
@@ -1,24 +1,41 @@
import {
defineConfig
defineConfig,
loadEnv
} from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
server: {
host: '0.0.0.0',
port: 5173,
cors: true,
hmr: {
host: 'localhost',
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
// Where the browser reaches the dev server when it is not http://localhost, e.g. OrbStack's
// https://vite.sealshare.orb.local. Laravel loads the assets from it, and HMR connects to it.
const devServerUrl = env.VITE_DEV_SERVER_URL ? new URL(env.VITE_DEV_SERVER_URL) : null;
const secure = devServerUrl?.protocol === 'https:';
return {
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
server: {
// IPv4 and IPv6: OrbStack's HTTPS proxy connects over either
host: true,
port: Number(env.VITE_PORT) || 5173,
cors: true,
origin: devServerUrl?.origin,
allowedHosts: devServerUrl ? [devServerUrl.hostname] : [],
hmr: devServerUrl ? {
protocol: secure ? 'wss' : 'ws',
host: devServerUrl.hostname,
clientPort: Number(devServerUrl.port) || (secure ? 443 : 80),
} : {
host: 'localhost',
},
watch: {
ignored: ['**/storage/framework/views/**'],
},
},
watch: {
ignored: ['**/storage/framework/views/**'],
},
},
};
});