Skip to content

Defining a Stack

A stack class extends Mozex\Compose\Stack and lives in the same directory as its compose file. The quickest way to get one is the scaffolder:

php artisan compose:make meilisearch

It writes app/Docker/Meilisearch/MeilisearchStack.php, a starter docker-compose.yml, and a .gitignore containing .env. The compose file's name: and container_name: carry your app's name (shop-meilisearch for an app called Shop), so two apps on one server never sweep each other's containers; that prefixed name is what compose:logs and the other commands take. Pass --path= to put it somewhere else; the namespace is derived from your composer.json autoload map, so Modules/Search/Docker works as well as app/Docker.

A real example

This stack runs Meilisearch for a Laravel Scout app. The port comes from the one URL the app already dials, so the app and the container can never disagree on it:

namespace App\Docker\Meilisearch;

use Illuminate\Support\Facades\Config;
use Mozex\Compose\Stack;

class MeilisearchStack extends Stack
{
    public function environment(): array
    {
        return [
            'MEILISEARCH_KEY' => Config::get('scout.meilisearch.key'),
            'MEILISEARCH_PORT' => parse_url(Config::get('scout.meilisearch.host'), PHP_URL_PORT) ?: 7700,
        ];
    }

    public function enabled(): bool
    {
        return (bool) Config::get('services.meilisearch.manage_container', false);
    }

    public function wait(): ?int
    {
        return 60;
    }
}

manage_container is an ordinary config key, so give it an env variable in config/services.php:

'meilisearch' => [
    'manage_container' => (bool) env('MANAGE_MEILISEARCH_CONTAINER', false),
],

With it false on a laptop that runs Meilisearch natively, the deploy pipeline stays the same and the stack is skipped.

Every method and its default

You only override what differs from the default.

Method Default What it controls
environment() [] Values written to the env file before every redeploy. Scalars, null, backed enums, and Stringable objects.
name() compose name:, else the directory name The Compose project name. Lowercase letters, digits, - and _, starting with a letter or digit.
directory() where the class file lives The stack directory.
composePath() first of compose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml in the directory The compose file.
containerNames() every container_name: in the compose file Names force-removed before up, with ${VAR} resolved through interpolationValues().
interpolationValues() shell, then the env file, then COMPOSE_PROJECT_NAME What ${VAR} in the compose file resolves against, the way compose resolves it.
enabled() true Whether this host manages the stack. Read a config value here.
profiles() [] Compose profiles to activate, such as a production-only TLS sidecar.
build() false Run compose build --pull first and pass --build to up.
pull() true Run compose pull before the sweep.
wait() null Seconds for up --wait --wait-timeout. Null returns as soon as the containers are created.
timeout() config timeouts.up (120) Seconds allowed for up. Raised automatically to wait() + 30 when waiting.
pullTimeout() config timeouts.pull (300) Seconds allowed for pull.
buildTimeout() config timeouts.build (600) Seconds allowed for build.
linkPath() {link_directory}/{name}, dashes as underscores Where the operator link points. An empty string disables the link for this stack.
host() config docker.host DOCKER_HOST for this stack.
context() config docker.context Docker context for this stack.

Runtime helpers

A stack object also talks to the running containers, which is what your own commands and health checks use:

use Mozex\Compose\Facades\Compose;

$stack = Compose::stack('meilisearch');

$stack->isRunning();                       // every container running, or finished as a one-shot
$stack->isHealthy();                       // running, and every healthcheck reports healthy
$stack->status()->containers;              // ContainerStatus objects: name, service, state, health, ports
$stack->logs(service: 'meilisearch', tail: 50);
$stack->exec('meilisearch', ['ls', '/meili_data']);
$stack->down(volumes: false);

exec() is the hook for maintenance that has to run inside a container. A gateway that stores per-session upload folders in a named volume, for example, prunes them from a scheduled command with $stack->exec('guacd', ['find', '/drive', '-type', 'f', '-mmin', '+1440', '-delete']).

A stack with a production-only sidecar

Profiles let one compose file serve local development and production. This gateway runs a Caddy TLS origin only where tls_enabled is on:

class GatewayStack extends Stack
{
    public function environment(): array
    {
        return [
            'GATEWAY_PORT' => Config::get('gateway.port'),
            'GATEWAY_DOMAIN' => Config::get('gateway.domain'),
            'JSON_SECRET_KEY' => Config::get('gateway.secret'),
        ];
    }

    public function profiles(): array
    {
        return Config::get('gateway.tls_enabled') ? ['tls'] : [];
    }
}

The caddy service in the compose file declares profiles: [tls], so a local compose:redeploy never starts it.

Scroll to top