Self-hosted service
micromdm/nanomdm avatar
micromdm/nanomdm

NanoMDM: a small Go server for Apple device management

NanoMDM is a minimalist Apple MDM server and library heavily inspired by MicroMDM

693 stars111 forksGoMIT

At a glance

What is it?
NanoMDM is a minimalist Apple MDM server and library written in Go, built as a thin layer between HTTP handlers and storage interfaces. It ships release archives, a container image and a make target, and it deliberately omits SCEP, TLS termination, ADE access and a JSON command API, so it is one component of an MDM stack rather than the whole thing.
Who is it for?
Adopt NanoMDM if you want an MDM server you can read in an afternoon, if you already run or plan to run the surrounding pieces such as a SCEP server and a TLS-terminating proxy, and if you are comfortable submitting raw Plist commands. Do not adopt it expecting a turnkey appliance, since SCEP, TLS, DEP API access, enrollment profiles and the JSON command API are all explicitly outside it.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 10 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A thin layer between HTTP handlers and storage

The architecture description in the README is unusually blunt about what the project is: NanoMDM, at its core, is a thin composable layer between HTTP handlers and a set of storage abstractions. That single sentence tells you the shape of the codebase and the shape of the decisions you will make as an adopter.

The three layers are separated into packages, and the naming maps directly onto that sentence. The front end is a set of standard Go HTTP handlers that handle MDM and API requests, with the core MDM handlers adapting requests to the service layer, and these live in the http package. The service layer is a composable interface for processing and handling MDM requests, where the main NanoMDM service dispatches to the storage layer, and these live under service. The storage layer is the set of interfaces and implementations that store and retrieve MDM enrollment and command data, under storage.

The composability claim in the service layer is the part worth noting. A service that dispatches to a storage interface is a place where you can substitute behaviour, and it is what makes the migration endpoint possible, since migrating enrollments between storage backends or between supported MDM servers is a first-class feature rather than an export script.

The rest of the tree supports that structure: an mdm package for the protocol types, a push package for APNs, an api package, a cli package, a certverify package, cryptoutil for certificate handling, cmd for the two binaries, and tools for the command generator. The project is developed as a library as well as a server, which is why the pkg.go.dev reference badge sits next to the CI badge in the README.

What the storage layer gives you, and why it matters for scaling

The first feature listed is horizontal scaling with zero or minimal local state, with persistence in storage layers and MySQL and PostgreSQL backends provided in the box. This is the feature that determines whether NanoMDM can sit behind more than one instance, and it is also the one that makes the rest of the architecture necessary.

If there is no local state, every request that needs to know something about an enrollment has to fetch it from the shared store. That is exactly why the service layer is a composable interface and why the storage layer is an interface rather than a concrete implementation. The API-driven interaction, where you queue commands and trigger APNs pushes over HTTP, follows from the same constraint: any instance can serve any request, and the state lives in MySQL or PostgreSQL rather than in the process.

The dependency list backs this up. The go.mod file requires github.com/go-sql-driver/mysql at 1.8.1 and github.com/lib/pq at 1.10.9, so both backends are real drivers rather than an abstraction with one implementation. The module also requires github.com/micromdm/nanolib, github.com/micromdm/plist for the raw Plist handling, github.com/RobotsAndPencils/buford for APNs, github.com/smallstep/pkcs7 for certificate handling, github.com/peterbourgon/diskv for the file-backed key-value store, and golang.org/x/net. The Go version declared is 1.18.

The file backend is what the tree hints at: the container declares two volumes, /app/dbkv and /app/db, which correspond to the disk-based key-value store and the database. The not-included list makes the point that the file and mysql backends are easy to inspect and query, which matters when the alternative feature, inspecting enrollment state through an API, is missing.

The not-included list is the most useful section in the README

NanoMDM is described as one component of a functioning MDM server, and at a minimum you need a SCEP server and TLS termination. The README then spells out what it does not include, by way of comparison with MicroMDM, and this list is more informative than the feature list above it.

