Search by

Docker-based local development environment and CLI for Maho Commerce applications, orchestrating development runtimes and services.

Package info

github.com/empiricompany/harbor

Language:Shell

Type:maho-module

pkg:composer/empiricompany/harbor

Statistics

Installs: 10

Dependents: 0

Suggesters: 0

Stars: 3

Open Issues: 2

v1.0.3 2026-09-15 10:46 UTC

This package is auto-updated.

Last update: 2026-09-19 05:03:16 UTC


README

Same harbor for every developer. A reproducible Docker Compose stack and the Swiss Army knife for Maho development, built for humans and naturally understood by AI agents.

Beta: Harbor is for local development only. It is actively developed and must not be used for production deployments. Commands, generated files, and defaults may change without notice.

Harbor is the Docker Compose launcher and local development stack for Maho projects: a single, reproducible environment no matter who runs it. Run all commands from the Maho project root. Harbor requires Docker and Docker Compose.

Harbor ships with all the required libraries to use:

  • Accessibility Scan (Playwright + Chromium): before the first scan, install the scanner runtime with ./vendor/bin/harbor maho accessibility:install.
  • VIPS image processing: enable the high-performance driver in a project with composer require intervention/image-driver-vips.

Table of contents

Install Harbor

Add Harbor to your Maho project as a Composer development dependency:

composer require --dev empiricompany/harbor

Tip: Harbor is under active development and changes frequently. To always use the latest changes, install the dev-main branch instead of a tagged release with composer require --dev empiricompany/harbor:dev-main.

Generate Harbor's project files in .harbor/:

./vendor/bin/harbor init

init creates these files:

File Purpose
.harbor/.env Environment variables for the stack
.harbor/.env.example Template of the available environment variables
.harbor/compose.yaml The Docker Compose stack
.harbor/docker.override.yaml Project and local Compose overrides (starts empty)
.harbor/docker.install.yaml Install-specific additions (starts empty)
.harbor/.gitignore Ignore rules for the generated files

Start the stack and check it:

./vendor/bin/harbor up -d
./vendor/bin/harbor ps

Install the project dependencies inside the application container, now that the stack is ready:

./vendor/bin/harbor composer install

Use ./vendor/bin/harbor up -d --build when the application image must be built. init creates .harbor/ files only when they are absent; init --force replaces generated environment and Compose files and can overwrite local settings.

Shell alias

Composer exposes the Harbor CLI at vendor/bin/harbor. To invoke it as harbor without the ./vendor/bin/ prefix, add a shell alias to ~/.zshrc or ~/.bashrc and restart your shell:

alias harbor='./vendor/bin/harbor'

Install Maho

Depending on the state of the project, choose one of the two options below.

Install from scratch

If the Maho project is not installed yet, run the installer with db as the database host and the credentials from .harbor/.env:

./vendor/bin/harbor maho install \
  --license_agreement_accepted yes \
  --locale en_US --timezone UTC --default_currency USD \
  --db_host db --db_name maho --db_user maho --db_pass maho \
  --url "https://localhost:8443/" \
  --use_secure 1 --secure_base_url "https://localhost:8443/" --use_secure_admin 1 \
  --admin_firstname Store --admin_lastname Admin --admin_email admin@example.com \
  --admin_username admin --admin_password veryl0ngpassw0rd \
  --sample_data 1

For the full installer documentation, see mahocommerce.com/about/getting-started/#install-maho.

Then reindex and flush the cache:

./vendor/bin/harbor maho index:reindex:all && ./vendor/bin/harbor maho cache:flush

Open the admin panel:

./vendor/bin/harbor open admin

Import an existing database

If you already have a database dump, Harbor provides two additional commands to move databases in and out of the container:

# Import a dump into the database
./vendor/bin/harbor maho db:import backup.sql.tzst [--compression=gzip|zstd|none] [--drop-tables]

# Export the current database to a dump file
./vendor/bin/harbor maho db:export backup.sql.tzst [--compression=gzip|zstd|none]

Compression is not auto-detected: a gzip or zstd dump (even with a plain .sql name) needs --compression=gzip or --compression=zstd. Use --drop-tables to drop and recreate the database before importing, for a clean restore. When pv is available in the container, both commands show a progress bar; zstd is required only when --compression=zstd is requested.

Harbor does not create app/etc/local.xml or replace the Maho installer. Database defaults exposed to the containers are db:3306, database maho, user maho, and password maho; make sure the Maho installation uses the values appropriate for the project.

