Skip to content

Instantly share code, notes, and snippets.

@Braunson
Last active October 30, 2025 21:18
Show Gist options
  • Select an option

  • Save Braunson/0724f2db6af74d30595d3e48ec1256e8 to your computer and use it in GitHub Desktop.

Select an option

Save Braunson/0724f2db6af74d30595d3e48ec1256e8 to your computer and use it in GitHub Desktop.
A lightweight, zero-dependency shell script that spins up a local PHP development server using the built-in php -S command.

🧰 dev.sh — Simple Cross-Platform PHP Dev Server

A lightweight, zero-dependency shell script that spins up a local PHP development server using the built-in php -S command.

Features

  • Works on Linux, macOS, and Windows (Git Bash / WSL)
  • Auto-detects the PHP binary
  • Supports custom host, port, and docroot
  • Optional router file for clean URLs
  • --free-port flag to automatically find an available port
  • Optional auto-open in browser

Clean colored output and safety checks

Usage

./dev.sh [options]

Options:
  -p, --port <n>        Port to listen on (default: 8000)
  -h, --host <host>     Host interface (default: 127.0.0.1)
  -d, --docroot <dir>   Document root (default: public/)
  -r, --router <file>   Optional router script (e.g., router.php)
  -o, --open            Auto-open in browser
      --free-port       Increment until a free port is found
  -?, --help            Show help

Example:

./dev.sh --free-port -d public -o

Why

Perfect for small PHP apps, APIs, or Laravel prototypes where you don’t want Docker, Vagrant, or a full stack just to test locally. Drop it in your repo, commit it, and every developer can boot the same server instantly.

