Skip to content

marko/dev-server

Start your full development environment with a single command. The Dev Server package provides up, down, status, and open CLI commands (aliases for dev:up, dev:down, dev:status, dev:open) that orchestrate your PHP built-in server, Docker Compose services, and frontend build tools together. It auto-detects your project’s Docker Compose file and package manager, so zero configuration is required for most projects. All services are managed as background processes tracked via a PID file, letting you stop everything cleanly with marko down.

Terminal window
composer require marko/dev-server
Terminal window
marko up

This starts all detected services:

  • PHP server --- always started at http://localhost:8000 (serving public/)
  • Docker --- started if a compose.yaml / docker-compose.yml file is found
  • Frontend --- started if package.json has a dev script (uses bun, pnpm, yarn, or npm)

By default marko up runs in detached (background) mode. Use marko status and marko down to manage running services.

Terminal window
marko up --foreground
# alias: marko up -f

Runs services in the foreground. Press Ctrl+C to stop all services. This overrides the detach default.

marko up runs detached by default. You can also make this explicit:

Terminal window
marko up --detach

Use marko status and marko down to manage background services.

Terminal window
marko status

Shows the name, PID, status (running/stopped), port, and start time for each managed process.

Terminal window
marko open

Opens the running PHP development server in your default browser. The URL is determined dynamically from the running process, so it works with custom ports (e.g. --port=8080). Throws a helpful error if no dev environment is running.

Terminal window
marko down

Stops all processes started by marko up.

marko up requires a public/index.php entry point. If the file is missing, a DevServerException is thrown with a helpful message showing the bootstrap code to create it:

public/index.php
<?php
declare(strict_types=1);
use Marko\Core\Application;
require dirname(__DIR__) . '/vendor/autoload.php';
Application::boot(dirname(__DIR__))->handleRequest();
Terminal window
marko dev:up --port=8080

Overrides the configured port for the PHP built-in server.

Publish or create config/dev.php in your application:

config/dev.php
<?php
declare(strict_types=1);
return [
'port' => 8000,
'detach' => true,
'docker' => true,
'frontend' => true,
'pubsub' => true,
'processes' => [],
];
KeyTypeDefaultDescription
portint8000Port for the PHP built-in server
detachbooltrueRun services in background by default (default: true)
dockertrue|string|falsetrueAuto-detect Docker (true), custom command (string), or disable (false)
frontendtrue|string|falsetrueAuto-detect frontend (true), custom command (string), or disable (false)
pubsubtrue|string|falsetrueAuto-detect pub/sub listener (true), custom command (string), or disable (false)
processesarray<string, string>[]Named custom processes to run alongside the dev environment

The docker and frontend keys accept three forms:

// Auto-detect (default): scan for compose file / package.json
'docker' => true,
// Custom command: run exactly this
'docker' => 'docker compose -f infrastructure/compose.yaml up -d',
// Disabled: skip entirely
'docker' => false,

Use the processes key to run additional named processes alongside the standard services:

'processes' => [
'tailwind' => './tailwindcss -i src/css/app.css -o public/css/app.css --watch',
'queue' => 'marko queue:work',
],

Each process is managed by ProcessManager --- output is prefixed with the process name (e.g. [tailwind]), and processes are tracked in the PID file when running in detached mode.

Flags passed to dev:up take precedence over config file values:

FlagDescription
--port=N, -p=NOverride the server port
--detach, -dRun in background (detached mode)
--foreground, -fRun in foreground mode (overrides detach default)
use Marko\Core\Attributes\Command;
use Marko\Core\Command\Input;
use Marko\Core\Command\Output;
#[Command(name: 'dev:up', description: 'Start the development environment', aliases: ['up'])]
public function execute(Input $input, Output $output): int;
use Marko\Core\Attributes\Command;
use Marko\Core\Command\Input;
use Marko\Core\Command\Output;
#[Command(name: 'dev:down', description: 'Stop the development environment', aliases: ['down'])]
public function execute(Input $input, Output $output): int;
use Marko\Core\Attributes\Command;
use Marko\Core\Command\Input;
use Marko\Core\Command\Output;
#[Command(name: 'dev:open', description: 'Open the running development server in a browser', aliases: ['open'])]
public function execute(Input $input, Output $output): int;
use Marko\Core\Attributes\Command;
use Marko\Core\Command\Input;
use Marko\Core\Command\Output;
#[Command(name: 'dev:status', description: 'Show development environment status', aliases: ['status'])]
public function execute(Input $input, Output $output): int;

Scans the project root for Docker Compose files (compose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml) and returns the appropriate up/down commands.

use Marko\DevServer\Detection\DockerDetector;
$dockerDetector = new DockerDetector(projectRoot: '/path/to/project');
/** @return array{upCommand: string, downCommand: string}|null */
$dockerDetector->detect();

Checks for a package.json with a dev script and auto-detects the package manager by looking for lock files (bun, pnpm, yarn, npm --- in that order).

use Marko\DevServer\Detection\FrontendDetector;
$frontendDetector = new FrontendDetector(projectRoot: '/path/to/project');
$frontendDetector->detect(); // e.g. 'bun run dev', or null

Manages named background processes with prefixed output streaming and signal handling for graceful shutdown.

use Marko\DevServer\Process\ProcessManager;
/** @throws DevServerException */
$processManager->start(string $name, string $command): int;
$processManager->stop(string $name): void;
$processManager->stopAll(): void;
$processManager->getPid(string $name): ?int;
$processManager->getPids(): array; // array<string, int>
$processManager->isRunning(string $name): bool;
$processManager->runForeground(): void;

Persists process entries to .marko/dev.json for tracking detached processes across commands.

use Marko\DevServer\Process\PidFile;
use Marko\DevServer\Process\ProcessEntry;
/** @param array<ProcessEntry> $entries */
$pidFile->write(array $entries): void;
/** @return array<ProcessEntry> */
$pidFile->read(): array;
$pidFile->clear(): void;
$pidFile->isRunning(int $pid): bool;
use Marko\DevServer\Process\ProcessEntry;
readonly class ProcessEntry
{
public function __construct(
public string $name,
public int $pid,
public string $command,
public int $port,
public string $startedAt,
) {}
}

Extends MarkoException with contextual error messages and suggestions:

  • processFailedToStart(string $name, string $command) --- thrown when a process fails to start. Suggests checking the command and running marko status.
  • portInUse(int $port) --- thrown when the PHP server port is already in use. Suggests using --port=XXXX to pick a different port.