feat: Docker dev env with Caddy, PHP on host (#103)

## Summary

- Replaces Valet/Traefik with Docker-based infrastructure: Caddy (HTTPS
reverse proxy), MySQL, Redis, Mailhog
- PHP runs natively on the host via `composer run dev` — Caddy proxies
to `host.docker.internal:8000`
- PHP container available under `--profile full` for self-hosting via
`setup.sh`
- Vite dev server serves over HTTPS using mkcert certs to avoid
mixed-content issues
- `.env.example` defaults updated for host-based dev workflow (127.0.0.1
+ forwarded ports)
- Deletes obsolete `traefik/` directory

## Test plan

- [x] `docker compose up -d` starts caddy, mysql, redis, mailhog (NOT
php)
- [x] `composer run dev` serves PHP on :8000, Vite on :5173 with HTTPS
- [x] `https://whisper.money.local` loads correctly via Caddy
- [x] Vite HMR works (edit a React component, see hot reload)
- [x] `php artisan test --compact` passes
- [x] `docker compose --profile full up -d` also starts the PHP
container
This commit is contained in:
Víctor Falcón 2026-02-09 13:31:16 +01:00 committed by GitHub
parent b5fd11efaf
commit caccac6166
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 38 additions and 88 deletions

View File

@ -21,8 +21,8 @@ LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_HOST=127.0.0.1
DB_PORT=3307
DB_DATABASE=whisper_money
DB_USERNAME=root
DB_PASSWORD=
@ -42,13 +42,13 @@ CACHE_STORE=database
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_PORT=6380
MAIL_MAILER=log
MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hi@whisper.money"
@ -94,6 +94,15 @@ SENTRY_LARAVEL_DSN=
SENTRY_TRACES_SAMPLE_RATE=1.0
SENTRY_PROFILES_SAMPLE_RATE=0
# Docker Service Ports (forwarded to host)
FORWARD_DB_PORT=3307
FORWARD_REDIS_PORT=6380
FORWARD_MAILHOG_PORT=1025
FORWARD_MAILHOG_DASHBOARD_PORT=8025
# Dev Mode (set to false for full Docker deployment via setup.sh)
DEV_MODE=true
# Demo Account Configuration
DEMO_EMAIL=demo@whisper.money
DEMO_PASSWORD=demo

1
.gitignore vendored
View File

@ -27,7 +27,6 @@ yarn-error.log
/.vscode
/.zed
.php-cs-fixer.cache
/traefik/certs/*.pem
/docker/caddy/certs/*.pem
/docker/caddy/certs/*-key.pem
.claude/settings.local.json

View File

@ -3,7 +3,7 @@ whisper.money.local {
# These certificates are automatically trusted by browsers
tls /etc/caddy/certs/whisper.money.local.pem /etc/caddy/certs/whisper.money.local-key.pem
reverse_proxy php:8000
reverse_proxy host.docker.internal:8000
header {
-Server

View File

@ -62,6 +62,8 @@ services:
networks:
- sail
php:
profiles:
- full
build:
context: .
dockerfile: Dockerfile

View File

@ -55,7 +55,7 @@
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --queue=default,emails\" \"php artisan pail --timeout=0\" \"bun run dev\" \"stripe listen --forward-to https://whispermoney.test/stripe/webhook\" --names=server,queue,logs,vite --kill-others"
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --queue=default,emails\" \"php artisan pail --timeout=0\" \"bun run dev\" \"stripe listen --forward-to https://whisper.money.local/stripe/webhook\" --names=server,queue,logs,vite --kill-others"
],
"dev:ssr": [
"bun run build:ssr",

View File

@ -790,7 +790,7 @@ start_services() {
fi
echo -e "${BLUE}Starting Docker services...${NC}"
docker compose up -d
docker compose --profile full up -d
wait_for_service "mysql"
wait_for_service "redis"
@ -939,7 +939,7 @@ install() {
# Now start PHP service (after dependencies are installed)
echo -e "${BLUE}Starting PHP service...${NC}"
if ! docker compose up -d php; then
if ! docker compose --profile full up -d php; then
echo -e "${RED}Failed to start PHP service!${NC}"
echo -e "${YELLOW}Checking logs...${NC}"
docker compose logs php 2>&1 | tail -20
@ -1217,7 +1217,7 @@ upgrade() {
# Restart services
echo -e "${BLUE}Restarting services...${NC}"
docker compose restart
docker compose --profile full restart
echo -e "${GREEN}Services restarted.${NC}"
echo ""

View File

@ -19,8 +19,8 @@ import {
import { useEncryptionKey } from '@/contexts/encryption-key-context';
import { useLocale } from '@/hooks/use-locale';
import { transactionSyncService } from '@/services/transaction-sync';
import { type Transaction } from '@/types/transaction';
import { type ParsedTransaction } from '@/types/import';
import { type Transaction } from '@/types/transaction';
import { formatDateMedium } from '@/utils/date';
import { __ } from '@/utils/i18n';
import { ChevronDown } from 'lucide-react';

View File

View File

@ -1,41 +0,0 @@
http:
routers:
whispermoney-http:
rule: Host(`whispermoney.test`)
entryPoints:
- web
middlewares:
- redirect-to-https
service: whispermoney
whispermoney-https:
rule: Host(`whispermoney.test`)
entryPoints:
- websecure
service: whispermoney
middlewares:
- headers-secure
tls: {}
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
permanent: true
headers-secure:
headers:
customRequestHeaders:
X-Forwarded-Proto: https
customResponseHeaders:
X-Forwarded-Proto: https
services:
whispermoney:
loadBalancer:
servers:
- url: http://host.docker.internal:8000
passHostHeader: true
tls:
certificates:
- certFile: /etc/traefik/certs/whispermoney.test.pem
keyFile: /etc/traefik/certs/whispermoney.test-key.pem

View File

@ -1,34 +0,0 @@
#!/bin/bash
set -e
if ! command -v mkcert &> /dev/null; then
echo "mkcert is not installed. Installing..."
if [[ "$OSTYPE" == "darwin"* ]]; then
if command -v brew &> /dev/null; then
brew install mkcert
brew install nss
else
echo "Please install Homebrew first, then run: brew install mkcert nss"
exit 1
fi
else
echo "Please install mkcert manually. Visit: https://github.com/FiloSottile/mkcert"
exit 1
fi
fi
if [ ! -f "$(mkcert -CAROOT)/rootCA.pem" ]; then
echo "Installing local CA..."
mkcert -install
fi
echo "Generating certificates for whispermoney.test..."
cd "$(dirname "$0")/certs"
mkcert -key-file whispermoney.test-key.pem -cert-file whispermoney.test.pem whispermoney.test
echo "Certificates generated successfully!"
echo "Files created:"
echo " - traefik/certs/whispermoney.test.pem"
echo " - traefik/certs/whispermoney.test-key.pem"

View File

@ -1,3 +1,6 @@
import fs from 'fs';
import path from 'path';
import { wayfinder } from '@laravel/vite-plugin-wayfinder';
import { sentryVitePlugin } from '@sentry/vite-plugin';
import tailwindcss from '@tailwindcss/vite';
@ -9,6 +12,18 @@ export default defineConfig({
build: {
sourcemap: true,
},
server: (() => {
const certPath = path.resolve(__dirname, 'docker/caddy/certs/whisper.money.local.pem');
const keyPath = path.resolve(__dirname, 'docker/caddy/certs/whisper.money.local-key.pem');
if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
return {
https: { cert: certPath, key: keyPath },
host: '0.0.0.0',
hmr: { host: 'whisper.money.local' },
};
}
return {};
})(),
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.tsx'],