# Malt - Full Reference > JSON-driven Homebrew Dev Services for macOS local development. Malt creates project-specific development environments using only Homebrew. Define your entire stack in `malt.json` — PHP version, web servers, databases, caching, and extensions — and replicate it anywhere with the portable `malt/` directory. No VMs or containers; native performance with port-based project isolation. ## Installation ```bash brew tap shivammathur/php brew tap shivammathur/extensions brew tap koriym/malt brew install malt ``` ## Workflow ```bash malt init # 1. Create malt.json from default template # Edit malt.json to match your project malt install # 2. Install Homebrew packages and PHP extensions malt create # 3. Generate malt/ directory with service configs malt start # 4. Start all configured services source <(malt env) # 5. Set up PATH, env vars, and aliases malt stop # 6. Stop all services ``` For existing projects (joining a team or second machine): ```bash malt install && malt start && source <(malt env) ``` > If `malt/` is not present, run `malt create` before `malt start`. ## malt.json Schema Single source of truth for the project environment. All fields are required. ```json { "project_name": "myapp", "dependencies": [ "php@8.4", "mysql@8.0", "composer", "redis", "nginx", "memcached" ], "ports": { "php": [9000], "mysql": [3306], "nginx": [80, 443], "httpd": [8080], "redis": [6379], "memcached": [11211] }, "php_extensions": [ "xdebug", "pcov", "redis", "memcached", "apcu" ] } ``` ### Fields - `project_name` (string, required): Project identifier used in logs and status output. - `dependencies` (string[], required): Homebrew formula names. Include the PHP version as `php@X.Y` (e.g., `php@8.4`), services, and tools. - `ports` (object, required): Service name → port array. Multiple ports create multiple service instances. Supported keys: `php`, `mysql`, `nginx`, `httpd`, `redis`, `memcached`. - `php_extensions` (string[], required): PHP extensions installed via `shivammathur/extensions` tap and loaded in php.ini. Use lowercase names. ## Commands ### `malt init` Creates `malt.json` from the default template. Does nothing if the file already exists. ### `malt install` Installs Homebrew packages listed in `dependencies` and PHP extensions from `php_extensions`. Skips already-installed packages. Extension formula names are resolved with multiple naming conventions: `{ext}@{version}`, `php{ext}@{version}`, `php-{ext}@{version}`. ### `malt create` Generates the `malt/` directory with subdirectories (`conf/`, `logs/`, `tmp/`, `var/`) and service configuration files. Creates `public/` with default dashboard if it does not exist. Does nothing if `malt/` already exists — delete it first to regenerate. ### `malt start` Cleans up old `.tmp` files, creates new temporary configs with template variable substitution, then starts services in order: PHP-FPM, MySQL, Redis, Memcached, Nginx, Apache. Displays access URLs after startup. ### `malt stop` Stops services in reverse order. Cleans up temporary config files (unless `MALT_DEBUG=1`). ### `malt status` Shows running/stopped state of all configured services by checking port usage per configured port. ### `malt kill` Force-kills all Malt-related service processes (php-fpm, mysqld, redis-server, memcached, nginx, httpd) using SIGKILL, regardless of malt.json configuration. ### `malt env` Outputs a shell script that exports environment variables and creates aliases: - `MALT_DIR` — Absolute path to malt/ directory - `DOCUMENT_ROOT` — Absolute path to public/ directory - `PATH` — Prepends version-specific binary directories for PHP and MySQL - Service aliases: `mysql@3306`, `redis-cli@6379`, etc. Usage: `source <(malt env)` ### `malt info` Displays project name, directory, malt directory path, and configured service ports. ## Directory Structure ``` your-project/ ├── malt.json # Environment definition (commit this) ├── malt/ # Generated by malt create │ ├── conf/ # Service config files (commit these) │ │ ├── php-fpm_9000.conf │ │ ├── php.ini │ │ ├── nginx_80.conf │ │ ├── nginx_main.conf │ │ ├── httpd_8080.conf │ │ ├── my_3306.cnf │ │ ├── redis_6379.conf │ │ ├── memcached_11211.conf │ │ └── *.tmp # Temporary files — gitignore │ ├── logs/ # Service log files — gitignore │ ├── tmp/ # Temporary/socket files — gitignore │ └── var/ # Data files (MySQL data) — gitignore └── public/ # Document root for web servers ``` ### Recommended .gitignore ``` malt/logs/ malt/tmp/ malt/var/ malt/conf/*.tmp ``` ## Template Variables Configuration files in `malt/conf/` use `{{VARIABLE}}` placeholders expanded when services start (creating `.tmp` files). You can use these in customized configs too. | Variable | Description | Example | |----------|-------------|---------| | `{{MALT_DIR}}` | Absolute path to malt/ directory | `/Users/dev/myapp/malt` | | `{{PROJECT_DIR}}` | Absolute path to project root | `/Users/dev/myapp` | | `{{HOMEBREW_PREFIX}}` | Homebrew installation prefix | `/opt/homebrew` | | `{{PHP_VERSION}}` | PHP version from dependencies | `8.4` | | `{{PORT}}` | Service port number | `9000` | | `{{INDEX}}` | Zero-based index (MySQL multi-instance) | `0` | | `{{PHP_EXTENSIONS}}` | Generated extension directives (php.ini) | `extension=/opt/homebrew/opt/...` | | `{{NGINX_INCLUDES}}` | Generated include statements (nginx_main) | `include .../nginx_80.conf.tmp;` | | `{{PHP_PORT}}` | First configured PHP port (web server configs) | `9000` | | `{{PHP_LIB_PATH}}` | Path to Apache PHP module | `/opt/homebrew/opt/php@8.4/...` | ## Service Details ### PHP-FPM - Config: `php-fpm_{port}.conf`, `php.ini` - Command: `{HOMEBREW_PREFIX}/opt/php@{version}/sbin/php-fpm -y {conf} -c {ini}` - Multiple instances on different ports supported - Extensions loaded with full Homebrew opt paths - xdebug loaded as `zend_extension`, others as `extension` ### Nginx - Config: `nginx_{port}.conf` (per virtual host), `nginx_main.conf` (master) - Single nginx process serves all ports via include directives in nginx_main.conf - FastCGI proxy to PHP-FPM on the first configured PHP port - Document root: `{PROJECT_DIR}/public` ### Apache HTTPD - Config: `httpd_{port}.conf` (per port) - Separate httpd instance per port - Loads libphp.so module from `{HOMEBREW_PREFIX}/opt/php@{version}/lib/httpd/modules/` - Graceful shutdown via `apachectl -k stop` ### MySQL - Config: `my_{port}.cnf` - Auto-initializes data directory on first start (`mysqld --initialize-insecure`) - Socket files in `malt/tmp/` - Separate data directories per instance: `malt/var/mysql_{index}` - Note: MySQL version is hardcoded to `8.0` in the service implementation ### Redis - Config: `redis_{port}.conf` - RDB dumps in `malt/tmp/` - Shutdown via `redis-cli -p {port} shutdown` ### Memcached - Config: `memcached_{port}.conf` generated by `malt create` (contains port, log path) - Note: `malt start` does not read the config file; memcached is launched with inline parameters (`-m 64 -c 1024 -l 127.0.0.1`) - 64MB memory, 1024 max connections, binds to 127.0.0.1 ## PHP Extension Resolution PHP extensions installed via the `shivammathur/extensions` tap are not placed in PHP's `extension_dir`. Malt resolves the full `.so` path by checking Homebrew opt directories in order: 1. `{HOMEBREW_PREFIX}/opt/{ext}@{php_version}/{ext}.so` — e.g., xdebug, pcov, apcu, memcached 2. `{HOMEBREW_PREFIX}/opt/php{ext}@{php_version}/{ext}.so` — e.g., phpredis → redis.so 3. `{HOMEBREW_PREFIX}/opt/php-{ext}@{php_version}/{ext}.so` — fallback If none found, falls back to bare name `{ext}.so` with a warning. ## Customization Guide After `malt create`, edit files in `malt/conf/` directly. Do not edit `share/templates/` — those are internal. **Re-running `malt create`**: If `malt/` already exists, `malt create` does nothing. To regenerate, delete `malt/` first. Back up or version-control your customizations before doing so. ### Change Document Root Edit `root` in `nginx_{port}.conf` or `DocumentRoot` in `httpd_{port}.conf`: ```nginx # nginx root "{{PROJECT_DIR}}/webroot"; # instead of public ``` ```apache # apache DocumentRoot "{{PROJECT_DIR}}/webroot" # ... ``` ### PHP Settings Edit `malt/conf/php.ini`: ```ini memory_limit = 512M upload_max_filesize = 100M post_max_size = 100M display_errors = On error_reporting = E_ALL ``` ### Xdebug Configuration Append to `malt/conf/php.ini`: ```ini [xdebug] xdebug.mode = debug,develop xdebug.start_with_request = yes xdebug.client_host = 127.0.0.1 xdebug.client_port = 9003 ``` ### MySQL Character Set Edit `malt/conf/my_{port}.cnf`: ```ini [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci [client] default-character-set = utf8mb4 ``` ### Nginx — Front Controller ```nginx location / { try_files $uri /index.php?$query_string; } location ~ \.php$ { include "{{HOMEBREW_PREFIX}}/etc/nginx/fastcgi_params"; fastcgi_pass 127.0.0.1:{{PHP_PORT}}; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } ``` ### Nginx — Basic Authentication ```bash htpasswd -c malt/conf/.htpasswd your_username ``` ```nginx location /admin { auth_basic "Restricted Area"; auth_basic_user_file {{MALT_DIR}}/conf/.htpasswd; } ``` ### HTTPS Setup Add HTTPS ports to `malt.json`: ```json "ports": { "nginx": [80, 443], "httpd": [8080, 8443] } ``` Generate self-signed certificate: ```bash openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout malt/conf/nginx-selfsigned.key \ -out malt/conf/nginx-selfsigned.crt \ -subj "/CN=localhost" ``` Edit the 443 config in `malt/conf/nginx_443.conf` to reference the certificate files. ### Re-generating Configs ```bash rm -rf malt/ malt create # Re-apply customizations ``` ## Using Malt with Non-PHP Projects Malt is language-agnostic. For Ruby, Go, or Node.js projects, use Malt to manage background services: **Rails example:** ```json { "project_name": "my_rails_app", "dependencies": ["ruby@3.2", "mysql@8.0", "redis"], "ports": {"php": [9000], "mysql": [3306], "redis": [6379]}, "php_extensions": [] } ``` Note: `php_extensions` is required in the schema even if empty. The `php` port is also required by `validate!` in the current implementation — include it even for non-PHP projects. ## Architecture (for developers) ### Source Structure ``` bin/malt.rb # CLI entry point, command routing lib/ ├── config.rb # Malt::Config — parse and validate malt.json ├── project.rb # Malt::Project — init, install, create, env, info ├── template.rb # Malt::Template — {{VAR}} string substitution ├── service_manager.rb # Malt::ServiceManager — orchestrate start/stop/kill/status └── services/ ├── base_service.rb # Malt::BaseService — temp file management, port checking ├── php_service.rb # Malt::PhpService ├── mysql_service.rb # Malt::MysqlService ├── nginx_service.rb # Malt::NginxService ├── httpd_service.rb # Malt::HttpdService ├── redis_service.rb # Malt::RedisService └── memcached_service.rb # Malt::MemcachedService share/ ├── default.json # Default malt.json template ├── templates/ # ERB-named files using {{VAR}} syntax (not ERB) │ ├── php/php-fpm.conf.erb │ ├── php/php.ini.erb │ ├── nginx/nginx.conf.erb │ ├── nginx/nginx_main.conf.erb │ ├── httpd/httpd.conf.erb │ ├── mysql/my.cnf.erb │ ├── redis/redis.conf.erb │ └── memcached/memcached.conf.erb └── public/ # Default dashboard files copied to public/ on malt create ``` ### Two-Phase Variable Substitution Config generation happens in two distinct phases: **Phase 1 — `malt create`** (`Project#generate_config_files`): Renders template files with port/index values resolved. Runtime variables (`{{MALT_DIR}}`, `{{HOMEBREW_PREFIX}}`, `{{PHP_VERSION}}`, `{{PROJECT_DIR}}`) are written as literal `{{VARIABLE}}` strings into the output files in `malt/conf/`. **Phase 2 — `malt start`** (`BaseService#create_temp_config`): Reads the static configs from `malt/conf/`, expands the remaining `{{VARIABLE}}` placeholders with actual runtime values, and writes `.tmp` files (e.g., `nginx_80.conf.tmp`). These `.tmp` files are passed directly to the service processes. Cleanup happens on `malt stop` unless `MALT_DEBUG=1`. ### Template System Templates use `{{VARIABLE}}` syntax (NOT ERB despite the `.erb` file extension). `Malt::Template#render` performs simple `gsub` replacement. This means ERB directives (`<%= %>`) do not work in templates. ### Key Constants and Flags - `MALT_IS_LOCAL` — `true` when running from source (`ruby bin/malt.rb`); the Homebrew formula replaces it with `false` and substitutes `MALT_LIB_PATH`, `MALT_SHARE_PATH`, `MALT_CONFIG_PATH`, `MALT_TEMPLATES_PATH` at install time. - `HOMEBREW_PREFIX` — Defined once in `Malt` module (base_service.rb): `ENV["HOMEBREW_PREFIX"] || \`brew --prefix\`.chomp` - `MALT_DEBUG=1` — Keeps `.tmp` files after stop, prints verbose substitution details. ### Service Lifecycle 1. `malt start` calls `ServiceManager.start(options)` 2. Old `.tmp` files in `conf/` are cleaned up 3. Services registered in order: PHP, MySQL, Redis, Memcached, Nginx, Apache 4. For each service: check port availability → create temp config → start process in background 5. `malt stop` reverses registration order, stops each service, removes `.tmp` files ### Config Validation `Malt::Config#validate!` raises if `project_name` is missing or if no `php` ports are configured. This means `php` ports are currently required even for non-PHP projects. ### Local Development Run from source with `ruby bin/malt.rb` (uses `MALT_IS_LOCAL = true`). ```bash MALT_DEBUG=1 ruby bin/malt.rb start # Keep .tmp files, verbose output ``` ### Homebrew Formula Installation `Formula/malt.rb` copies `lib/` and `share/` from the tap, then writes `bin/malt` by reading `bin/malt.rb` and performing string replacements: - `MALT_IS_LOCAL = true` → `MALT_IS_LOCAL = false` - `{{MALT_LIB_PATH}}` → tap `lib/` path - `{{MALT_SHARE_PATH}}` → tap `share/` path - `{{MALT_CONFIG_PATH}}` → tap `share/default.json` path - `{{MALT_TEMPLATES_PATH}}` → tap `share/templates/` path ## Comparison | Feature | Malt | Docker | Devbox | |---------|------|--------|--------| | Ecosystem | Homebrew | Docker Hub | Nix | | Isolation | Port-based | Container | Nix store | | Performance | Native | Reduced (macOS) | Native | | Memory | Low | Medium-High | Low | | Startup | Instant | Seconds | Fast | | Config format | JSON | Dockerfile/YAML | JSON | | Service mgmt | Built-in | docker-compose | Plugin |