Quick start and base stack

The default stack contains:

Container Description Endpoint / notes
app FrankenPHP (Caddy) + PHP + Node, serving the Maho application https://localhost:8443/ (via HARBOR_APP_URL)
db MySQL 8.4 db:3306, database/user/password maho
mailpit Mailpit, local SMTP capture and web inbox Web UI http://localhost:8025; SMTP mailpit:1025 inside the Compose network
cron Ofelia, the scheduled-task runner Watches Docker labels

The application URL is controlled by HARBOR_APP_URL and defaults to https://localhost:8443/. The base Mailpit UI defaults to http://localhost:8025; applications send SMTP to mailpit:1025 inside the Compose network.

Project files are generated in .harbor/. Do not edit .harbor/compose.yaml directly; see Customizing the stack to override it.

Main commands

Lifecycle

./vendor/bin/harbor init [--force]
./vendor/bin/harbor up -d [service...]
./vendor/bin/harbor down
./vendor/bin/harbor stop [service...]
./vendor/bin/harbor restart [service...]
./vendor/bin/harbor ps [service...]
./vendor/bin/harbor logs [service]
./vendor/bin/harbor logs -f app
./vendor/bin/harbor build [service...]
./vendor/bin/harbor config
./vendor/bin/harbor doctor

down removes containers and networks and includes disabled profile services as orphans. down -v also removes persistent volumes, including the database volume.

Application tools

./vendor/bin/harbor php --version
./vendor/bin/harbor composer install
./vendor/bin/harbor maho cache:flush
./vendor/bin/harbor bin phpunit
./vendor/bin/harbor test
./vendor/bin/harbor phpunit
./vendor/bin/harbor phpstan

php, composer, maho, and bin execute in app. Unknown commands are delegated to Docker Compose.

Shell and service tools

./vendor/bin/harbor shell
./vendor/bin/harbor shell --service db
./vendor/bin/harbor root-shell
./vendor/bin/harbor exec --service app php -v
./vendor/bin/harbor exec --user root --service app bash
./vendor/bin/harbor mysql --execute='SELECT 1'
./vendor/bin/harbor redis --raw ping
./vendor/bin/harbor open
./vendor/bin/harbor open admin
./vendor/bin/harbor open mailpit

Optional services

How profiles work

Base services and optional profile services are separate. The built-in profiles are redis, adminer, and phpmyadmin. Harbor also reads any custom service you declare with a profiles: key in .harbor/docker.override.yaml or .harbor/docker.install.yaml (inline profiles: [newprofilename]) and manages it exactly like the built-in ones. Profiles are stored as a comma-separated HARBOR_PROFILES value in .harbor/.env.

./vendor/bin/harbor services list
./vendor/bin/harbor services add redis
./vendor/bin/harbor services remove redis
./vendor/bin/harbor up -d
./vendor/bin/harbor ps

services add and services remove update .harbor/.env; they do not start or stop containers. Run up -d to apply a profile change and ps to verify it.

Redis (optional profile: redis)

Property Default
Image redis:7.4
Endpoint redis:6379 (published on host port 6379)
Environment variables HARBOR_REDIS_HOST=redis, HARBOR_REDIS_PORT=6379, HARBOR_REDIS_SESSION_DB=1, HARBOR_REDIS_CACHE_DB=2

The service uses a temporary /data storage with volatile-lfu eviction. Connect from containers with redis:6379, from the host with redis-cli -h 127.0.0.1 -p 6379, or from the CLI with ./vendor/bin/harbor redis ....

Configuration example for local.xml:

<session_save>redis</session_save>
<redis_session>
  <dsn>redis://redis:6379/1</dsn>
  <key_prefix>maho_session:</key_prefix>
</redis_session>
<cache>
  <lifetime>86400</lifetime>
  <backend>redis</backend>
  <backend_options>
    <dsn>redis://redis:6379/2</dsn>
  </backend_options>
</cache>

For the full Redis hosting documentation, see mahocommerce.com/hosting/redis/.

Mailpit (base service)

  • Web UI: http://localhost:${HARBOR_MAILPIT_PORT:-8025}; default http://localhost:8025.
  • SMTP from the host: localhost:${HARBOR_MAILPIT_SMTP_PORT:-1025}; default localhost:1025.
  • SMTP from containers: mailpit:1025.
  • HARBOR_MAILPIT_PORT and HARBOR_MAILPIT_SMTP_PORT can change the published host ports. Mailpit is always part of the base stack.