SCEP is not included. You spin up your own SCEP server or bring your own. TLS is not included either, because you need to provide your own reverse proxy or load balancer that terminates TLS. That second item is a reminder that the HTTPS endpoint an Apple device talks to during enrollment has to exist and be trusted, and that the component which does it is somebody else's job.

ADE and DEP API access is not included, although DEP enrollments themselves are supported, so you can act on an enrollment you have but you cannot query Apple for the Automated Device Enrollment records. Enrollment profiles are not included either, and you need to create and serve your own to devices.

Then the ones that will bite during daily operation. There is no blueprints support, meaning there is no automatic command sending upon enrollment, and everything is driven by a webhook or another integration. There is no JSON command API: commands are submitted in raw Plist form only, with tools/cmdr.py in the repository helping to generate raw commands, and the micro2nano project providing an API translation server between MicroMDM's JSON command API and NanoMDM's raw Plist API. There is no VPP, and no enrollment or device APIs, so there is no ability yet to inspect enrollment details or state.

That last omission is the one that most affects day-to-day operations, because MDM work is mostly answering questions about what state a device is in.

Getting a version: release zips, a container image, or make

There are three ways to obtain the software, and the details of each are in the README.

Release zip files containing the server and supplementals are attached to every GitHub release, and release zips are also published for every main branch commit. The Makefile shows what goes into them. The SUPPLEMENTAL list is tools/cmdr.py and docs/enroll.mobileconfig, so a release archive is the nanomdm binary, the nano2nano binary and those two files. The zip is assembled by linking the targets and zipping them, and the pattern rule builds one archive per platform for the given version.

The platforms are listed in the Makefile. The server is built for darwin-amd64, darwin-arm64, linux-amd64, linux-arm64, linux-arm and windows-amd64.exe, and the same six for nano2nano. The version string is injected at link time with -ldflags -X main.version, taking the value from git describe --tags --always --dirty, so a build from a dirty tree says so.

A Docker container is built and published to the GHCR.io registry for every release, and the commands given are:

dockerfile
FROM gcr.io/distroless/static
ARG TARGETOS TARGETARCH
COPY nanomdm-$TARGETOS-$TARGETARCH /app/nanomdm
COPY nano2nano-$TARGETOS-$TARGETARCH /app/nano2nano
EXPOSE 9000
VOLUME ["/app/dbkv", "/app/db"]
WORKDIR /app
ENTRYPOINT ["/app/nanomdm"]

A container is also published for every main branch commit and tagged :main. The Dockerfile is deliberately minimal, based on gcr.io/distroless/static with no shell and no package manager, copying in the nanomdm and nano2nano binaries for the target platform, exposing port 9000, and declaring volumes for /app/dbkv and /app/db.

Third, if you have a Go toolchain installed you can check out the source and run make. The default target builds both binaries for your own platform, and the Makefile also has a release target for every platform archive and a test target that runs go test -v -cover -race ./...

Multi-tenant APNs and the webhook compatibility layer

Two features are worth separating from the rest because they are about interoperability rather than function.

The first is multiple APNs topics, described as potentially multi-tenant. APNs is the push service Apple devices use, and a topic corresponds to a push certificate, which corresponds to an Apple Developer account. Supporting several means one NanoMDM deployment can serve more than one organisation's devices, which is what a managed service provider needs and what a single-tenant internal deployment does not. The go.mod dependency on buford, the APNs client library, is the mechanism underneath.

The second is the MicroMDM-emulating HTTP webhook and callback, listed alongside enrollment-certificate authorization and API-driven interaction. This is the migration path. Since NanoMDM is heavily inspired by MicroMDM, and since the micro2nano project provides an API translation server between MicroMDM's JSON command API and NanoMDM's raw Plist API, a team that already runs MicroMDM has a documented route to move over rather than starting from nothing. The migration endpoint, which allows migrating MDM enrollments between storage backends or supported MDM servers, is the piece that makes the data move as well as the API calls.

