| name | malt |
| description | Malt development environment manager. Use when setting up, starting, stopping, troubleshooting, customizing Homebrew-based dev environments with malt.json, or converting malt.json to other formats (Docker, Kubernetes, Terraform, Ansible, GitHub Actions, .env). |
Malt - JSON-driven Homebrew Dev Services
Malt creates project-specific development environments using only Homebrew. Define your stack in malt.json, and Malt handles installation, configuration, and service management with native performance and port-based isolation.
Command Reference
| Command | Description |
|---|
malt init | Create malt.json from default template (skips if exists) |
malt install | Install Homebrew packages and PHP extensions from malt.json |
malt create | Generate malt/ directory with service configs (skips if exists) |
malt start | Create temp configs with variable substitution and start services |
malt stop | Stop services and clean up temp files |
malt status | Show running status of all configured services |
malt kill | Force-kill all Malt service processes (SIGKILL) |
malt env | Output shell script for env vars and aliases. Usage: source <(malt env) |
malt info | Show project name, directory, and configured ports |
Setup Guide
Prerequisites
Before any malt operation, verify Homebrew and Malt are installed:
which brew
which malt
If Homebrew is missing, direct the user to https://brew.sh/. If Malt is missing:
brew tap shivammathur/php
brew tap shivammathur/extensions
brew tap koriym/malt
brew install malt
Setup Flow
When composer.json exists (PHP project)
- Read
composer.json to auto-detect requirements:
| Source | Pattern | Service |
|---|
composer.json | php require constraint | PHP version (e.g., ^8.1 -> php@8.1) |
composer.json | ext-redis or predis/predis | Redis |
composer.json | ext-memcached | Memcached |
composer.json | ext-pdo_mysql | MySQL |
| Filesystem | .htaccess file exists | Apache (httpd); otherwise Nginx |
- Report detected dependencies and generate
malt.json:
Detected these external dependencies from composer.json. Adding to malt.json:
- PHP 8.4 (port: 9000)
- MySQL 8.0 (port: 3306)
- Redis (port: 6379)
- Nginx (port: 80)
- Extensions: xdebug, redis
- Generate
malt.json and run setup commands.
When composer.json does not exist
Ask the user what services they need in natural language. Example interactions:
- "MySQL and Redis are needed" -> generate
malt.json with MySQL and Redis
- "Set up a PHP 8.4 environment with Nginx" -> generate accordingly
Then present the proposed malt.json and confirm with AskUserQuestion before proceeding.
Setup Commands
cd your-project
malt init
malt install
malt create
malt start
source <(malt env)
Step 3 (malt create) is required, not optional. malt start reads configs from malt/ and fails with "Malt directory not found" if the directory does not exist. Before invoking malt start, verify malt/ is present and run malt create first if it is missing.
Joining Existing Project
For projects with malt.json already committed, the malt/ directory may or may not be present. malt.json is committed, but malt/ is often regenerated per-checkout. Always check before starting:
malt install
[ -d malt ] || malt create
malt start
source <(malt env)
Precondition: malt start requires the malt/ directory. If only malt.json is present, run malt create first; otherwise malt start will fail with "Malt directory not found".
malt.json Schema
All fields are required. Schema: docs/schema.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"
]
}
| Field | Type | Description |
|---|
project_name | string | Project identifier |
dependencies | string[] | Homebrew formula names (services, tools, PHP version) |
ports | object | Service name -> port array. Keys: php, mysql, nginx, httpd, redis, memcached. Multiple ports create multiple instances |
php_extensions | string[] | PHP extensions to install via shivammathur/extensions tap |
Troubleshooting Guide
Detecting State: malt.json Exists but malt/ Does Not
Common on a fresh checkout: the project commits malt.json but malt/ is gitignored or has never been generated. Running malt start in this state fails with "Malt directory not found".
Correct response:
malt create && malt start
Not malt start alone. The skill should test for malt/ (e.g. [ -d malt ]) before invoking malt start and run malt create when the directory is absent.
Port Already in Use
lsof -i :PORT_NUMBER
kill -9 $(lsof -t -i :PORT_NUMBER)
Or change the port in malt.json, then rm -rf malt/ && malt create && malt start.
MySQL First Start Fails
MySQL auto-initializes its data directory on first start (--initialize-insecure). If it fails:
rm -rf malt/var/mysql_*
malt start
PHP Extension Load Failure
Extensions are resolved from Homebrew opt directories. If an extension fails to load:
- Check it's listed in
malt.json php_extensions
- Run
malt install to ensure it's installed
- Check
malt/conf/php.ini for correct paths
- Extension resolution order:
{HOMEBREW_PREFIX}/opt/{ext}@{php_version}/{ext}.so, then php{ext}@..., then php-{ext}@...
Service Won't Stop
malt kill
malt create Does Nothing
The malt/ directory already exists. To regenerate:
rm -rf malt/
malt create
Debug Mode
MALT_DEBUG=1 malt start
MALT_DEBUG=1 malt stop
Check Logs
Service logs are in malt/logs/:
php-fpm_9000.log
mysql_3306_error.log
nginx_error_80.log
httpd_error_8080.log
Customization
Edit files in malt/conf/ directly, then restart with malt stop && malt start.
Change Document Root
Edit root in malt/conf/nginx_*.conf:
root "{{PROJECT_DIR}}/webroot"; # instead of public
Or DocumentRoot in malt/conf/httpd_*.conf:
DocumentRoot "{{PROJECT_DIR}}/webroot"
<Directory "{{PROJECT_DIR}}/webroot">
PHP Settings
Edit malt/conf/php.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:
[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_*.cnf:
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
Re-generating Configs
After changing dependencies or ports in malt.json:
malt install
rm -rf malt/
malt create
malt start
Template Variables
Config files in malt/conf/ use {{VARIABLE}} placeholders, expanded at malt start into .tmp files:
| 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 (multi-instance) | 0 |
{{PHP_EXTENSIONS}} | Generated extension directives | extension=... |
{{NGINX_INCLUDES}} | Generated include statements | include ...; |
{{PHP_PORT}} | First configured PHP port | 9000 |
{{PHP_LIB_PATH}} | Path to Apache PHP module | /opt/homebrew/opt/php@8.4/... |
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
│ │ └── *.tmp # Temporary files (gitignore)
│ ├── logs/ # Service log files (gitignore)
│ ├── tmp/ # Temporary/socket files (gitignore)
│ └── var/ # Data files like MySQL data (gitignore)
└── public/ # Document root for web servers
Recommended .gitignore
malt/logs/
malt/tmp/
malt/var/
malt/conf/*.tmp
Format Conversion
Use malt.json as Single Source of Truth to generate other formats.
Supported Formats
Dockerfile, Docker Compose, .env, GitHub Actions, Kubernetes, Terraform (AWS), Ansible.
Mapping Table: Docker / .env / GitHub Actions
| malt.json | Docker image | .env | GitHub Actions |
|---|
php@8.4 | php:8.4-fpm | PHP_VERSION=8.4 | shivammathur/setup-php php-version: 8.4 |
mysql@8.0 | mysql:8.0 | MYSQL_PORT=3306 | services.mysql image: mysql:8.0 |
redis | redis:latest | REDIS_PORT=6379 | services.redis |
nginx | nginx:latest | NGINX_PORT=80 | N/A |
memcached | memcached:latest | MEMCACHED_PORT=11211 | services.memcached |
Mapping Table: Kubernetes / Terraform / Ansible
| malt.json | Kubernetes | Terraform (AWS) | Ansible |
|---|
php@8.4 | Deployment + Service | ECS task definition | php-fpm package + systemd |
mysql@8.0 | StatefulSet + PVC | RDS mysql 8.0 | mysql-server + systemd |
redis | Deployment + Service | ElastiCache Redis | redis-server + systemd |
nginx | Deployment + Ingress | ALB | nginx + systemd |
memcached | Deployment + Service | ElastiCache Memcached | memcached + systemd |
| ports | Service ports | Security Group ingress | listen config |
| php_extensions | custom image | ECS image build | pecl install tasks |
Dockerfile
Generate a Dockerfile from malt.json. Typically one Dockerfile per service (used with Docker Compose):
FROM php:8.4-fpm
RUN docker-php-ext-install pdo_mysql \
&& pecl install redis apcu xdebug \
&& docker-php-ext-enable redis apcu xdebug
COPY . /var/www/html
WORKDIR /var/www/html
EXPOSE 9000
Key rules: use official base images, match PHP version and extensions from malt.json. Generate separate Dockerfiles per service when used with Docker Compose.
Docker Compose
Read malt.json and generate docker-compose.yml:
services:
php:
image: php:8.4-fpm
ports:
- "9000:9000"
volumes:
- .:/var/www/html
mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:latest
ports:
- "6379:6379"
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- .:/var/www/html
volumes:
mysql_data:
.env
Generate environment variables from malt.json:
PHP_VERSION=8.4
PHP_PORT=9000
MYSQL_PORT=3306
MYSQL_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_HOST=127.0.0.1
NGINX_PORT=80
GitHub Actions
Generate CI workflow using shivammathur/setup-php for PHP and GitHub Actions services:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
ports:
- 3306:3306
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: redis, pdo_mysql
- run: composer install
- run: composer test
Kubernetes
Generate Deployment + Service manifests per service in malt.json:
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-fpm
spec:
replicas: 1
selector:
matchLabels:
app: php-fpm
template:
metadata:
labels:
app: php-fpm
spec:
containers:
- name: php-fpm
image: php:8.4-fpm
ports:
- containerPort: 9000
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ALLOW_EMPTY_PASSWORD
value: "yes"
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
Key rules: MySQL/stateful services use StatefulSet + PVC. Stateless services (PHP-FPM, Nginx, Redis, Memcached) use Deployment. Add resource requests/limits. Use ConfigMap for custom configs.
Terraform
Generate AWS infrastructure from malt.json (ECS Fargate + RDS):
# main.tf
resource "aws_ecs_task_definition" "app" {
family = "myapp"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = "512"
memory = "1024"
container_definitions = jsonencode([
{
name = "php-fpm"
image = "php:8.4-fpm"
portMappings = [{ containerPort = 9000 }]
},
{
name = "nginx"
image = "nginx:latest"
portMappings = [{ containerPort = 80 }]
}
])
}
resource "aws_db_instance" "mysql" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
port = 3306
}
resource "aws_elasticache_cluster" "redis" {
engine = "redis"
node_type = "cache.t3.micro"
num_cache_nodes = 1
port = 6379
}
Key rules: MySQL → RDS, Redis/Memcached → ElastiCache, PHP+Nginx → ECS Fargate. Add VPC, subnets, security groups with port-based ingress matching malt.json ports. Organize as main.tf, variables.tf, outputs.tf.
Ansible
Generate playbook to provision a Linux server from malt.json:
---
- name: Setup development environment
hosts: all
become: true
vars:
php_version: "8.4"
mysql_port: 3306
redis_port: 6379
nginx_port: 80
tasks:
- name: Install PHP and extensions
apt:
name:
- "php{{ php_version }}-fpm"
- "php{{ php_version }}-mysql"
- "php{{ php_version }}-redis"
- "php{{ php_version }}-apcu"
state: present
notify: restart php-fpm
- name: Install MySQL
apt:
name: mysql-server-8.0
state: present
notify: restart mysql
- name: Install Redis
apt:
name: redis-server
state: present
notify: restart redis
- name: Install Nginx
apt:
name: nginx
state: present
notify: restart nginx
handlers:
- name: restart php-fpm
systemd:
name: "php{{ php_version }}-fpm"
state: restarted
- name: restart mysql
systemd:
name: mysql
state: restarted
- name: restart redis
systemd:
name: redis-server
state: restarted
- name: restart nginx
systemd:
name: nginx
state: restarted
Key rules: Map each dependency to apt packages. Use handlers for service restarts. Extract ports and versions into vars. Add configuration templates for custom settings from malt/conf/.