add nix development environment

Provide a direnv-loaded Laravel and future Vue toolchain with isolated worktree ports and a process-compose stack. Use PostgreSQL for local runtime services while retaining in-memory SQLite for PHPUnit.
This commit is contained in:
Yisroel Baum 2026-07-30 22:54:20 +03:00
parent 2d25c28596
commit 83ca2cf43c
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
12 changed files with 345 additions and 13 deletions

11
.envrc Normal file
View file

@ -0,0 +1,11 @@
# Load the flake environment
use flake
onefetch
# Use PHP and Node layouts
layout php
layout node
# Reload when backend dependencies change
watch_file backend/composer.json

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/.cert
/.direnv/
/.mailpit.log
/.postgres/
/.worktrees/

17
Caddyfile Normal file
View file

@ -0,0 +1,17 @@
{
auto_https off
admin off
}
localhost:{$CADDY_PORT:8000}, 127.0.0.1:{$CADDY_PORT:8000} {
tls .cert/localhost.pem .cert/localhost-key.pem
handle /storage/* {
root * backend/public
file_server
}
handle {
reverse_proxy 127.0.0.1:{$BACKEND_PORT:8001}
}
}

View file

@ -2,7 +2,7 @@ APP_NAME=Laravel
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://localhost:8000 APP_URL=https://localhost:8000
APP_LOCALE=en APP_LOCALE=en
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en
@ -20,12 +20,12 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=sqlite DB_CONNECTION=pgsql
# DB_HOST=127.0.0.1 DB_HOST="${PGHOST}"
# DB_PORT=3306 DB_PORT=5432
# DB_DATABASE=laravel DB_DATABASE=postgres
# DB_USERNAME=root DB_USERNAME=postgres
# DB_PASSWORD= DB_PASSWORD=
SESSION_DRIVER=database SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120
@ -47,7 +47,7 @@ REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_MAILER=log MAIL_MAILER=smtp
MAIL_SCHEME=null MAIL_SCHEME=null
MAIL_HOST=127.0.0.1 MAIL_HOST=127.0.0.1
MAIL_PORT=2525 MAIL_PORT=2525

View file

@ -81,7 +81,6 @@
], ],
"post-create-project-cmd": [ "post-create-project-cmd": [
"@php artisan key:generate --ansi", "@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi" "@php artisan migrate --graceful --ansi"
], ],
"pre-package-uninstall": [ "pre-package-uninstall": [

View file

@ -17,7 +17,7 @@ return [
| |
*/ */
'default' => env('DB_CONNECTION', 'sqlite'), 'default' => env('DB_CONNECTION', 'pgsql'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View file

@ -103,7 +103,7 @@ return [
*/ */
'batching' => [ 'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'), 'database' => env('DB_CONNECTION', 'pgsql'),
'table' => 'job_batches', 'table' => 'job_batches',
], ],
@ -122,7 +122,7 @@ return [
'failed' => [ 'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'), 'database' => env('DB_CONNECTION', 'pgsql'),
'table' => 'failed_jobs', 'table' => 'failed_jobs',
], ],

View file

@ -1 +0,0 @@
*.sqlite*

61
flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1785318670,
"narHash": "sha256-dN6Ou5x/+23FZLEpYP3IffO+NyJFzUlGumt1uu3MMaY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0954f7ee2f6bb3dc7d4e3d0d8bcb8fd4bde4cfc5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

54
flake.nix Normal file
View file

@ -0,0 +1,54 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
utils,
}:
utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
php = pkgs.php.buildEnv {
extraConfig = ''
memory_limit = "1G";
upload_max_filesize = "10M";
post_max_size = "25M";
'';
};
in
{
formatter = pkgs.nixfmt-tree;
devShells.default = pkgs.mkShell {
packages = with pkgs; [
bash
onefetch
just
php
phpPackages.composer
phpPackages.php-codesniffer
vscode-langservers-extracted
nodejs
nixfmt
nixfmt-tree
cypress
yaml-language-server
typescript
postgresql
mailpit
process-compose
mkcert
caddy
];
shellHook = builtins.readFile ./nix/shell-hook.sh;
};
}
);
}

127
nix/shell-hook.sh Normal file
View file

