Skip to content

Assemble the whole project

$ grim install --dry-run

You have a directory with a grim.json in it and not much else: a project cloned from its repository, which carries grim.json and grim.lock and leaves core, modules and themes to be fetched, or one whose grim.json you edited by hand. grim install makes the directory match the file. On a fresh clone that means everything, from fetching core to a running stack with an admin account.

grim create-project ends by running it for you. To add one package to a project that is already installed, grim require is the shorter road. To move an installed project to a newer core, the command is grim update; install never does that.

Usage

$ grim install --dry-run             # resolve and report, touch nothing
$ grim install                       # the whole thing
$ grim install --no-composer         # files and lock only, no build, nothing started
$ grim install --dry-run --strict    # for CI: exit 1 on modules nobody asked for

Run it anywhere inside the project; it walks up to the nearest grim.json. You must be logged in with grim auth:login, and Docker must be running unless you pass --no-docker or --dry-run.

Arguments and options

Install modules and themes from grim.json

Usage

grim install [--dry-run] [--strict] [--no-composer] [--force-composer] [--no-docker] [--stop-herd] [--regenerate-docker] [--no-enable] [--allow-unpublished-theme ALLOW-UNPUBLISHED-THEME]

Options

Option Description
--dry-run Resolve and report what would be installed, without touching the project
--strict With --dry-run: exit 1 when the closure contains modules grim.json does not require
--no-composer Skip composer install
--force-composer Run composer install even when vendor/ already matches composer.lock
--no-docker Skip Docker file generation
--stop-herd Stop Laravel Herd when its nginx holds ports 80/443 (never done without this flag)
--regenerate-docker Rewrite docker-compose.yml and docker/ from the current templates, even when they exist (the old files are kept as *.bak)
--no-enable Skip enabling modules (generate modules-available.php only)
--allow-unpublished-theme=ALLOW-UNPUBLISHED-THEME Resolve the named theme slug from resources/themes/<type>/<slug>/theme.json without contacting GitHub. Used by create-project --new-theme when the publish pipeline could not push the new theme (missing git identity, no GitHub permissions, etc.).

What it actually does

