Skip to content

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 only

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

  1. 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 to https://: 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.
  2. Makes the certificate, unless the machine runs behind-herd (see grim config and the recipe Run grim projects beside Laravel Herd). Then Herd's nginx terminates TLS: before the steps, grim up makes sure Herd proxies shop.test and mail.shop.test to Traefik's loopback port, printing one Herd proxy … → http://127.0.0.1:8080 line 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 a tls block of mode custom stops before any step: behind Herd only *.test is served. mkcert issues one for shop.test and mail.shop.test into ~/.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.
  3. Runs docker compose up -d, with --build and the service names when you gave them. The stack is the compose project shop-dev, named in the first lines of docker-compose.yml, and its Traefik routers carry the same name, so production of shop on 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 fresh redis_data starts empty.
  4. Generates translations. php artisan lang:js in the app container. A failure here does not stop the run.
  5. Starts Vite on your machine, not in a container. See Vite on the host.
  6. Installs Composer dependencies, only when the app container has no vendor/autoload.php yet. On a fresh clone this is the long step.
  7. Runs php artisan storage:link.
  8. Copies vendor/ to the host, only when the project directory has none, so your editor can resolve classes.
  9. Probes port 443 on 127.0.0.1, then verifies the certificate chain end to end and prints TLS 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 app

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

Recipes that use it