From 83ca2cf43c9b13164e9aa8af442aef943be6a1b7 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Thu, 30 Jul 2026 22:54:20 +0300 Subject: [PATCH] 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. --- .envrc | 11 ++++ .gitignore | 5 ++ Caddyfile | 17 +++++ backend/.env.example | 16 ++--- backend/composer.json | 1 - backend/config/database.php | 2 +- backend/config/queue.php | 4 +- backend/database/.gitignore | 1 - flake.lock | 61 +++++++++++++++++ flake.nix | 54 +++++++++++++++ nix/shell-hook.sh | 127 ++++++++++++++++++++++++++++++++++++ process-compose.yaml | 59 +++++++++++++++++ 12 files changed, 345 insertions(+), 13 deletions(-) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Caddyfile delete mode 100644 backend/database/.gitignore create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/shell-hook.sh create mode 100644 process-compose.yaml diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..53d7982 --- /dev/null +++ b/.envrc @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01f5f11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.cert +/.direnv/ +/.mailpit.log +/.postgres/ +/.worktrees/ diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..8df2d09 --- /dev/null +++ b/Caddyfile @@ -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} + } +} diff --git a/backend/.env.example b/backend/.env.example index 40bb13b..c3214a6 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -2,7 +2,7 @@ APP_NAME=Laravel APP_ENV=local APP_KEY= APP_DEBUG=true -APP_URL=http://localhost:8000 +APP_URL=https://localhost:8000 APP_LOCALE=en APP_FALLBACK_LOCALE=en @@ -20,12 +20,12 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=sqlite -# DB_HOST=127.0.0.1 -# DB_PORT=3306 -# DB_DATABASE=laravel -# DB_USERNAME=root -# DB_PASSWORD= +DB_CONNECTION=pgsql +DB_HOST="${PGHOST}" +DB_PORT=5432 +DB_DATABASE=postgres +DB_USERNAME=postgres +DB_PASSWORD= SESSION_DRIVER=database SESSION_LIFETIME=120 @@ -47,7 +47,7 @@ REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 -MAIL_MAILER=log +MAIL_MAILER=smtp MAIL_SCHEME=null MAIL_HOST=127.0.0.1 MAIL_PORT=2525 diff --git a/backend/composer.json b/backend/composer.json index 5a8f4f4..611f358 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -81,7 +81,6 @@ ], "post-create-project-cmd": [ "@php artisan key:generate --ansi", - "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", "@php artisan migrate --graceful --ansi" ], "pre-package-uninstall": [ diff --git a/backend/config/database.php b/backend/config/database.php index abbb88e..00445da 100644 --- a/backend/config/database.php +++ b/backend/config/database.php @@ -17,7 +17,7 @@ return [ | */ - 'default' => env('DB_CONNECTION', 'sqlite'), + 'default' => env('DB_CONNECTION', 'pgsql'), /* |-------------------------------------------------------------------------- diff --git a/backend/config/queue.php b/backend/config/queue.php index 79c2c0a..e633605 100644 --- a/backend/config/queue.php +++ b/backend/config/queue.php @@ -103,7 +103,7 @@ return [ */ 'batching' => [ - 'database' => env('DB_CONNECTION', 'sqlite'), + 'database' => env('DB_CONNECTION', 'pgsql'), 'table' => 'job_batches', ], @@ -122,7 +122,7 @@ return [ 'failed' => [ 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), - 'database' => env('DB_CONNECTION', 'sqlite'), + 'database' => env('DB_CONNECTION', 'pgsql'), 'table' => 'failed_jobs', ], diff --git a/backend/database/.gitignore b/backend/database/.gitignore deleted file mode 100644 index 9b19b93..0000000 --- a/backend/database/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.sqlite* diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..143e71c --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..85a5fe9 --- /dev/null +++ b/flake.nix @@ -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; + }; + } + ); +} diff --git a/nix/shell-hook.sh b/nix/shell-hook.sh new file mode 100644 index 0000000..90e0f50 --- /dev/null +++ b/nix/shell-hook.sh @@ -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" diff --git a/process-compose.yaml b/process-compose.yaml new file mode 100644 index 0000000..dc1ea2f --- /dev/null +++ b/process-compose.yaml @@ -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