Adminer (optional profile: adminer)

Property Default
Image adminer:latest
Endpoint http://localhost:8082
Environment variables HARBOR_ADMINER_PORT=8082 (host port), ADMINER_DEFAULT_SERVER=db

The login server field is pre-filled with db; enter user, password, and database manually, using the credentials from .harbor/.env.

phpMyAdmin (optional profile: phpmyadmin)

Property Default
Image phpmyadmin:latest
Endpoint http://localhost:8081
Environment variables HARBOR_PHPMYADMIN_PORT=8081 (host port), PMA_HOST=db, PMA_PORT=3306, PMA_USER=maho, PMA_PASSWORD=maho

Connects to the db service at db:3306 with the credentials from .harbor/.env.

Extending the stack: profiles vs. default services

When you extend and customize your local Harbor installation in .harbor/docker.override.yaml or .harbor/docker.install.yaml, the profiles key controls how a service is started:

  • Additional (opt-in) service: declare profiles: [yourprofile]. The service starts only when the profile is enabled, with ./vendor/bin/harbor services add yourprofile (or by passing --profile yourprofile to up). It is discovered automatically and managed like the built-in profiles.
  • Part of the default stack: omit the profiles key entirely. The service starts on every ./vendor/bin/harbor up, without needing --profile.
services:
  phpredisadmin:
    image: erikdubbelboer/phpredisadmin:latest
    profiles: [phpredisadmin]   # remove this line to always start it
    environment:
      REDIS_1_HOST: redis
    ports:
      - "8084:80"

Additional cron jobs

The base cron service runs Ofelia. It watches Docker labels for the current Compose project through the read-only Docker socket. The app stub defines these Maho jobs:

Both jobs run as user maho and set no-overlap: "true". Start the service explicitly with:

./vendor/bin/harbor cron

Custom jobs are supported through Compose labels in .harbor/docker.override.yaml. Add labels to app (or another service) using Ofelia's ofelia.job-exec.<job-name>.* format, for example:

services:
  app:
    labels:
      ofelia.job-exec.custom.schedule: '@every 10m'
      ofelia.job-exec.custom.command: "sh -c 'cd /app && ./maho your:command'"
      ofelia.job-exec.custom.user: maho
      ofelia.job-exec.custom.no-overlap: "true"

Recreate the service after changing labels with ./vendor/bin/harbor up -d and inspect it with ./vendor/bin/harbor logs cron.

Xdebug

Xdebug is included in the application image: the package Dockerfile installs it together with the core PHP extensions.

Enable it by setting HARBOR_XDEBUG_MODE in .harbor/.env:

HARBOR_XDEBUG_MODE=debug

HARBOR_XDEBUG_MODE accepts off (default), develop, debug, or debug,develop,coverage. Xdebug connects back to the IDE on port 9003 using host.docker.internal; override the host with HARBOR_XDEBUG_CLIENT_HOST if needed (see below).

Verify Xdebug from the running app container:

./vendor/bin/harbor php -m | grep -i '^xdebug$'
./vendor/bin/harbor php -i | grep -E 'xdebug.mode|xdebug.client'

Use the Harbor debug wrapper to run a PHP command or script with Xdebug enabled:

./vendor/bin/harbor debug script.php
./vendor/bin/harbor debug -r 'var_dump(extension_loaded("xdebug"));'

IDE setup

The container mounts the project at /app, so map /app to the project root in your IDE.

VS Code: install the "PHP Debug" extension (xdebug.php-debug) and add this launch configuration (already provided in .vscode/launch.json):

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Listen for Xdebug",
      "type": "php",
      "request": "launch",
      "port": 9003,
      "pathMappings": { "/app": "${workspaceFolder}" }
    }
  ]
}

Start "Listen for Xdebug" (F5), set a breakpoint, then trigger a request in the browser or run ./vendor/bin/harbor debug <script>.

PhpStorm: enable "Listening for PHP Debug Connections" (port 9003), and under Settings > PHP > Servers map the /app directory to the project root.

Linux and Docker Desktop

host.docker.internal normally resolves to the host. On Docker Desktop for Linux it can resolve to an unreachable IPv6 address, and Xdebug logs Network is unreachable. In that case set the IPv4 literal in .harbor/.env:

HARBOR_XDEBUG_CLIENT_HOST=192.168.65.254