@ -0,0 +1,127 @@
#!/usr/bin/env bash
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
GIT_DIR_PATH="$(git rev-parse --git-dir 2>/dev/null)"
GIT_COMMON_PATH="$(git rev-parse --git-common-dir 2>/dev/null)"
# Linked worktrees get their own runtime state and port range.
if [ -n "$GIT_COMMON_PATH" ] \
&& [ "$GIT_DIR_PATH" != "$GIT_COMMON_PATH" ]; then
IS_WORKTREE=1
MAIN_REPO_ROOT="$(cd "$GIT_COMMON_PATH/.." && pwd)"
else
IS_WORKTREE=0
MAIN_REPO_ROOT="$REPO_ROOT"
fi
if [ "$IS_WORKTREE" = 1 ]; then
if [ -n "$PORT_OFFSET" ]; then
OFFSET="$PORT_OFFSET"
else
PORT_HASH="$(printf '%s' "$REPO_ROOT" | cksum | cut -d' ' -f1)"
OFFSET=$(( (PORT_HASH % 49 + 1) * 100 ))
fi
else
OFFSET=0
fi
export CADDY_PORT=$((8000 + OFFSET))
export BACKEND_PORT=$((8001 + OFFSET))
export VITE_PORT=$((5173 + OFFSET))
export MAILPIT_SMTP_PORT=$((2525 + OFFSET))
export MAILPIT_UI_PORT=$((8025 + OFFSET))
export PC_PORT_NUM=$((8080 + OFFSET))
export PGDATA="$REPO_ROOT/.postgres"
export PGHOST="$PGDATA"
export PGUSER="postgres"
export PGDATABASE="postgres"
DEV_APP_URL="https://localhost:$CADDY_PORT"
DEV_DB_CONNECTION="pgsql"
DEV_DB_HOST="$PGHOST"
DEV_DB_PORT="5432"
DEV_DB_DATABASE="$PGDATABASE"
DEV_DB_USERNAME="$PGUSER"
DEV_DB_PASSWORD=""
DEV_MAIL_MAILER="smtp"
DEV_MAIL_HOST="127.0.0.1"
DEV_MAIL_PORT="$MAILPIT_SMTP_PORT"
if [ ! -d "$PGDATA" ]; then
echo "[pg] initializing cluster at $PGDATA"
initdb --auth=trust --username="$PGUSER" --no-locale --encoding=UTF8 >/dev/null
{
echo "listen_addresses = ''"
echo "unix_socket_directories = '$PGDATA'"
} >> "$PGDATA/postgresql.conf"
fi
# Worktrees share host-independent localhost certificates with the main checkout.
MAIN_CERT_ROOT="$MAIN_REPO_ROOT/.cert"
if [ ! -d "$MAIN_CERT_ROOT" ]; then
echo "[cert] installing local CA + generating TLS certs in $MAIN_CERT_ROOT"
mkcert -install
mkdir -p "$MAIN_CERT_ROOT"
mkcert \
-key-file "$MAIN_CERT_ROOT/localhost-key.pem" \
-cert-file "$MAIN_CERT_ROOT/localhost.pem" \
localhost 127.0.0.1 ::1
fi
if [ "$IS_WORKTREE" = 1 ] \
&& [ ! -e "$REPO_ROOT/.cert" ] \
&& [ ! -L "$REPO_ROOT/.cert" ]; then
ln -s "$MAIN_CERT_ROOT" "$REPO_ROOT/.cert"
fi
ENV_FILE="$REPO_ROOT/backend/.env"
if [ ! -f "$ENV_FILE" ]; then
if [ "$IS_WORKTREE" = 1 ] && [ -f "$MAIN_REPO_ROOT/backend/.env" ]; then
cp "$MAIN_REPO_ROOT/backend/.env" "$ENV_FILE"
else
cp "$REPO_ROOT/backend/.env.example" "$ENV_FILE"
fi
fi
set_env_value() {
local env_key="$1"
local env_value="$2"
local escaped_value
escaped_value="$(printf '%s' "$env_value" | sed 's/[&|\\]/\\&/g')"
if grep -q "^${env_key}=" "$ENV_FILE"; then
sed -i "s|^${env_key}=.*|${env_key}=${escaped_value}|" "$ENV_FILE"
else
printf '%s=%s\n' "$env_key" "$env_value" >> "$ENV_FILE"
fi
}
set_env_value APP_URL "$DEV_APP_URL"
set_env_value DB_CONNECTION "$DEV_DB_CONNECTION"
set_env_value DB_HOST "$DEV_DB_HOST"
set_env_value DB_PORT "$DEV_DB_PORT"
set_env_value DB_DATABASE "$DEV_DB_DATABASE"
set_env_value DB_USERNAME "$DEV_DB_USERNAME"
set_env_value DB_PASSWORD "$DEV_DB_PASSWORD"
set_env_value MAIL_MAILER "$DEV_MAIL_MAILER"
set_env_value MAIL_HOST "$DEV_MAIL_HOST"
set_env_value MAIL_PORT "$DEV_MAIL_PORT"
if [ ! -d "$REPO_ROOT/backend/vendor" ]; then
echo "[composer] installing backend dependencies"
(cd "$REPO_ROOT/backend" && composer install)
fi
if grep -q '^APP_KEY=$' "$ENV_FILE"; then
echo "[laravel] generating application key"
(cd "$REPO_ROOT/backend" && php artisan key:generate)
fi
if [ ! -e "$REPO_ROOT/backend/public/storage" ]; then
echo "[laravel] linking public storage"
(cd "$REPO_ROOT/backend" && php artisan storage:link)
fi
echo "[dev] run 'process-compose up' to start postgres + mailpit + backend + caddy"

59
process-compose.yaml Normal file
View file

@ -0,0 +1,59 @@
version: "0.5"
processes:
postgres:
command: postgres -D "$PGDATA" -k "$PGDATA" -c listen_addresses=''
shutdown:
signal: 2
readiness_probe:
exec:
command: pg_isready -h "$PGDATA" -d "$PGDATABASE" -U "$PGUSER"
initial_delay_seconds: 1
period_seconds: 2
migrate:
command: php artisan migrate --force
working_dir: ./backend
depends_on:
postgres:
condition: process_healthy
mailpit:
command: mailpit --smtp 127.0.0.1:${MAILPIT_SMTP_PORT:-2525} --listen 127.0.0.1:${MAILPIT_UI_PORT:-8025}
readiness_probe:
http_get:
host: 127.0.0.1
port: ${MAILPIT_UI_PORT:-8025}
path: /
initial_delay_seconds: 1
period_seconds: 2
backend:
command: php artisan serve --host=127.0.0.1 --port=${BACKEND_PORT:-8001}
working_dir: ./backend
depends_on:
migrate:
condition: process_completed_successfully
mailpit:
condition: process_healthy
readiness_probe:
http_get:
host: 127.0.0.1
port: ${BACKEND_PORT:-8001}
path: /up
initial_delay_seconds: 2
period_seconds: 10
caddy:
command: caddy run --config Caddyfile --adapter caddyfile
depends_on:
backend:
condition: process_healthy
readiness_probe:
http_get:
host: 127.0.0.1
port: ${CADDY_PORT:-8000}
path: /up
scheme: https
initial_delay_seconds: 1
period_seconds: 2