Post

Automating Local VMs on macOS (Apple Silicon) with Lima πŸ¦™πŸ

Automating local VMs with Lima (VZ) πŸ¦™πŸ

I wanted a local VM setup on Apple Silicon that’s:

βœ… CLI-first (no clicking around)
βœ… Repeatable (same commands every time)
βœ… Modular (one VM per service: MongoDB VM, Postgres VM, Nginx VM, etc.)
βœ… Safe (no accidental shared disks)
βœ… Idempotent provisioning (safe reruns)

So I built a small framework repo:

Code on GitHub: https://github.com/corbtastik/vm-bakeoff πŸ”—

It uses:


0) What you’ll build 🧱

By the end, you’ll be able to do this:

  • Create a MongoDB VM:
    • VM name: mongodb-vz
    • Disk name: mongodb-data (optional, but recommended for DBs)
    • MongoDB stores data under /data/mongodb
    • Auth enabled + users created
  • Create a Postgres VM:
    • VM name: postgres-vz
    • Disk name: postgres-data
    • Postgres cluster lives under /data/postgres/<major>/main
    • App role + database created
  • Keep host port forwards collision-free using offset ports (manual per VM), e.g.:
    • MongoDB guest 27017 β†’ host 37017
    • Postgres guest 5432 β†’ host 35432

Most importantly: each VM is independent. No β€œone VM running everything” and no β€œoops, two VMs share a disk.” πŸ™…β€β™‚οΈπŸ’Ύ


βœ… Why Lima?

Lima is a great fit for local automation because it’s:

  • YAML-driven 🧩
  • Scriptable ⌨️
  • Supports vmType: "vz" on Apple Silicon 🍏⚑
  • Works nicely with a β€œdriver” model (start/stop/run/provision) πŸ”

One key Lima concept:

Some VM settings are effectively creation-time (β€œbirth-time”).
So the right pattern is: generate VM YAML per VM, then create it.

That’s exactly what this repo does.


1) Repo layout πŸ—‚οΈ

This repo is intentionally structured around two kinds of config:

A) VM configuration (CPU, memory, disk, port forwards)

Each VM has its own file:

  • vms/mongodb.env
  • vms/postgres.env
  • vms/nginx.env (example diskless VM)

These define how the VM runs.

B) Software configuration (MongoDB/Postgres settings)

Each piece of software has its own file:

  • software/mongodb.env
  • software/postgres.env
  • software/nginx.env

These define what gets installed.

And provisioning scripts combine both.


2) Prereqs (host) 🧰

Install Lima and HTTPie:

brew install lima httpie
limactl --version
http --version

3) Deterministic Ubuntu pinning πŸ”’

Cloud images change over time. I want a deterministic VM baseline, so we pin the Ubuntu image SHA256 digest.

This generates a pinned file used by all Ubuntu VMs:

make ubuntu-pin

Under the hood, we fetch the SHA256 for the exact Ubuntu cloud image build and write a pinned config in:

  • platforms/lima/images/ubuntu.env

This gives you a stable foundation: β€œsame inputs β†’ same VM base.”


4) VM lifecycle: make up/down/status/ssh/destroy (per VM) πŸŽ›οΈ

This is the core loop.

Bring up the MongoDB VM

make up VM=mongodb
make status VM=mongodb
make ssh VM=mongodb

Bring up the Postgres VM

make up VM=postgres
make status VM=postgres
make ssh VM=postgres

Stop a VM (no data loss)

make down VM=postgres

Destroy a VM (and its disk, by default) πŸ’£

make destroy VM=postgres

Want to delete the VM but keep its disk (persistence test / rebuild VM config / etc.)?

KEEP_DISK=1 make destroy VM=postgres

5) The disk strategy: optional, per VM πŸ’Ύ

Each VM can choose:

  • HAS_DATA_DISK=1 β†’ create a named Lima disk (<vm>-data)
  • HAS_DATA_DISK=0 β†’ diskless VM (fine for Nginx, utility boxes, etc.)

Inside the guest, the Lima attached disk appears under /mnt/... and is bind-mounted to:

  • /data

So for DB VMs, /data becomes the β€œpersistence contract.”

βœ… MongoDB data goes to /data/mongodb
βœ… Postgres data goes to /data/postgres/...


6) Port forwards: manual β€œoffset style” per VM πŸ”Œ

We define port forwards in each VM’s .env so they’re explicit and collision-free.

Example pattern:

  • MongoDB VM forwards guest 27017 to host 37017
  • Postgres VM forwards guest 5432 to host 35432

That means you can run both at once without conflict 😎


7) Provisioning: MongoDB Community πŸƒ

Once the VM is up, provisioning installs and configures software inside it.

Provision MongoDB VM

make provision-mongodb VM=mongodb

