Set up a dev box for a client
A dev box is raised once and then mostly left alone, so the work is in the order. The tokens go in first, the project second, the people last. Every command here runs on your machine, and your machine becomes the only one that knows the box.
Before you start
The server is a fresh Ubuntu or Debian machine that accepts your SSH key, and nothing else lives on it. A dev box is not a production server. Traefik takes ports 80 and 443 directly, each project brings its own MySQL container, and everyone works as one shared dev user on code that is edited in place. A server raised by grim server:setup wants the same ports, so the two never share a host.
Each project needs three names that resolve to the box: the domain, vite. in front of it and mail. in front of it. Let's Encrypt issues a certificate for each when the project's containers start, and it has to find the box behind the name.
$ ssh root@203.0.113.10 true
$ dig +short shop.dev.example.com vite.shop.dev.example.com mail.shop.dev.example.com
203.0.113.10
203.0.113.10
203.0.113.10You need two GitHub tokens, each saved in a file of its own. The first belongs to a machine account that can read the project's repositories. It is stored on the box and every developer there acts on GitHub with it, so it must not be yours. The second is a classic token with read:packages and nothing else, for pulling the dev base image.
The project is pushed. The box clones from GitHub, and whatever exists only on your laptop does not arrive.
The ritual, in order
Seven steps. The first two are done once per box, the third once per project, the fourth and fifth once per developer. The last two wait for the day somebody or something leaves.
Raise the box
grim server:dev:setupinstalls Docker, creates thedevuser with your key, starts Traefik and installs grim on the box. That last part needs a token, which is why the team token goes in now. Reading it from a file keeps it out of your shell history.$ grim server:dev:setup 203.0.113.10 --alias=dev1 --email=ssl@example.com --shared-pat="$(cat ~/secrets/dev1-team-token)" Setting up dev VPS: dev1 … ✓ [10/12] Installing grim-cli on VPS (for dev workflow) ✓ [11/12] Seeding shared GitHub auth for dev userThe
GHCR loginstep turns green here although no token was given and nothing was logged in. The next step does it properly. A cloud image that refusesrootneeds--user=ubuntu, or whatever login the provider gave you. A box on an office network with no public DNS takes a certificate instead of--email, and that path is LAN dev box with a custom certificate.Give the box its registry token
The dev image of every project is built on the box from a base image in GHCR.
grim server:ghcr-loginasks for the classic token at a hidden prompt, logs thedevuser in and prints Docker's own answer.$ grim server:ghcr-login --host=dev1 dev1 (203.0.113.10) ✓ Login SucceededSetup accepts the same token as
--ghcr-token, but there a failed login does not stop the run and the token stays in your shell history.Put the first project on it
grim server:dev:add-projectclones the repository into/home/dev/projects/shop, writes a dev.envwith generated passwords, and runsgrim installandgrim upon the box. Expect minutes: dependencies are installed and the image is built there.$ grim server:dev:add-project shop --host=dev1 --repo=acme/shop --domain=shop.dev.example.com $ curl -I https://shop.dev.example.com/api/v1/healthcheckUse the name
grim.jsoncarries, and prefer one without a dash. Withmy-shopthe database and its user are calledmy_shopin.env, while every summary you are shown saysmy-shop. ADNS pre-flightwarning does not stop the run, but the browser gets Traefik's default certificate until the records exist.Run this command once per project. A second run over an existing clone writes your own GitHub token into
.git/config, where everyone on the box can read it, and prints a new database password that MySQL does not accept. New commits arrive bygit pullon the box. If you did run it twice, reset the remote there withgit remote set-url origin https://github.com/acme/shop.gitand take the password from.env.Let the developers in
Access is a public key in the
devuser'sauthorized_keys.grim server:dev:keysstores each key under a label, and the label is how you take it away later. Setup put your own key there without one, so add yourself as well.$ grim server:dev:keys add dev1 --key=~/.ssh/id_ed25519.pub --label=operator $ grim server:dev:keys add dev1 --key=~/keys/anna.pub --label=anna Added key "anna" (SHA256:…) for dev $ grim server:dev:keys list dev1The list shows labelled keys only. Ask each developer to compare the fingerprint with what
ssh-keygen -lfprints for their key. One label per person and device: a shared key can only be taken from everyone at once.Hand over the access card
A developer's machine knows nothing about the box, and these commands answer only on yours. Print the card with
grim server:dev:infoand the SSH block withgrim server:dev:ssh-config, and send both.$ grim server:dev:info shop $ grim server:dev:ssh-config dev1Send the URLs, the project path and the block from
HosttoServerAliveCountMax. Leave out the fiveDBrows. The tunnel on the card leads to a port nothing listens on, because the project's MySQL is not published on the box, and the password is only right until somebody edits.env. Developers reach the database from the box, as Work on a client's dev box describes.Remove a developer
Removing the label stops the next login. A terminal or an editor that is already connected stays connected until it closes.
$ grim server:dev:keys remove dev1 --label=anna Removed key "anna" from dev $ grim server:dev:setup 203.0.113.10 --alias=dev1 --email=ssl@example.com --shared-pat="$(cat ~/secrets/dev1-team-token-new)"The second line is for a parting on bad terms. The team token sits in the
devuser's grim config and everyone on the box could read it. Revoke it on GitHub, create a new one and run setup again, which overwrites the stored token and leaves the rest of the box as it is.Remove a project
grim server:dev:removestops the stack, deletes the volumes and the directory, and forgets the project on your machine. It shows what is about to go and asks first.$ grim server:dev:remove shop Project: shop Path: /home/dev/projects/shop on dev@203.0.113.10 Volumes: WILL BE DROPPED (DB data lost) Repo: WILL BE DELETEDThe database is a volume of the project's own stack, so this deletes it for the whole team, together with every commit that was never pushed. Ask before you answer. To come back to the data later, pass
--keep-volumesand--keep-repotogether. The volume alone is useless without the.envthat holds its passwords.
What you have now
On the box, one Traefik for all projects and one directory per project, each with its own database.
/opt/traefik/ shared, holds ports 80 and 443
/home/dev/.grim/config.json the team token
/home/dev/projects/shop/
.env APP_KEY and the database passwords, mode 0644
grim.json with an uncommitted "deployment" block
docker-compose.yml app, nginx, mysql, redis, vite, mailpitOn your machine, ~/.grim/servers.json holds the server and the dev environment of each project. Nobody else's machine does, so keys, the card, grim server:dev:reload and removal stay with you. The next project starts at step three and the next developer at step four.
When it does not work
“Cannot connect to root@203.0.113.10”
On AWS, GCP and Azure images that is not a firewall. They log in as ubuntu, ec2-user or admin. Pass the login as --user. It needs passwordless sudo.
Setup stops at “Installing grim-cli on VPS”
The installer refuses to go on without a GitHub token, and it got none. Setup passes it --shared-pat, or --ghcr-token when that is all there is. Run setup again with --shared-pat. Steps that already passed are checked and left alone.
“Cannot SSH to dev@203.0.113.10 — make sure your pubkey is added (grim server:dev:keys add).”
Your key is not in the dev user's file. That happens when setup ran with --dev-keys and your key was not in that file, because those keys are used instead of yours. Add it with step four. That command connects with the bootstrap login, not as dev, so it still gets in.
The build on the box cannot pull the base image
unauthorized or denied in the output of grim up means the dev user's GHCR login is missing or its token cannot read the image. Repeat step two with a classic token. A fine-grained token is accepted by GitHub and still cannot pull.
The browser shows Traefik's default certificate
One of the three names did not resolve to the box when the stack started, so Let's Encrypt refused it. Fix the record, wait until dig shows the address, then run grim server:dev:reload shop. The new containers make Traefik ask for the certificates again.
Next recipe
LAN dev box with a custom certificate
Check a certificate from your own authority by hand, raise a dev box that serves it, put a project on it under names one wildcard covers, make developer machines trust the authority, and renew the certificate without downtime.
Read itSpells used here
grim server:dev:setup
Raise a shared dev box
Turn a bare VPS or LAN machine into a shared development server with Docker, Traefik, one dev user and grim itself.
grim server:ghcr-login
Let a server pull images
Log a server's deploy user in to the GitHub Container Registry, so the server can pull the project's private images.
grim server:dev:add-project
Settle a project on the box
Clone a repository onto a shared dev box, give it a dev .env and domains, and start its stack there.
grim server:dev:keys
Let developers onto the box
List, add and remove the labelled SSH keys that let developers log in as the shared dev user of a dev box.
grim server:dev:info
Show a project's dev card
Print the URLs, SSH and VS Code commands, database credentials and container states of a project on a dev box.
grim server:dev:remove
Take a project off the box
Stop a project's stack on a dev box and delete its volumes, its directory and its registration, keeping what you ask to keep.