Each step is printed with its duration, and the first failure stops the run.

  1. Resolves core. The core constraint in grim.json becomes a grim-core tag. In a directory without composer.json and config/ the core tree is fetched, at the version grim.lock records when there is a lock, and the step reads Installing grim-core. Where they exist it reads Resolving grim-core and core on disk is left alone. Either way the root README.md and CLAUDE.md are the project's, not core's: they are written when missing (short, about this instance) and never overwritten once they are yours, except that a CLAUDE.md without the @AGENTS.md import gets it as its first line (that import loads grim-core's agent map), and a README or CLAUDE.md an earlier grim wrote and nobody edited — a README from before model A among them — is replaced by the current one.
  2. Checks package availability. Every module and theme in grim.json is looked up before anything is cloned. The same check runs again on the full closure after step 3.
  3. Resolves dependencies. The requires of grim.json, then the requires of every module.json they lead to, each to the version grim.lock records. A module the lock does not know yet gets the newest tag its constraint allows on the project's train.
  4. Installs modules. One plain copy of its release per module under src/<Module>/. A module directory already in the project, as after a git clone, is left as it is.
  5. Installs themes. Every theme of every slot, into resources/themes/<type>/<name>. The first of a slot is the active one.
  6. Generates configuration. The module lists, the Docker files and .env, each under the conditions in the table below. An empty APP_KEY is filled in, and shared secrets from the registry are written into .env.
  7. Writes grim.lock. Train, core version and commit, every module with version, commit and requires, every theme.
  8. Creates the local database. It logs in with DB_USERNAME, DB_PASSWORD and DB_PORT from .env (host.docker.internal means 127.0.0.1 here), so a MariaDB with its own user works once those lines are right. When that login fails, or it may not create the database, the install goes on without one: files, image and dependencies still land, step 10 is skipped, and a yellow Database unreachable as … above the summary names the login it tried. Put the right DB_* lines into .env and run grim install again; the second run finds the database and does step 10. Without a mysql client on the machine nothing is checked, and a warning after the steps says so. A DB_HOST that is a compose service or another server is left alone. The name is DB_DATABASE from .env, reduced to lowercase letters, digits and underscores.
  9. Builds and starts. Builds the image, runs composer install in a one-off container unless vendor/ already matches composer.lock, makes sure the local Traefik is up, starts the stack and waits for the app container. Then pnpm install and a Vite build on your machine, when pnpm is there (npm for a project that ships a package-lock.json). When the install writes or changes pnpm-lock.yaml in a project with a git repository, the summary ends with pnpm-lock.yaml written — commit it. or pnpm-lock.yaml changed — commit it.
  10. Migrates, generates translations, creates an admin. php artisan migrate --force, lang:js and users:admin, all in the app container.

The closing table shows project, core, the number of modules and themes and, only when the account was created, the admin email admin@<project domain> with a generated password. With --no-composer, steps 9 and 10 do not run at all.

The admin password is printed once and cannot be shown again. Copy it before you clear the terminal. grim user makes a new account if it is lost.

Reading the dry run

--dry-run needs no Docker and writes nothing. It prints a table of every module the install would land, with its version and a why column: grim.json, or the modules that require it. Then the themes with (active) on the first of each slot, the core constraint as written, and a total.

Between the table and the themes sits the part worth reading when you take over a project.

Resolved but not in grim.json:
  `payments` required by `cart`

Each line is a module the project would get without having asked for it. That is normal for a dependency. It becomes a finding when the app uses the module directly: then it belongs in grim.json. With --strict a non-empty section ends the run with exit code 1 and --strict: 1 module(s) resolved that grim.json does not require.

What it generates, and when

Files Written
grim.lock, config/modules-available.php On every run.
docker-compose.yml, .dockerignore, and in docker/: Dockerfile.dev, Dockerfile.production, nginx-dev.conf, .env.docker.example Only when docker-compose.yml or docker/Dockerfile.dev is missing, or with --regenerate-docker. Never with --no-docker.
docker/nginx-production.conf, docker/php-override.ini Overwritten on every run without --no-docker.
.env, storage/config/modules-enabled.php Only when missing. .env is a copy of docker/.env.docker.example; the enabled list is a copy of the available one.

An installed project keeps its compose file and Dockerfiles through any number of installs. Bringing those up to date is the job of grim upgrade, or of --regenerate-docker, which rewrites them from the current templates and leaves the old ones beside them as *.bak.

docker-compose.override.yml is yours. grim never writes it, and the compose commands grim runs read it after docker-compose.yml, the same as a bare docker compose would.

Install reads them on the way past: a Dockerfile that builds FROM an image in ghcr.io that is not one of grim's base images is named at the end of the run, with that flag as the way out. It is what a project carried over from another brand looks like, and the image it names has usually stopped existing.

Pitfalls

Install does not move an installed core

Core 2.4.2 is installed but 2.5.0 satisfies "~2.4" — run `grim update` to move onto it. is a notice, not an error. Once core is on disk, install records the version the lock already had and changes nothing under it. Run grim update --dry-run when you want the move.

The lock wins over a newer tag

Install reproduces, it does not move. Core, modules and themes land at the versions in grim.lock, so a fresh clone matches what your colleagues committed even when newer tags exist. Only a unit the lock has never seen is resolved fresh, and then within the train the lock names. Newer versions are the business of grim update.

A constraint in grim.json that you tightened by hand can exclude the locked version. Install then stops with No version of module 'cart' satisfies constraint '~2.5' and lists the locked version as the only one available. Run grim update, which rewrites the lock.

One unavailable package stops everything

1 package cannot be installed:

  webhooks is archived — it is part of core since 2.5; remove it from grim.json

Nothing was installed. Fix grim.json (or request access) and run the command again.

There is no partial install. The message names the remedy for each package: remove it from grim.json, or ask for access to a private one.

Shared secrets replace values you set yourself

A key that exists both in .env and in the registry gets the registry's value on every install. Keep project-specific values under names the registry does not use. When the registry cannot be reached the step is skipped without a word; -v prints the reason.

A missing PHP extension is only a warning

⚠ php extension "soap" is missing — required by invoices does not fail the run. The check reads php.extensions from the installed modules and compares it with php -m in the app container, or on your machine when the container is not up. Add the extension to the image before you use those modules.

After it finishes

The stack is already up; grim up adds the Vite dev server. When grim.json no longer requires modules that are still in src/, install lists them and offers to delete them, which is grim prune run for you. Then commit the lock.

$ grim up
$ git add grim.lock

Questions

Do I run install after every git pull?

When grim.json or grim.lock changed, yes: install lands the modules and themes the new lock records. It does not move a core that is already on disk. When the lock names a newer core or train, run grim update instead.

Why did install not enable my new module?

modules-enabled.php is copied from the available list only when it does not exist. On an installed project, grim require enables what it adds; after a hand edit of grim.json, add the provider line yourself.

Recipes that use it

The idea underneath