What provisioning does (high level):

  1. Ensures /data exists (and uses persistent disk if configured) πŸ’Ύ
  2. Installs MongoDB Community from MongoDB’s official apt repo πŸƒ
  3. Configures /etc/mongod.conf:
    • dbPath: /data/mongodb
    • log path under /data
    • binds to 127.0.0.1 for safety πŸ”
  4. Creates a root-only secrets file: /etc/todo-secrets.env πŸ”’
  5. Enables auth and reconciles users idempotently:
    • dbAdmin (root on admin)
    • dbUser (readWrite + dbAdmin on todo)
  6. Writes MONGODB_URI to the secrets file
  7. Installs mdb_user and mdb_admin helper aliases 🎯

8) Verify MongoDB βœ…

SSH into the VM:

make ssh VM=mongodb

Confirm data directory

sudo ls -la /data
sudo ls -la /data/mongodb
sudo systemctl status mongod --no-pager

Check secrets

sudo cat /etc/todo-secrets.env

Connect as app user (dbUser)

sudo bash -lc 'source /etc/todo-secrets.env && mongosh "$MONGODB_URI" --eval "db.runCommand({ ping: 1 })"'

Connect as admin (dbAdmin)

sudo bash -lc 'source /etc/todo-secrets.env && mongosh --host 127.0.0.1 --port 27017 --username "$DB_ADMIN_USER" --password "$DB_ADMIN_PASS" --authenticationDatabase admin --eval "db.runCommand({ connectionStatus: 1 })"'

If both work, auth is on and users exist. πŸŽ‰


9) Provisioning: Postgres 🐘

Bring up the Postgres VM and provision it:

make up VM=postgres
make provision-postgres VM=postgres

What provisioning does:

  1. Ensures /data exists (persistent disk if configured) πŸ’Ύ
  2. Installs Postgres packages from Ubuntu repos 🐘
  3. Creates/moves the Postgres cluster to /data/postgres/<major>/main
  4. Configures:
    • listen_addresses = 127.0.0.1
    • port 5432
    • scram-sha-256 auth for localhost
  5. Generates or reuses secrets in /etc/todo-secrets.env:
    • PG_DB, PG_USER, PG_PASS
    • POSTGRES_URI
  6. Creates/updates the role idempotently
  7. Creates the database idempotently (using createdb, because CREATE DATABASE can’t run inside DO) βœ…

10) Verify Postgres βœ…

SSH into the VM:

make ssh VM=postgres

Check secrets

sudo cat /etc/todo-secrets.env

Connect as the app user (todo_pg_user) and create a table

sudo bash -lc 'source /etc/todo-secrets.env && psql "$POSTGRES_URI" -v ON_ERROR_STOP=1 <<SQL
CREATE TABLE IF NOT EXISTS todos (
  id bigserial PRIMARY KEY,
  title text NOT NULL,
  done boolean NOT NULL DEFAULT false,
  created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO todos (title) VALUES (''hello from todo_pg_user'');
SELECT * FROM todos ORDER BY id DESC LIMIT 5;
SQL'

Admin check (superuser)

On Ubuntu, β€œadmin” is the postgres OS user and DB role:

sudo -u postgres psql -c "select current_user, current_database();"

Verify the role and DB exist:

sudo bash -lc 'source /etc/todo-secrets.env && sudo -u postgres psql -tAc "select rolname from pg_roles where rolname='\''$PG_USER'\''"'
sudo bash -lc 'source /etc/todo-secrets.env && sudo -u postgres psql -tAc "select datname from pg_database where datname='\''$PG_DB'\''"'

11) Optional: connect from macOS via forwarded ports 🍏➑️🐧

If your Postgres VM forwards guest 5432 to host 35432, you can connect from macOS like:

psql "postgresql://todo_pg_user:<PG_PASS>@127.0.0.1:35432/todo_pg" -c "select now();"

Same idea for MongoDB if you forward guest 27017 to host 37017:

mongosh "mongodb://dbUser:<DB_USER_PASS>@127.0.0.1:37017/todo?authSource=todo"

(Grab passwords from /etc/todo-secrets.env inside the VM.)


12) Acceptance checklist βœ…βœ…βœ…

  • [βœ…] Ubuntu image is pinned deterministically (digest) πŸ”’
  • [βœ…] Multiple independent VMs can exist: mongodb-vz, postgres-vz, etc. 🧩
  • [βœ…] Disks are per-VM: mongodb-data, postgres-data (no accidental sharing) πŸ’Ύ
  • [βœ…] VMs can be diskless when appropriate (e.g. nginx) πŸͺΆ
  • [βœ…] MongoDB stores data on /data/mongodb and auth works πŸ”πŸƒ
  • [βœ…] Postgres stores data on /data/postgres/... and app role can create tables 🐘
  • [βœ…] Rerunning provisioning is safe (idempotent behavior) πŸ”

Wrap-up 🎬

This repo is intentionally small and boring (in a good way). πŸ˜„
It’s a repeatable pattern you can grow:

  • add more VM configs under vms/
  • add more provisioners under scripts/guest/
  • keep a consistent lifecycle: up β†’ provision β†’ test β†’ down/destroy

If you’re building local demos, POCs, or just want a reliable VM baseline on Apple Silicon… this is a great place to start. πŸ¦™πŸ