💡 Want nicer PHP error pages? Check out empiricompany/maho-ignition, which adds Spatie Ignition error pages, optional AI-generated solutions, and Flare reporting (shown in developer mode).

Dev Containers and VS Code

Generate the Dev Container file with:

./vendor/bin/harbor devcontainer --force

This writes .devcontainer/devcontainer.json. The generated configuration points to ../.harbor/compose.yaml, opens the app service, and uses /app as the workspace folder.

Open the project in VS Code with the Microsoft Dev Containers extension. When VS Code detects the .devcontainer/devcontainer.json file, it prompts you to reopen the project in the configured container.

The generated configuration installs the Maho VS Code extension (mahocommerce.maho) automatically. Install the Microsoft Dev Containers extension locally to reopen the project in the container.

VS Code PHP integration

For the Maho VS Code extension, configure PHP to run through Harbor from the repository root.

Add this setting to .vscode/settings.json:

{
  "maho.phpCommand": "./vendor/bin/harbor php"
}

MCP server

Harbor can run the Maho MCP server over stdio for compatible AI clients.

Start the application stack first, then configure the client to execute Harbor from the repository root:

{
  "mcpServers": {
    "maho-stdio": {
      "command": "./vendor/bin/harbor",
      "args": ["maho", "dev:mcp:start"]
    }
  }
}

Customizing the stack

Customize your local stack by adding Compose fragments to .harbor/docker.override.yaml. Both it and .harbor/docker.install.yaml are merged over .harbor/compose.yaml; never edit the generated file directly.

Add a service

# .harbor/docker.override.yaml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
    ports:
      - "5432:5432"

To start a service only on demand instead of on every up, declare profiles: [name]; see How profiles work for the details.

Override an existing service

# .harbor/docker.override.yaml
services:
  db:
    ports:
      - "3307:3306"

Add an environment variable or a volume

# .harbor/docker.override.yaml
services:
  app:
    environment:
      MY_FLAG: "1"
    volumes:
      - ../data:/data

Enable developer mode

Set MAGE_IS_DEVELOPER_MODE on the app service to run Maho in developer mode:

# .harbor/docker.override.yaml
services:
  app:
    environment:
      MAGE_IS_DEVELOPER_MODE: "1"

After editing, apply the changes with ./vendor/bin/harbor up -d. Add --build when the application image must be rebuilt.

Customization and generated files

  • .harbor/docker.override.yaml is for persistent project or local Compose overrides.

  • .harbor/docker.install.yaml is for install-specific additions and settings.

  • .harbor/stubs/<service>.yaml overrides a matching package stub when present.

  • HARBOR_PHP_EXTENSIONS in .harbor/.env adds extra PHP extensions to the application image, as a space-separated list of install-php-extensions names:

    HARBOR_PHP_EXTENSIONS="gmp imagick"

    Rebuild the image after changing it with ./vendor/bin/harbor up -d --build.

Keep secrets out of versioned override files. init --force can replace generated environment and Compose files.

Harbor does not update Maho's local.xml automatically.

Logs

The application container writes both the web server access log and the PHP error log to stdout/stderr, so everything arrives in a single stream:

./vendor/bin/harbor logs app
./vendor/bin/harbor logs -f app
  • Access log — resources/caddy/26.7.caddyfile enables Caddy's log directive with output stdout: one JSON line per request carrying the method, uri, status, duration and size. Caddy does not log requests by default, so without that directive the container log only shows startup and runtime messages.
  • PHP error log — resources/php/php.ini sets log_errors = On and error_log = /proc/self/fd/2, the container's stderr. error_reporting = E_ALL, display_errors = On and display_startup_errors = On stay enabled for developer mode, so an error is both displayed in the response and written to the log.

Troubleshooting

./vendor/bin/harbor config
./vendor/bin/harbor doctor
./vendor/bin/harbor ps
./vendor/bin/harbor logs app
./vendor/bin/harbor logs cron

Run the launcher from the directory containing vendor/, .harbor/, and the Maho project files. Harbor requires Docker and Docker Compose; this README does not replace their installation documentation.

For developers

To contribute to Harbor itself (forking the repository, cloning it locally, and running the pre-pull-request checks), read the contribution guide in CONTRIBUTING.md.

Package tests

From the Harbor package directory, the shell test suite can run without starting containers:

bash tests/harbor.sh

License

Harbor is open-sourced software licensed under the MIT license.