Set up the Weslink local development environment with shared Docker infrastructure (Traefik, PostgreSQL, Redis, MeiliSearch, Mailpit, MinIO), native PHP-FPM + nginx, HTTPS via mkcert, and shell aliases. Use when a developer says 'setup local dev', 'einrichten', 'install infra', or 'ich will das lokale Setup'.
This skill guides the installation of the complete local development stack. Read project config from references/projects.json using the Read tool.
Browser (HTTPS)
-> Traefik (Docker, SSL) :443
-> nginx (local) :8000-8003 -> PHP-FPM (local) :9090
-> soketi (Docker) :6001 # WebSockets (kibi only)
-> livekit (Docker) :7880 # Video Calls (kibi only)
Shared Docker Services:
PostgreSQL :5432 | Redis :6379 | MeiliSearch :7700
Mailpit :8025 | MinIO :9000 | Traefik Dashboard :8080
Execute each step, verify it works, then move on. Ask the user before running destructive commands.
brew install [email protected] nginx dnsmasq mkcert
After installation, link [email protected] as the active PHP version:
brew unlink php && brew link [email protected] --force
Check first:
which valet 2>/dev/null && echo "Valet installed" || echo "No Valet"
If installed:
valet stop 2>/dev/null
composer global remove laravel/valet
sudo brew services stop nginx
sudo brew services stop php
sudo killall php-fpm 2>/dev/null
sudo killall nginx 2>/dev/null
Also check for Laravel Herd (blocks port 443, conflicts with Traefik):
# If Herd is installed, uninstall via: Herd app -> Settings -> Uninstall
mkcert -install
Only needed once. Installs the local CA in the system keychain.
mkdir -p $(brew --prefix)/etc/dnsmasq.d
echo "address=/.test/127.0.0.1" > $(brew --prefix)/etc/dnsmasq.d/test.conf
echo "listen-address=127.0.0.1" >> $(brew --prefix)/etc/dnsmasq.d/test.conf
grep -q "conf-dir=$(brew --prefix)/etc/dnsmasq.d" $(brew --prefix)/etc/dnsmasq.conf 2>/dev/null || \
echo "conf-dir=$(brew --prefix)/etc/dnsmasq.d/,*.conf" >> $(brew --prefix)/etc/dnsmasq.conf
sudo mkdir -p /etc/resolver
echo "nameserver 127.0.0.1" | sudo tee /etc/resolver/test
sudo brew services start dnsmasq
Verify: dig demo.kibi.test @127.0.0.1 +short should return 127.0.0.1.
Create ~/dev/infrastructure/ with the following structure. Read references/projects.json for project details and ports.
Files to create:
~/dev/infrastructure/docker-compose.yml - All shared services (Traefik, PostgreSQL, Redis, MeiliSearch, Mailpit, MinIO, Infra Dashboard) on the shared-infra Docker network~/dev/infrastructure/pgsql/init/01-create-databases.sh - Creates all project databases on first PostgreSQL start~/dev/infrastructure/traefik/dynamic/tls.yml - Points to the wildcard TLS certificate~/dev/infrastructure/traefik/dynamic/default.yml - Catch-all route to the infra-dashboard container (priority 1), shows a dashboard for any unknown .test domain~/dev/infrastructure/traefik/dynamic/<project>.yml - One routing config per project (routes domain to host.docker.internal:<port>)~/dev/infrastructure/dashboard/index.html - Static catch-all dashboard page (displays available projects, active worktrees via Traefik API, and service links)~/dev/infrastructure/bin/infra - CLI helper script (up, down, status, logs, cert-regen)~/dev/infrastructure/bin/wt - Global worktree management scriptKey details:
pgvector/pgvector:pg16 (superset, works for all projects)shared-infra Docker networktraefik/dynamic/ directory)infra and wt scripts must be chmod +xGenerate TLS certificates:
cd ~/dev/infrastructure/certs
mkcert -cert-file _wildcard.test.pem -key-file _wildcard.test-key.pem \
"*.test" \
"kibi.test" "*.kibi.test" \
"bessler.test" "*.bessler.test" \
"scada.test" "*.scada.test" \
"contradoo.test" "*.contradoo.test"
Traefik routing for projects with tenant subdomains (kibi):
HostRegexp + PathPrefix(/app) -> soketi:6001 (priority 100)HostRegexp + PathPrefix(/storage/minio) -> minio:9000 with path rewrite (priority 100)Host(livekit.kibi.test) -> livekit:7880HostRegexp(^(.+\.)?kibi\.test$) -> host.docker.internal:8000 (priority 10)Traefik routing for simple projects (bessler, scada, contradoo):
Host(project.test) -> host.docker.internal:<port>6.1 Disable Xdebug (3-5x performance impact):
mv /opt/homebrew/etc/php/8.4/conf.d/20-xdebug.ini \
/opt/homebrew/etc/php/8.4/conf.d/20-xdebug.ini.disabled 2>/dev/null
6.2 Disable Valet FPM pool (if exists):
mv /opt/homebrew/etc/php/8.4/php-fpm.d/valet-fpm.conf \
/opt/homebrew/etc/php/8.4/php-fpm.d/valet-fpm.conf.disabled 2>/dev/null
6.3 Install all required PHP extensions:
The Docker containers use these extensions: pgsql, sqlite3, gd, curl, imap, mbstring, xml, zip, bcmath, soap, intl, readline, ldap, msgpack, igbinary, redis, memcached, pcov, imagick. Most are included with [email protected] via Homebrew. Missing ones need pecl:
pecl install redis igbinary msgpack
memcached needs a manual build:
cd /tmp && pecl download memcached && tar xzf memcached-*.tgz && cd memcached-*/
phpize && ./configure --with-php-config=/opt/homebrew/opt/[email protected]/bin/php-config \
--with-zlib-dir=/opt/homebrew/opt/zlib --with-libmemcached-dir=/opt/homebrew \
--enable-memcached-igbinary=yes --enable-memcached-msgpack=yes \
--enable-memcached-json=yes --enable-memcached-session=yes --enable-memcached-sasl=yes
make -j$(sysctl -n hw.ncpu) && make install
echo "extension=memcached.so" > /opt/homebrew/etc/php/8.4/conf.d/20-memcached.ini
Note: imap and swoole are difficult to compile on macOS but are not actually used by any project. Skip them.
6.3b Disable pcov by default (enables JIT):
mv /opt/homebrew/etc/php/8.4/conf.d/20-pcov.ini /opt/homebrew/etc/php/8.4/conf.d/20-pcov.ini.disabled
pcov can be toggled on/off with the pcov:on / pcov:off shell aliases (see Step 11).
6.4 Create performance config at /opt/homebrew/etc/php/8.4/conf.d/99-performance.ini:
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000
opcache.revalidate_freq=0
opcache.validate_timestamps=1
opcache.enable_file_override=1
opcache.jit=1255
opcache.jit_buffer_size=128M
realpath_cache_size=4096K
realpath_cache_ttl=600
memory_limit=512M
6.5 Change FPM listen port (9000 is used by MinIO):
sed -i '' 's/listen = 127.0.0.1:9000/listen = 127.0.0.1:9090/' \
/opt/homebrew/etc/php/8.4/php-fpm.d/www.conf
6.6 Fix FPM user:
sed -i '' "s/^user = _www/user = $(whoami)/" /opt/homebrew/etc/php/8.4/php-fpm.d/www.conf
sed -i '' "s/^group = _www/group = staff/" /opt/homebrew/etc/php/8.4/php-fpm.d/www.conf
6.7 Start PHP-FPM:
/opt/homebrew/opt/[email protected]/sbin/php-fpm \
--daemonize \
--fpm-config /opt/homebrew/etc/php/8.4/php-fpm.conf
Verify: lsof -i :9090 -P | head -3 should show php-fpm.
7.1 Replace nginx.conf at /opt/homebrew/etc/nginx/nginx.conf:
error_log /tmp/nginx-error.log and pid /tmp/nginx.pid (avoids permission issues)access_log /tmp/nginx-access.log in the http blockuser directiveinclude servers/*7.2 Create server configs in /opt/homebrew/etc/nginx/servers/:
One config per project. Each listens on its port, serves static files directly, proxies PHP to 127.0.0.1:9090 via FastCGI.
Important FastCGI params to include:
fastcgi_param HTTPS "on";
fastcgi_param HTTP_X_FORWARDED_PROTO "https";
7.3 Start nginx:
nginx -t && brew services start nginx
Update each project's .env:
APP_URL=https://<domain>
DB_HOST=127.0.0.1
DB_DATABASE=<project_db_name>
DB_USERNAME=sail
DB_PASSWORD=password
REDIS_HOST=127.0.0.1
MAIL_HOST=127.0.0.1
MEILISEARCH_HOST=http://127.0.0.1:7700
Kibi needs Soketi and LiveKit in Docker. Create docker-compose.override.yml (gitignored) that:
busybox:latest no-ops (entrypoint: ["true"], ports: !override [], volumes: !override [])shared-infra networkinfra-redis# Start infrastructure
~/dev/infrastructure/bin/infra up
# Start kibi Docker services
cd ~/dev/projects/kibi-connect/kibi && ./vendor/bin/sail up -d
# Migrate (first time only)
php artisan migrate --seed
php artisan kibi:create-demo-tenant
# Build caches for performance
php -d memory_limit=512M artisan optimize
# Test
curl -sk -o /dev/null -w "%{http_code} %{time_total}s\n" https://demo.kibi.test/login
# Expected: 200 ~0.09s
Add to .zshrc or .aliases:
export PATH="$HOME/dev/infrastructure/bin:$PATH" for infra and wt commandskibi, bessler, scada, ctr)dev / fast / fresh functions for toggling OPcache and Laravel cachespcov:on / pcov:off functions for toggling pcov extension (pcov blocks JIT)kibi:up / kibi:down for complete start/stop including Horizon and Schedulerwt:create, wt:remove, wt:list, wt:run aliases for worktree managementAfter setup, all of these should work:
dig demo.kibi.test @127.0.0.1 +short returns 127.0.0.1curl -sk https://demo.kibi.test/login returns HTTP 200curl -sk https://kibi.test/admin returns HTTP 200 or 302curl -sk https://bessler.test/ returns HTTP 200 or 302curl -sk https://contradoo.test/ returns HTTP 200 or 302http://localhost:8080 shows all routershttp://localhost:8025 is accessiblephp -m | grep redis shows redis extension is loadedpsql -h 127.0.0.1 -U sail -d kibi -c "SELECT 1" works