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:
- Lima ๐ฆ for VM lifecycle on macOS
- Apple Virtualization.framework ๐ via
vmType: vz(native, not emulation) - Ubuntu ARM64 cloud images from Canonical ๐ง
- Separate provisioning scripts for:
- MongoDB Community ๐ (I work for MongoDB, soโฆ obviously ๐)
- Postgres ๐
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
- VM name:
- Create a Postgres VM:
- VM name:
postgres-vz - Disk name:
postgres-data - Postgres cluster lives under
/data/postgres/<major>/main - App role + database created
- VM name:
- Keep host port forwards collision-free using offset ports (manual per VM), e.g.:
- MongoDB guest
27017โ host37017 - Postgres guest
5432โ host35432
- MongoDB guest
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.envvms/postgres.envvms/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.envsoftware/postgres.envsoftware/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
27017to host37017 - Postgres VM forwards guest
5432to host35432
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):
- Ensures
/dataexists (and uses persistent disk if configured) ๐พ - Installs MongoDB Community from MongoDBโs official apt repo ๐
- Configures
/etc/mongod.conf:dbPath: /data/mongodb- log path under
/data - binds to
127.0.0.1for safety ๐
- Creates a root-only secrets file:
/etc/todo-secrets.env๐ - Enables auth and reconciles users idempotently:
dbAdmin(root onadmin)dbUser(readWrite + dbAdmin ontodo)
- Writes
MONGODB_URIto the secrets file - Installs
mdb_userandmdb_adminhelper 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:
- Ensures
/dataexists (persistent disk if configured) ๐พ - Installs Postgres packages from Ubuntu repos ๐
- Creates/moves the Postgres cluster to
/data/postgres/<major>/main - Configures:
listen_addresses = 127.0.0.1- port
5432 scram-sha-256auth for localhost
- Generates or reuses secrets in
/etc/todo-secrets.env:PG_DB,PG_USER,PG_PASSPOSTGRES_URI
- Creates/updates the role idempotently
- Creates the database idempotently (using
createdb, becauseCREATE DATABASEcanโt run insideDO) โ
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/mongodband 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. ๐ฆ๐