Skip to content

Liber Tertius · Chapter 2 · Starting a project

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.

≈25 min 7 steps The instance directory on this machine A machine where grim doctor passes A spare copy of its database
Source

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 ~/code

Stand 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.

  1. Convert without installing

    grim create-project with --from-instance reads the enabled modules, six keys of .env and the host blocks of deploy.php. It stops after writing: a conversion never runs the install itself, because the copied .env still 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 requires is a module that has since moved into core and needs no entry. No THEME_ADMIN in .env means the admin theme fell back to float. The project name comes from the matching host() block in deploy.php, not from the directory, and a name argument changes only the directory that is created.

  2. Read the plan

    Nothing is written and Docker is not needed. The dry run of grim install resolves every module to a version and says, in the why column, 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 installed

    Resolved but not in grim.json is 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 says no match for has no release on this train yet.

  3. 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 under requires with 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.json names 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.

  4. Point the copied .env at this machine

    The .env was copied byte for byte and install leaves an existing .env alone. It still describes the old app's surroundings. Inside a container 127.0.0.1 is the container, the cache is a service called redis, and mail goes to mailpit.

    $ grep -E '^(APP_URL|DB_|REDIS_HOST|MAIL_HOST|MAIL_PORT)=' .env

    Set APP_URL=https://shop.test, DB_HOST=host.docker.internal, REDIS_HOST=redis, MAIL_HOST=mailpit and MAIL_PORT=1025. Keep APP_KEY, or everything the old app encrypted becomes unreadable.

    The install runs php artisan migrate --force and creates an admin account in whatever database DB_* names. Copied unchanged, that is the old app's database. Load a dump into a new database and put its name in DB_DATABASE before the next step.

  5. 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 install

    The closing table carries the Admin and Password rows once. With a database that already has your own admin, ignore them. Keys that exist in both your .env and the registry's shared secrets now hold the registry's value, so compare .env with the instance's if an integration stops answering.

  6. 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 .gitignore ignores 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' >> .gitignore

    Be 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.

  7. Bring it up, test, commit

    grim up adds the certificate and Vite. grim test runs 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 status should show .gitignore, grim.json, grim.lock and the files you allowed in step six. Everything else, src/, docker/, vendor/ and .env included, 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.