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. ๐Ÿฆ™๐Ÿ