Raise the stack
$ grim up
The start of a working day, the first run after a clone, the morning after a reboot. grim up is what you type when the project should answer in a browser and does not yet. It is safe to run on a project that is already up: containers that are running are left as they are, and only what is missing gets started.
That same habit makes it the wrong command after an edit to .env. A running container keeps the values it started with. For that there is grim reload.
Usage
$ grim up # everything, Vite included
$ grim up --build # rebuild the app image first
$ grim up app redis # only these services
$ grim up --no-vite # containers onlyRun it from the project directory or anywhere below it. Docker has to be running, and on your own machine mkcert has to be installed.
Arguments and options
Start Docker services + Vite dev server
Usage
grim up [-b|--build] [--no-vite] [-t|--timeout TIMEOUT] [--stop-herd] [--] [<services>...]Arguments
| Argument | Description |
|---|---|
services |
Specific services to start (optional, repeatable) |
Options
| Option | Description |
|---|---|
-b, --build |
Rebuild images before starting |
--no-vite |
Skip starting Vite dev server |
-t, --timeout=TIMEOUT |
Docker compose timeout in seconds |
--stop-herd |
Stop Laravel Herd when its nginx holds ports 80/443 (never done without this flag) |
What it actually does
On your own machine, in this order. Steps 2 and 9 (certificate, HTTPS probe on 443 and TLS chain check) apply to the default proxy mode; in behind-herd the probe hits Traefik's loopback port, one HTTPS request through Herd replaces the chain check (Herd serves https://shop.test (200)), and the summary adds a Proxy line. In behind-proxy the probe hits the loopback port too, there is no chain check (the certificate is your proxy's), a custom certificate is still copied for Vite, and the summary adds a Proxy line. Each step is printed with its result and the time it took.
- Checks Traefik. The one Traefik that routes every project on the machine is started when it is not running, and ports 80 and 443 are checked. Docker holding them is Traefik itself and fine, whether Docker Desktop or OrbStack forwards them. In the default proxy mode Traefik answers
http://with a redirect tohttps://: core builds https URLs, and a page opened on http would have its translations and cookies blocked by the browser. A Traefik started by an older grim is recreated once to pick this up. Laravel Herd's nginx holding them is an error unless you passed--stop-herd; then Herd is stopped and a line after the steps tells you so. Herd merely running in the menu bar, with its nginx stopped, is left alone. - Makes the certificate, unless the machine runs
behind-herd(seegrim configand the recipe Run grim projects beside Laravel Herd). Then Herd's nginx terminates TLS: before the steps,grim upmakes sure Herd proxiesshop.testandmail.shop.testto Traefik's loopback port, printing oneHerd proxy … → http://127.0.0.1:8080line each,(written)when it had to; the first one on a machine asks for your password so Herd can trust its own authority. Vite reads Herd's certificate for the domain. A project with atlsblock of modecustomstops before any step: behind Herd only*.testis served.mkcertissues one forshop.testandmail.shop.testinto~/.grim/traefik/certs, and Traefik is restarted to serve it. On later runs the certificate is checked and left alone when it is still good. - Runs
docker compose up -d, with--buildand the service names when you gave them. The stack is the compose projectshop-dev, named in the first lines ofdocker-compose.yml, and its Traefik routers carry the same name, so production ofshopon the same machine is never touched. Containers this directory ran earlier under its folder name are removed once first. Their volumes stay but are not the new stack's, so a freshredis_datastarts empty. - Generates translations.
php artisan lang:jsin the app container. A failure here does not stop the run. - Starts Vite on your machine, not in a container. See Vite on the host.
- Installs Composer dependencies, only when the app container has no
vendor/autoload.phpyet. On a fresh clone this is the long step. - Runs
php artisan storage:link. - Copies
vendor/to the host, only when the project directory has none, so your editor can resolve classes. - Probes port 443 on
127.0.0.1, then verifies the certificate chain end to end and printsTLS chain verified. When a link of the chain is broken it restarts or recreates Traefik on its own and says which.
The run ends with a table titled Development ready: the URL, Mailpit, Horizon when the project uses it, the Vite address and the duration.
A project with its own certificate, tls.mode set to custom in grim.json with tls.domain, tls.cert and tls.key, skips mkcert and the hosts entries. The certificate is copied to Traefik as it is, and docker-compose.yml is written again so the routed domain follows tls.domain.
One machine can use a domain of its own without changing grim.json, which every other checkout shares: GRIM_LOCAL_DOMAIN=shop.corp.local in the project's .env. It wins over <project>.test and over tls.domain, Mailpit follows as mail.<domain>, and the certificate stays as grim.json says. As with tls.domain, there are no hosts entries and docker-compose.yml is written again on every grim up. Behind Herd it is refused, Herd serves only *.test. Behind your own proxy without a custom certificate grim leaves the VITE_DEVELOPMENT_* keys in .env as you set them.
Linux and Windows add steps of their own. On Linux, the line 127.0.0.1 shop.test mail.shop.test is added to /etc/hosts before anything else, and sudo may ask for your password. A Mac where shop.test does not resolve gets the same line; one with Herd, Valet or dnsmasq is left alone. On Windows the same entries go into the Windows hosts file. Both also make storage/ and bootstrap/cache/ writable inside the container.
Vite on the host
Locally the dev server is a process on your machine, started with Vite's own binary and left running in the background, so its output is in no Docker or OrbStack console. The Vite HMR row of the summary says where to read it, grim logs -f vite, and says not running when the dev server did not come up. grim up waits at most ten seconds for Vite to answer: the slow part of a cold start is the first page, while Vite pre-bundles dependencies, and that shows in the log. It is skipped when the project has no package.json or when you pass --no-vite.
| File | What it holds |
|---|---|
.grim/vite.pid |
The process id grim down uses to stop it. |
.grim/vite.log |
Everything Vite printed. grim logs -f vite reads it; do that when assets do not load. |
public/hot |
The dev server address. While it exists the app loads assets from Vite. |
.env |
Three VITE_DEVELOPMENT_* lines, appended once, pointing Vite at the project's certificate. |
Vite listens on 127.0.0.1:5173. VITE_DEVELOPMENT_BIND in .env changes the address, 0.0.0.0 on a shared box whose developers browse from another machine, and VITE_DEVELOPMENT_PORT the port, for a second project on the same box. A port of its own is strict: when it is taken, Vite stops with an error in the log instead of moving to the next one.
When node_modules is missing, or was written by yarn before the move to pnpm, pnpm install runs first. A Vite left over from an earlier run of the same project is stopped before the new one starts.
On a shared dev box
A project whose grim.json says deployment.kind is vps-dev takes a different and shorter road. There is no mkcert, no hosts file and no Vite process: certificates come from the box's Traefik, and MySQL and Vite are services in the stack.
| Your machine | Shared dev box | |
|---|---|---|
docker-compose.yml |
Left as it is | Written again from the template on every run |
| Database | On the machine, outside the stack | A mysql container |
| Vite | A process on the host | A vite container |
| Migrations | Not run | php artisan migrate --force on every run |
| Address | https://shop.test |
deployment.domain from grim.json |
It checks that the traefik-public network exists before starting anything, and ends with a table titled Dev environment ready. The box itself is raised by grim server:dev:setup.
Pitfalls
Without pnpm on the host, Vite is skipped
No step fails; one yellow line says pnpm not found on host — skipping Vite dev server, the summary table has no Vite HMR row, and pages load built assets or none. Install Node and pnpm (corepack enable pnpm), then run grim up again.
Laravel Herd holds port 80 or 443
Laravel Herd holds :80, :443 (nginx-arm64 (pid 4242)) and grim's Traefik needs them. Run again with --stop-herd to stop Herd (your Herd sites go down until \herd start`), or stop it yourself first: herd stop. Nothing has been started. Choose: grim up --stop-herdstops Herd for you and says so after the steps;herd stop` by hand does the same and Herd stays yours to start again. Earlier versions stopped Herd on every run without asking; this one never stops it without the flag.
Something else holds port 80 or 443
Port(s) 80, 443 are held by a non-Docker process: nginx (pid 910). Free them before running grim (e.g. stop the conflicting service). A Homebrew nginx, Apache or another proxy is listening. Stop it and run grim up again.
mkcert is missing
The certificate step fails with mkcert is not installed or not on PATH and the install command for your system. No project container has been started at that point. Install it and run grim up again; the first run also adds the local authority to the system trust store.
The first start times out
docker compose up gets 300 seconds when the app image has to be built and 120 when it exists. On a slow connection the first build can need more. Pass --timeout=600, or set docker.timeout in grim.json to keep it.
On your own machine grim up does not run migrations. A fresh clone with an empty database answers with an SQL error until you run grim artisan migrate.
After it finishes
Open the URL from the table. Mail the app sends lands in Mailpit at https://mail.shop.test. When a page errors, the next stop is the log.
$ grim logs -f appAt the end of the day, grim down stops it all and keeps your data.
Questions
Can two projects be up at once?
Yes. Each has its own compose project <name>-dev, its own .test name and its own router names, and the one Traefik routes to both. A compose file from an older grim has no name: line yet and keeps the folder name; grim upgrade or grim install --regenerate-docker writes the new one.
Does grim up app still start Vite?
Yes. Service names only narrow what docker compose up starts. Add --no-vite when you want containers and nothing else.
Related spells
grim down
Lower the stack
Stop the project's containers and its Vite server. Volumes stay unless you say otherwise.
grim reload
Apply a changed .env
Recreate every container of the project and clear the config cache, so the app reads the .env as it is now.
grim logs
Read the logs
Show what the containers have been printing, for one service or all of them, on your machine or on a deployed server.
grim doctor
Check your circle
Check every tool grim expects on this machine, and the project you are standing in, and say what to do about what is missing.
Recipes that use it
Set up a dev box for a client
Raise a shared development server, give it a registry token, put the first project on it, let developers in by key, hand over the access card, and later take a developer or a project off again.
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 give the first commit a remote.