Taken together, these features describe a project designed to be swapped for another one. That is a design position worth weighing: it means the project fits an existing MicroMDM-shaped environment, and it also means the project is deliberately not the most feature-rich option.

Maintenance pace and what a 0.9 release implies

The tagged releases are v0.9.0 from 2025-12-25, v0.8.0 from 2025-05-30 and v0.7.0 from 2025-01-23, so roughly one tagged release every four to five months through 2025. The last push to the repository was on 2026-10-13 and the project is not archived, so work continues between tags.

A version below 1.0 in Go carries a conventional meaning: the API is not considered stable and may change in a backwards-incompatible way in a minor release. For a project whose whole purpose is to be embedded in an infrastructure you operate, that matters, and it is the main argument for pinning a version rather than tracking the branch. The mitigating factor is the project's own shape: it is a server you deploy as a unit, and the boundary is HTTP, so a breaking change at the Go API level is contained to code that imports the library rather than to a deployed fleet.

The licence is MIT, with a LICENSE file at the root, and the project is from the MicroMDM organisation, the same group that maintains the SCEP server the README tells you to use alongside it. The documentation is split into two files referenced from the README: docs/quickstart.md, a quick guide to getting NanoMDM up and running using ngrok, and docs/operations-guide.md, a brief overview of the command-line switches and the HTTP endpoints and APIs available. That ngrok-based quickstart is worth reading first for a specific reason: Apple device enrollment needs a reachable HTTPS endpoint with a trusted certificate, and the quickstart shows the shortest path to that.

Editorial conclusion

Adopt NanoMDM if you want an MDM server you can read in an afternoon, if you already run or plan to run the surrounding pieces such as a SCEP server and a TLS-terminating proxy, and if you are comfortable submitting raw Plist commands. Do not adopt it expecting a turnkey appliance, since SCEP, TLS, DEP API access, enrollment profiles and the JSON command API are all explicitly outside it. Before you commit, work through docs/quickstart.md, which uses ngrok to get a runnable instance, decide whether you need the MySQL or PostgreSQL backend, and read the not-included list in full, because the pieces NanoMDM leaves out are the pieces you will have to build and maintain.

Frequently asked questions

What is NanoMDM and how is it different from MicroMDM?

NanoMDM is a minimalist Apple MDM server and library written in Go, heavily inspired by MicroMDM. It keeps a MicroMDM-emulating HTTP webhook and callback, and the micro2nano project provides an API translation server between MicroMDM's JSON command API and NanoMDM's raw Plist API.

What does NanoMDM not include?

It does not include SCEP, TLS termination, ADE or DEP API access, enrollment profiles, blueprints, a JSON command API, VPP, or enrollment and device APIs. You need your own SCEP server, your own reverse proxy to terminate TLS, your own enrollment profiles, and to submit commands as raw Plist.

Which storage backends does NanoMDM support?

MySQL and PostgreSQL backends are provided in the box, with the go-sql-driver/mysql and lib/pq drivers in the module dependencies, and there is also a file backend. The design goal is horizontal scaling with zero or minimal local state and persistence in the storage layer.

How do I get the latest version of NanoMDM?

Release zip files with the server and supplementals are attached to every GitHub release and published for every main branch commit, a container is published to the GHCR.io registry for each release and each main commit, and if you have a Go toolchain you can check out the source and run make. The container exposes port 9000 with volumes at /app/dbkv and /app/db.

What is the architecture of the NanoMDM server?

It is a thin composable layer between HTTP handlers and storage abstractions: standard Go HTTP handlers in the http package, a composable service layer under service that dispatches to the storage layer, and the storage interfaces and implementations under storage that hold enrollment and command data.

Official sources

  1. Issues
  2. License: MIT
  3. micromdm/nanomdm on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/micromdm-nanomdm.svg)](https://hysenlabs.com/projects/micromdm-nanomdm)

Was this useful?