Skip to content

Liber Tertius · Chapter 6 · Shared dev boxes

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.

≈30 min 7 steps A fresh Ubuntu or Debian server Three DNS records per project A GitHub token of a machine account
Source

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

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

  1. Raise the box

    grim server:dev:setup installs Docker, creates the dev user 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 user

    The GHCR login step turns green here although no token was given and nothing was logged in. The next step does it properly. A cloud image that refuses root needs --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.

  2. 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-login asks for the classic token at a hidden prompt, logs the dev user in and prints Docker's own answer.

    $ grim server:ghcr-login --host=dev1
      dev1  (203.0.113.10)
        ✓ Login Succeeded

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

  3. Put the first project on it

    grim server:dev:add-project clones the repository into /home/dev/projects/shop, writes a dev .env with generated passwords, and runs grim install and grim up on 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/healthcheck

    Use the name grim.json carries, and prefer one without a dash. With my-shop the database and its user are called my_shop in .env, while every summary you are shown says my-shop. A DNS pre-flight warning 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 by git pull on the box. If you did run it twice, reset the remote there with git remote set-url origin https://github.com/acme/shop.git and take the password from .env.

  4. Let the developers in

    Access is a public key in the dev user's authorized_keys. grim server:dev:keys stores 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 dev1

    The list shows labelled keys only. Ask each developer to compare the fingerprint with what ssh-keygen -lf prints for their key. One label per person and device: a shared key can only be taken from everyone at once.

  5. 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:info and the SSH block with grim server:dev:ssh-config, and send both.

    $ grim server:dev:info shop
    $ grim server:dev:ssh-config dev1

    Send the URLs, the project path and the block from Host to ServerAliveCountMax. Leave out the five DB rows. 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.

  6. 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 dev user'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.

  7. Remove a project

    grim server:dev:remove stops 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 DELETED

    The 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-volumes and --keep-repo together. The volume alone is useless without the .env that 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, mailpit

On 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 it

Spells used here