Bring an existing app onto grim
The conversion reads three files and brings one. Everything else in the new directory is rebuilt from packages, so the work is in finding out what the old app carried that no package does. Two stops are built in for that, and a third keeps the install away from a database you still need.
Before you start
This is for an app that predates grim.json: an existing instance whose modules are listed in storage/config/modules-enabled.php and whose hosts are in deploy.php. The conversion runs once. Afterwards grim.json is the source of truth, and the instance is only a place to copy files from.
The instance is never written to, and it does not have to be running. Every module it enables has to exist as a package you can read, so be logged in. Without a stored token the modules are not checked at all, and the first failure comes later and says less.
$ ls ~/Sites/shop/storage/config/modules-enabled.php ~/Sites/shop/deploy.php
$ grim auth:whoami
$ cd ~/codeStand where the new project directory should appear. When the instance sits in that same directory under the project's name, pass another name as the argument.
The ritual, in order
Seven steps. The first writes a new directory, the fifth fills it, and nothing before the fifth touches Docker or a database.
Convert without installing
grim create-projectwith--from-instancereads the enabled modules, six keys of.envand the host blocks ofdeploy.php. It stops after writing: a conversion never runs the install itself, because the copied.envstill names the old database.$ grim create-project --from-instance=~/Sites/shop Reading instance: /Users/anna/Sites/shop ⚠ Cart is core, omitted from requires ✓ Created shop/grim.json 6 modules, core ~2.5, themes: frontend=acme admin=float ✓ Copied .env from the instance (unchanged) ⚠ .env was copied unchanged: DB_* still points at the instance's database. grim install migrates and creates an admin there. Point DB_* at a copy first.Read the
⚠lines.is core, omitted from requiresis a module that has since moved into core and needs no entry.No THEME_ADMIN in .envmeans the admin theme fell back tofloat. The project name comes from the matchinghost()block indeploy.php, not from the directory, and a name argument changes only the directory that is created.Read the plan
Nothing is written and Docker is not needed. The dry run of
grim installresolves every module to a version and says, in thewhycolumn, who asked for it.$ cd shop $ grim install --dry-run Dry run — no changes will be made +----------+---------+----------------------+ | module | version | why | +----------+---------+----------------------+ | invoices | 2.5.0 | grim.json | | payments | 2.5.1 | required by invoices | +----------+---------+----------------------+ Resolved but not in grim.json: `payments` required by `invoices` Themes: frontend acme 2.5.0 (active) admin float 2.5.0 (active) Core: ~2.5 Total: 2 modules would be installedResolved but not in grim.jsonis the section to read. Each line is a module the old app never enabled and the new project would get anyway, because a module it does enable requires it. A theme line that saysno match forhas no release on this train yet.Settle the modules nobody asked for
An extra module is normal when it is only a dependency. It is a finding when your own code or templates use it directly. Then it belongs in
grim.json. Add it underrequireswith the same constraint as its neighbours, and let the strict dry run confirm the list is empty.$ grim install --dry-run --strict … --strict: 1 module(s) resolved that grim.json does not require.That line comes with exit code 1, so a checklist or a script can gate on it. No line and exit code 0 mean
grim.jsonnames everything the project will load. Leaving a pure dependency out is a fair choice too. Then skip the strict run. Mind one difference: the old app ran only what it had enabled, and a fresh install enables every module it lands.Point the copied .env at this machine
The
.envwas copied byte for byte and install leaves an existing.envalone. It still describes the old app's surroundings. Inside a container127.0.0.1is the container, the cache is a service calledredis, and mail goes tomailpit.$ grep -E '^(APP_URL|DB_|REDIS_HOST|MAIL_HOST|MAIL_PORT)=' .envSet
APP_URL=https://shop.test,DB_HOST=host.docker.internal,REDIS_HOST=redis,MAIL_HOST=mailpitandMAIL_PORT=1025. KeepAPP_KEY, or everything the old app encrypted becomes unreadable.The install runs
php artisan migrate --forceand creates an admin account in whatever databaseDB_*names. Copied unchanged, that is the old app's database. Load a dump into a new database and put its name inDB_DATABASEbefore the next step.Install
Now the real run. It fetches core, checks out the modules and themes, writes the Docker files and
grim.lock, builds the image, migrates and creates an admin.$ grim installThe closing table carries the
AdminandPasswordrows once. With a database that already has your own admin, ignore them. Keys that exist in both your.envand the registry's shared secrets now hold the registry's value, so compare.envwith the instance's if an integration stops answering.Move your own files in
The conversion brought no code, no uploads and no configuration besides
.env. Whatever the old app had that neither core nor a package ships is still in the instance. Copy it, and tell git about it: the project's.gitignoreignores everything it does not list.$ diff -rq ~/Sites/shop/config config $ rsync -a ~/Sites/shop/storage/app/ storage/app/ $ cp ~/Sites/shop/config/shop.php config/ $ printf '!/config/\n/config/*\n!/config/shop.php\n' >> .gitignoreBe strict about what counts as yours. A file that differs because the instance is older is not yours. The next update would own it again. Uploads belong in the project directory and not in git.
Bring it up, test, commit
grim upadds the certificate and Vite.grim testruns the suite against a database of its own,shop_test. Then the first commit.$ grim up $ grim test $ git init $ git add .gitignore grim.json grim.lock config/shop.php $ git status --short $ git commit -m "chore: move shop onto grim"git statusshould show.gitignore,grim.json,grim.lockand the files you allowed in step six. Everything else,src/,docker/,vendor/and.envincluded, is ignored and stays out.
What you have now
A project directory next to the old one, answering at https://shop.test, that git clone and grim install can rebuild anywhere. grim.json already carries deploy.domain, and deploy.environments when deploy.php had a staging host next to the production one. It carries no server address. Where the project deploys is decided when you register it on a server.
The instance is untouched and still runs. Retire it when the new project has served a release. When you wonder later why a module is there, grim why answers from the lock.
When it does not work
The directory is not recognised as an instance
The run stops before reading anything when storage/config/modules-enabled.php is missing, and ends with Point --from-instance at the instance root (the directory holding artisan and deploy.php). Give it the root, not public/ or a release subdirectory.
“deploy.php declares 3 hosts and none is named 'shop3'”
The directory name matched no host, so grim cannot tell which site this is. The message lists the hosts it found. Pass the right one as --host=shop. A name deploy.php does not know is accepted too, and the domain then comes from APP_URL.
“Module Foo (foo) has no package”
it is not in the registry, or archived, or private without access. Every such module is listed and nothing is written. A module that lives only inside the instance has to be published as a package first. One you no longer use comes out of the instance's enabled list. For a private one, ask for access and run the conversion again with --refresh-registry.
“Not authenticated. Run grim auth:login first.”
The dry run resolves versions on GitHub and needs the token. The conversion itself only warned about it, which is why this shows up one step later.
The migrations fail at once
The app container could not reach the database. DB_HOST still says 127.0.0.1 or localhost. Finish step four and run grim install again.
Next recipe
Move a project to a new train
Read the train plan, decide what core stopped shipping, update, renew the infrastructure files, and commit a lock you have tested.
Read itSpells used here
grim create-project
Conjure a project
Create a project directory with a grim.json on the current train and install it, or write the grim.json for an application that already runs.
grim install
Assemble the whole project
Turn grim.json into a working project by fetching core, every module and theme, writing grim.lock, building the image and starting the stack.
grim why
Ask why a module is here
Show the chain of requirements that leads from grim.json to an installed module, and what that module requires in turn.
grim up
Raise the stack
Start the project's containers and its Vite dev server, with a trusted certificate and a working address, and say where to open it.
grim test
Run the test suite
Run the project's tests in the app container against a separate test database that grim creates and migrates for you.
Related recipes
Create a project
Answer two questions, let the install run, bring the stack up, sign in as the generated admin, run the suite once and commit the three files that are the project.
Add, trace and remove modules
Find a module in the catalogue, require it on the project's train, trace what came along with it, and later remove it, sweep the leftovers and write the lock again.