# How to create a DMG installer for a Mac app

> Package your Mac app in a drag-to-Applications DMG, sign and notarize it, verify the download, and attach it to a GitHub release.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-10-22 | Topics: [Mac](https://flaviocopes.com/tags/mac/) | Canonical: https://flaviocopes.com/macos-dmg-installer/

A DMG gives your Mac app a familiar installation screen. Open the download, drag the app to Applications, and eject the disk image.

You can make that window with the tools macOS already includes. The useful part is getting the release around it right: the app inside needs its own valid signature, and the file people download needs notarization and testing.

We'll prepare the disk image, arrange its icons, and publish it as a GitHub release asset. I'll use **Note Repo** as the app name in the commands. Substitute your app's name and signing identity when following along. These commands describe a packaging workflow; they don't change how Note Repo is currently distributed.

If you haven't built the `.app` bundle yet, the free [Ship macOS Apps course](https://flaviocopes.com/courses/ship-macos-apps/) covers release builds and signing first.

## What a DMG installs

A disk image mounts as a volume in Finder. The `.app` is a directory containing your executable and resources. The Applications icon in the window is a link to the user's `/Applications` directory.

Dragging the app onto that icon copies the bundle there. The disk image doesn't need a script to perform the installation.

That suits an app that can run from its bundle. If your product needs privileged installation steps, a system service, or files installed in several protected locations, work out the appropriate installer and authorization flow separately. A drag-and-drop window doesn't perform those steps for you.

A ZIP is also a valid way to distribute an app. I'd add a DMG when a clearer installation window helps people, while keeping a ZIP if an updater already expects one. My [GitHub Releases updater](https://flaviocopes.com/mac-app-updates/) consumes ZIP assets, so replacing its ZIP with a DMG would break that contract.

## Start with the release app

For this walkthrough, put the finished app here:

```text
dist/
  Note Repo.app
```

Use a release build. Check the displayed version, bundle identifier, minimum macOS requirement, and supported architectures before packaging. A disk image preserves a mistake just as reliably as a correct build.

The app needs a **Developer ID Application** signature for direct distribution. That certificate requires paid Apple Developer Program membership. Publishing source code can still let other people build the app locally without paying for their own membership. [Free and paid Apple developer accounts](https://flaviocopes.com/apple-developer-free-vs-paid/) have different distribution capabilities.

List the signing identities available on your Mac:

```bash
security find-identity -v -p codesigning
```

For my releases, the identity is `Developer ID Application: Flavio Copes (DGFKNTAG99)`. Your certificate will contain your own name and team identifier.

Prefer configuring signing and Hardened Runtime in the build itself. For a small app with no embedded frameworks, helpers, or special entitlements, the signing command has this shape:

```bash
codesign --force --options runtime --timestamp \
  --sign 'Developer ID Application: Flavio Copes (DGFKNTAG99)' \
  'dist/Note Repo.app'
```

Don't apply that command blindly to an Electron app or a bundle with nested executables. Those components need signing in the correct order, with the entitlements each component needs. Your build system's distribution workflow should handle them.

[Apple advises against using `--deep` as a signing shortcut](https://developer.apple.com/library/archive/technotes/tn2206/_index.html#//apple_ref/doc/uid/DTS40007919-CH1-TNTAG203). It's useful in the verification command below, where we want to inspect nested code:

```bash
codesign --verify --deep --strict --verbose=2 'dist/Note Repo.app'
codesign --display --verbose=4 'dist/Note Repo.app'
```

Check the signing authority and team identifier in the second command's output. A successful signature check alone doesn't establish that you used the intended certificate.

[Apple's notarization requirements](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution) also cover secure timestamps, Hardened Runtime, and development entitlements that must be removed. Resolve those in the app before wrapping it in a disk image.

## Prepare a clean staging folder

We need a directory containing only what should appear in the installation window.

Run these commands in the same terminal session:

```bash
mkdir -p dist
DMG_STAGE=$(mktemp -d /tmp/note-repo-dmg.XXXXXX)
ditto 'dist/Note Repo.app' "$DMG_STAGE/Note Repo.app"
ln -s /Applications "$DMG_STAGE/Applications"
```

`ditto` copies the bundle while preserving its structure. The symbolic link points at `/Applications` on the Mac where the image is mounted. We aren't copying the Applications folder into the download.

Inspect the staging folder before proceeding:

```bash
ls -la "$DMG_STAGE"
readlink "$DMG_STAGE/Applications"
```

It should contain the app and the Applications link. Keep source files, test fixtures, personal documents, and credentials out of this directory.

Copying the finished app into staging also protects the original build from the Finder layout work. From this point, avoid editing anything inside the app bundle. Changing a signed resource can invalidate its signature.

## Create an editable disk image

We'll make a writable image while arranging the window. Later we'll convert it into the read-only file people download.

Use `hdiutil`, which ships with macOS:

```bash
hdiutil create \
  -volname 'Note Repo' \
  -fs HFS+ \
  -format UDRW \
  -srcfolder "$DMG_STAGE" \
  'dist/Note Repo-layout.dmg'
```

`UDRW` creates the editable image. The explicit filesystem choice avoids tying this small installation volume to the filesystem of the source folder.

Mount it:

```bash
hdiutil attach 'dist/Note Repo-layout.dmg'
```

Read the mounted path in the output. Normally it's `/Volumes/Note Repo`. If another volume already has that name, macOS may choose a different path. Use the path it prints in the next steps.

## Arrange the drag-to-Applications window

Open the mounted volume in Finder. Switch to icon view with **Command-1**.

Then open **View → Show View Options** with **Command-J**. Choose an icon size that makes the app and Applications folder easy to recognize. Set sorting to **None** so you can position them yourself.

Put the app on the left and Applications on the right. Resize the Finder window so both fit without scrolling. Close the window after arranging it, then reopen the volume to check that Finder saved the layout.

Finder stores view settings in a `.DS_Store` file on the writable volume. This is one of the few places where that file belongs in a release artifact.

You can add a background illustration, but I'd start with the plain window. It remains readable at different display settings and gives you fewer files to maintain. The installation instructions in the README can say: “Drag Note Repo to Applications, then eject the disk image.”

Keep this step focused on the installation window. We'll launch the installed copy after building the final artifact.

Eject the volume through Finder, or detach its exact mounted path:

```bash
hdiutil detach '/Volumes/Note Repo'
```

If detaching reports that the volume is busy, close Finder windows or terminal sessions using that volume and try again. Avoid forcing it while Finder may still be writing the layout.

## Convert it to the download format

Convert the editable image to a compressed, read-only image:

```bash
hdiutil convert 'dist/Note Repo-layout.dmg' \
  -format UDZO \
  -o 'dist/Note Repo-download.dmg'
```

`UDZO` is the read-only, zlib-compressed format described in [Apple's packaging guide](https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution). Keep the editable image as a local intermediate file. The download asset is `Note Repo-download.dmg`.

Verify the image's integrity:

```bash
hdiutil verify 'dist/Note Repo-download.dmg'
```

This checks the image container. It doesn't prove that the app works or that Gatekeeper accepts it.

If you change the app, start again from staging. If you change only the window layout, edit the writable image and convert it again. Either change produces a new final file that needs the remaining release steps again.

## Sign the disk image

The DMG gets its own signature, separate from the app inside. Use the **Developer ID Application** certificate here too:

```bash
codesign --timestamp \
  --identifier com.flaviocopes.noterepo.disk-image \
  --sign 'Developer ID Application: Flavio Copes (DGFKNTAG99)' \
  'dist/Note Repo-download.dmg'

codesign --verify --verbose=2 'dist/Note Repo-download.dmg'
```

Choose a disk-image identifier belonging to your product, distinct from the app's bundle identifier.

Signing the container doesn't repair an unsigned helper inside the app. That's why we verified the bundle first.

## Notarize and staple the final DMG

Notarization sends the artifact to Apple's automated checking service. It is separate from Mac App Store review.

Store your notarization credentials in Keychain using the interactive command:

```bash
xcrun notarytool store-credentials notary
```

Follow the prompts for your account and authentication details. When using an Apple ID, supply an app-specific password as described in [Apple's custom workflow](https://developer.apple.com/documentation/security/customizing-the-notarization-workflow).

The submit command references the saved profile called `notary`, keeping the password out of the script.

Submit the signed final image:

```bash
xcrun notarytool submit 'dist/Note Repo-download.dmg' \
  --keychain-profile notary \
  --wait
```

Keep the submission ID. Wait for an **Accepted** result before continuing. An upload completing successfully isn't the same as the software passing notarization.

If the result is invalid, retrieve the submission log with `xcrun notarytool log`, using that ID and the same Keychain profile. The log identifies the file that failed. Fix that component, rebuild the artifact, and submit again.

After acceptance, attach the ticket:

```bash
xcrun stapler staple 'dist/Note Repo-download.dmg'
xcrun stapler validate 'dist/Note Repo-download.dmg'
```

Stapling puts the ticket on the distributed file so the user doesn't depend entirely on an online lookup.

For a DMG containing the app, submitting the outer container lets Apple inspect the nested code. You don't need to submit every nested item separately for that distribution. If you also distribute the app in a ZIP, prepare that artifact deliberately: notarize its app and staple the app before creating the downloadable ZIP. ZIP files themselves can't receive a stapled ticket.

## Test the file people will download

Mount the final DMG and check its window again. Copy the app into Applications, eject the image, then launch the installed copy.

Use a separate Mac, clean account, or test VM for this. An existing development installation can hide missing resources, saved permissions, and first-launch problems.

For the app copied from the final image, check its signature and Gatekeeper assessment:

```bash
codesign --verify --deep --strict --verbose=2 '/Applications/Note Repo.app'
spctl --assess --type execute --verbose=4 '/Applications/Note Repo.app'
```

The app should launch from Applications after the image is ejected. Test the main workflow and quit it. An app that still reads a resource from its mounted installer needs fixing.

Also test a browser download of the release. That exercises the download metadata and first-launch path a local build doesn't cover. If macOS blocks it, investigate the signature, notarization log, and exact downloaded artifact. Don't make disabling Gatekeeper part of the installation instructions.

## Attach it to a GitHub release

Calculate the checksum only after signing and stapling, because those steps change the file:

```bash
shasum -a 256 'dist/Note Repo-download.dmg'
```

For an existing release, [GitHub CLI can upload an asset](https://cli.github.com/manual/gh_release_upload) like this:

```bash
gh release upload v1.0.0 'dist/Note Repo-download.dmg' \
  --repo flaviocopes/note-repo
```

Here `v1.0.0` is an example tag. Use the release that matches the app inside your image. If you are creating a release, `gh release create` can accept the file as an asset too; `--verify-tag` ensures the intended tag already exists remotely.

Keep the release notes explicit about the supported macOS versions and architectures. Include the checksum and short installation instructions. If there's also a ZIP for automatic updates, label both downloads so people know which one to choose.

Download the uploaded DMG into a fresh folder and compare its checksum with your local final file. Then repeat the installation test with that download. That's the file your users will receive.