# For plain CMD users
@echo off
set PORT=%1
if "%PORT%"=="" set PORT=8000
set HOST=127.0.0.1
set DOCROOT=public
echo Starting PHP dev server on http://%HOST%:%PORT% (docroot=%DOCROOT%)
php -S %HOST%:%PORT% -t %DOCROOT%
#!/usr/bin/env bash
# dev.sh — start a local PHP dev server (Linux/macOS/Windows Git Bash/WSL)
# Usage:
# ./dev.sh [-p port] [-h host] [-d docroot] [-r router.php] [-o] [--free-port]
# Examples:
# ./dev.sh
# ./dev.sh -p 8080 -d public -o
# ./dev.sh --free-port -r router.php
set -euo pipefail
cd "$(dirname "$0")"
DOCROOT="v1.0" # Specific to this project
HOST="127.0.0.1" # safer than "localhost" on Windows
PORT="8000"
AUTO_OPEN="false"
FIND_FREE="false"
ROUTER=""
print_help() {
cat <<EOF
Usage: $(basename "$0") [options]
Options:
-p, --port <n> Port to listen on (default: ${PORT})
-h, --host <host> Host interface (default: ${HOST})
-d, --docroot <dir> Document root (default: ${DOCROOT})
-r, --router <file> Optional router script (e.g., router.php)
-o, --open Open the URL in your browser after start
-f, --free-port If PORT is busy, increment until a free port is found
-?, --help Show this help
Examples:
$(basename "$0") -p 8080 -d public -o
$(basename "$0") --free-port -r router.php
EOF
}
# --- parse args ---
while [[ $# -gt 0 ]]; do
case "$1" in
-p|--port) PORT="${2:-}"; shift 2 ;;
-h|--host) HOST="${2:-}"; shift 2 ;;
-d|--docroot) DOCROOT="${2:-}"; shift 2 ;;
-r|--router) ROUTER="${2:-}"; shift 2 ;;
-o|--open) AUTO_OPEN="true"; shift ;;
-f|--free-port) FIND_FREE="true"; shift ;;
-\?|--help) print_help; exit 0 ;;
*) echo "Unknown option: $1"; print_help; exit 64 ;;
esac
done
# --- php detection ---
if ! command -v php >/dev/null 2>&1; then
echo "❌ PHP not found in PATH."
exit 127
fi
PHP_BIN="$(command -v php)"
# --- sanity checks ---
if [[ ! -d "$DOCROOT" ]]; then
echo "❌ Docroot '$DOCROOT' not found."
exit 66
fi
if [[ -n "$ROUTER" && ! -f "$ROUTER" ]]; then
echo "❌ Router file '$ROUTER' not found."
exit 66
fi
# Port validation
if ! [[ "$PORT" =~ ^[0-9]+$ ]] || (( PORT < 1 || PORT > 65535 )); then
echo "❌ Invalid port: $PORT (must be 1–65535)."
exit 65
fi
# --- port check helpers (cross-platform best-effort) ---
is_port_in_use() {
local port="$1"
# lsof
if command -v lsof >/dev/null 2>&1; then
lsof -i TCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 && return 0 || return 1
fi
# ss (use process substitution to avoid subshell return bug)
if command -v ss >/dev/null 2>&1; then
while read -r line; do
case "$line" in
*":$port "*|*":$port"*) return 0 ;;
esac
done < <(ss -ltn 2>/dev/null)
return 1
fi
# netstat (Windows/mac/Linux; no grep)
if command -v netstat >/dev/null 2>&1; then
while IFS= read -r line; do
case "$line" in
*":$port "*|*":$port"*)
case "$line" in
*LISTEN*|*LISTENING*) return 0 ;;
esac
;;
esac
done < <(netstat -an 2>/dev/null)
return 1
fi
return 1
}
show_port_owner() {
local port="$1"
# mac/Linux with lsof
if command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:"$port" -sTCP:LISTEN 2>/dev/null | awk 'NR==2{print "🔒 In use by:", $1, "PID", $2}' || true
return
fi
# Windows netstat with PIDs
if command -v netstat >/dev/null 2>&1; then
local line pid
while IFS= read -r line; do
case "$line" in
*":$port "*|*":$port"*)
case "$line" in
*LISTEN*|*LISTENING*)
# Last column is PID on Windows; try to print it
pid="$(echo "$line" | awk '{print $NF}' 2>/dev/null || true)"
[[ -n "$pid" ]] && echo "🔒 In use by PID $pid"
return
;;
esac
;;
esac
done < <(netstat -ano 2>/dev/null)
fi
}
if [[ "$FIND_FREE" == "true" ]]; then
original="$PORT"
attempt=0
while is_port_in_use "$PORT"; do
attempt=$((attempt+1))
PORT=$((PORT+1))
if (( attempt > 50 )); then
echo "❌ Could not find a free port near $original (tried 50)."
exit 69
fi
done
else
if is_port_in_use "$PORT"; then
echo "❌ Port $PORT is already in use. Use --free-port or pick another with -p."
show_port_owner "$PORT" || true
exit 98
fi
fi
URL="http://${HOST}:${PORT}"
echo "🚀 Starting local PHP dev server"
echo "➡️ ${URL}"
echo "📂 Serving: ${DOCROOT}/"
[[ -n "$ROUTER" ]] && echo "🧭 Router: ${ROUTER}"
echo "⏹ Press Ctrl+C to stop."
# Export for router scripts that read DOCROOT
export DOCROOT
# --- auto-open browser (best-effort) ---
if [[ "$AUTO_OPEN" == "true" ]]; then
if command -v xdg-open >/dev/null 2>&1; then xdg-open "$URL" >/dev/null 2>&1 || true
elif command -v open >/dev/null 2>&1; then open "$URL" >/dev/null 2>&1 || true
elif command -v powershell.exe >/dev/null 2>&1; then powershell.exe -NoProfile -Command "Start-Process '$URL'" >/dev/null 2>&1 || true
elif command -v cmd.exe >/dev/null 2>&1; then cmd.exe /c start "" "$URL" >/dev/null 2>&1 || true
fi
fi
# --- run server (foreground) ---
if [[ -n "$ROUTER" ]]; then
exec "$PHP_BIN" -S "${HOST}:${PORT}" -t "$DOCROOT" "$ROUTER"
else
exec "$PHP_BIN" -S "${HOST}:${PORT}" -t "$DOCROOT"
fi
<?php
declare(strict_types=1);
// router.php — example -- static first, else route to index.php inside docroot
$docroot = getenv('DOCROOT') ?: __DIR__ . '/public';
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? '/';
$file = realpath($docroot . $path);
if ($file && is_file($file)) {
return false; // serve directly
}
require rtrim($docroot, '/\\') . '/index.php';
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment