Mac App Direct Distribution, DMG Signing & Notarization Guide
This guide walks through a generic direct-distribution workflow for a macOS app.
You can following along with me as I preform all of these steps in this YouTube video: Distribute Your macOS App Like a Pro DMG, Code Signing & Notarization
- Archive the app in Xcode.
- Export the notarized
.app. - Verify the exported app's signature and Gatekeeper status.
- Build a polished installer DMG with a background image and an Applications shortcut.
- Convert the DMG to a compressed read-only image.
- Sign, notarize, staple, and verify the final DMG.
The commands intentionally use placeholders so this document can be shared safely.
Placeholders Used in This Guide
Some values are known before you start. Others are discovered later from command output or created during the notarization setup. This guide introduces each value when you need it.
Values you choose at the beginning:
<<APPNAME>> # App bundle name without .app, for example: MyApp<<VOLUMENAME>> # Mounted DMG volume name, for example: MyApp Installer<<DMGNAME>> # Final DMG filename without .dmg, for example: MyApp
Values you will collect or create later:
<<APPLE_ID>> # Apple ID email for the Apple Developer account<<TEAM_ID>> # Apple Developer Team ID<<NOTARY_PROFILE>> # Local Keychain profile name for notarytool<<APP_SPECIFIC_PASSWORD>> # App-specific password generated at account.apple.com<<DEVELOPER_ID_CERT>> # Developer ID Application certificate name or SHA-1 hash
1. Archive the App in Xcode
Archiving creates the distributable build using your release configuration, signing settings, hardened runtime, entitlements, and provisioning choices. For direct distribution outside the Mac App Store, this is the starting point for a properly signed Developer ID app.
In Xcode:
- Select a generic macOS destination, not a simulator.
- Choose Product > Archive.
- When Organizer opens, select the archive.
- Click Distribute App.
- Choose Direct Distribution.
- Continue through the signing and upload steps.
- Wait for Apple to process/notarize the archive.
- Export the notarized app when Xcode allows it.
Expected result:
Xcode Organizer shows the archive as validated/notarized, and you can export a signed .app bundle.
2. Create a Release Builds Folder
Keeping the exported app, temporary DMG, final DMG, and background image in one working folder makes the rest of the process easier to follow. It also avoids accidentally packaging the wrong copy of the app.
Create a folder somewhere convenient, such as your Desktop:
~/Desktop/ReleaseBuilds
In Finder:
- Open your Desktop.
- Create a new folder named
ReleaseBuilds. - Copy the notarized app you exported from Xcode into that folder.
If you already have a background image for the DMG, this is also a good place to put it so everything for this release lives together.
3. Set the Starting Variables
Do this after copying the exported app into ReleaseBuilds and before running the first verification command.
- Open Terminal.
- Use
cdto move into theReleaseBuildsfolder. - Copy the variable block below.
- Replace the placeholders with your app name, volume name, DMG name, and exported app path.
- Paste the edited block into Terminal and press Return.
After that, the early commands can use short names like $APP_PATH, $TEMP_DMG, and $FINAL_DMG instead of making you type the full app name and file paths over and over.
Template:
APP_NAME="<<APPNAME>>"VOLUME_NAME="<<VOLUMENAME>>"DMG_NAME="<<DMGNAME>>"APP_PATH="/path/to/${APP_NAME}.app"TEMP_DMG="${DMG_NAME}-temp.dmg"FINAL_DMG="${DMG_NAME}.dmg"
Example using fake values:
cd ~/Desktop/ReleaseBuildsAPP_NAME="MyGreatApp"VOLUME_NAME="MyGreatApp Installer"DMG_NAME="MyGreatApp"APP_PATH="$HOME/Desktop/ReleaseBuilds/MyGreatApp.app"TEMP_DMG="${DMG_NAME}-temp.dmg"FINAL_DMG="${DMG_NAME}.dmg"
With those example values, the guide will create:
MyGreatApp-temp.dmgMyGreatApp.dmg
And when a later command says:
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
Terminal treats it as:
codesign --verify --deep --strict --verbose=2 "$HOME/Desktop/ReleaseBuilds/MyGreatApp.app"
The important thing: Keep using the same Terminal window as you work through the guide. The variables live in that Terminal session.
4. Verify the Exported App Signature
Notarization is not a substitute for local verification. Before packaging the app into a DMG, confirm that the .app bundle is signed correctly and that nested code, frameworks, helpers, and resources pass strict validation.
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
Expected result:
/path/to/<<APPNAME>>.app: valid on disk/path/to/<<APPNAME>>.app: satisfies its Designated Requirement
If this fails, fix the app signing issue before building the DMG.
5. Inspect the App Signing Details
This confirms which Developer ID certificate signed the app. You may also need the team identifier later when storing notarization credentials.
codesign -dv --verbose=4 "$APP_PATH"
Look for these fields:
Authority=Developer ID Application: <<DEVELOPER_NAME>> (<<TEAM_ID>>)TeamIdentifier=<<TEAM_ID>>Runtime Version=...
Copy the value after TeamIdentifier=. You will need it later when you store notarization credentials.
Now save it as a Terminal variable:
TEAM_ID="<<TEAM_ID>>"
6. Check Gatekeeper Assessment for the App
Gatekeeper is what users encounter when opening software downloaded from the internet. This check confirms macOS sees the exported app as acceptable executable code.
spctl -a -t exec -vv "$APP_PATH"
Expected result:
/path/to/<<APPNAME>>.app: acceptedsource=Notarized Developer IDorigin=Developer ID Application: <<DEVELOPER_NAME>> (<<TEAM_ID>>)
The exact wording can vary by macOS version, but you want accepted and a Developer ID/notarized source.
7. Create a Writable Temporary DMG
The temporary DMG is the staging area. You need it writable so you can copy in the app, add the Applications shortcut, add the background image, and set Finder layout preferences.
Choose a size comfortably larger than your app bundle.
diskutil image create blank \ --size 100m \ --volumeName "$VOLUME_NAME" \ --format UDRW \ "$TEMP_DMG"
Expected result:
created: /path/to/<<DMGNAME>>-temp.dmg
If your app is larger, increase 100m to something appropriate, such as 250m or 500m.
8. Mount the Temporary DMG
Mounting exposes the writable disk image as a normal Finder volume under /Volumes, which lets you arrange the installer exactly how users will see it.
In Finder, double-click the temporary DMG:
<<DMGNAME>>-temp.dmg
Expected result: a new mounted volume appears on your Desktop and in Finder's sidebar.
/Volumes/<<VOLUMENAME>>
9. Copy the App into the Mounted Volume
The DMG should contain the app bundle users will drag into Applications.
In Finder:
- Open the mounted
<<VOLUMENAME>>volume. - Find your exported
<<APPNAME>>.app. - Drag
<<APPNAME>>.appinto the mounted volume.
10. Add the Applications Shortcut
The Applications shortcut gives users the familiar drag-to-install experience: app on the left, Applications on the right.
ln -s /Applications "/Volumes/${VOLUME_NAME}/Applications"
You should now see both the application and the shortcut to the applications folder in the mounted volume.
11. Add the Hidden Background Folder
Finder can use an image stored inside the DMG as the window background. Keeping it in .background keeps the installer window clean for users and under normal circumstances, background folder will be hidden
mkdir -p "/Volumes/${VOLUME_NAME}/.background"
Then, in Finder:
- Open the mounted volume.
- Press Command-Shift-. to show hidden files.
- Open the
.backgroundfolder. - Drag your background image into that folder.
Expected result: the background image is inside /Volumes/<<VOLUMENAME>>/.background/.
12. Configure the Finder Window
Finder stores window layout metadata on the mounted volume. This gives users a clean installer window instead of a plain file listing.
Open the mounted DMG volume in Finder, then set:
- View > As Icons
- View > Show View Options or press Command-J
- Enable Always open in icon view
- Set icon size, commonly
96or128 - Set text size, commonly
12 - Set Background > Picture
- Open the hidden
.backgroundfolder in Finder. - Drag the background image from
.backgroundonto the picture drop target in the View Options window. - Position
<<APPNAME>>.appon the left - Position
Applicationson the right - Resize the Finder window to the desired final size
- Hide hidden files again with Command-Shift-.
- Close the Finder window
13. Eject the Temporary DMG
Ejecting the mounted volume finalizes the changes you made to the writable temporary DMG.
In Finder:
- Close the mounted DMG window.
- Eject the
<<VOLUMENAME>>volume from the Finder sidebar or Desktop.
If Finder says the volume is still in use, close any Finder windows showing the mounted DMG and try again.
14. Convert to a Compressed Read-Only DMG
Users should receive a final DMG that is smaller, read-only, and not accidentally modifiable.
diskutil image create from \ "$TEMP_DMG" \ "$FINAL_DMG" \ --format UDZO
You can remove the temporary writable image after confirming the final DMG exists and when mounted and opened, you can see the background image as well as the application and shortcut to the applications folder.
15. Create an App-Specific Password
The notarytool should not use your normal Apple ID password. Apple requires an app-specific password for command-line notarization when using Apple ID authentication.
- Sign in with the Apple ID for the developer account.
- Open Sign-In and Security.
- Choose App-Specific Passwords.
- Generate a new password.
- Give it a recognizable name, such as
NotaryTool. - Copy it immediately.
Expected result:
Apple shows a one-time app-specific password.
Do not paste the real password into documentation, screenshots, source control, issue trackers, or chat. You cannot retrieve it later; if lost, revoke it and create another one.
16. Store Notarization Credentials in Keychain
Storing credentials once in Keychain avoids putting secrets directly into every notarization command.
Before running the command, decide on two values:
APPLE_ID: the Apple ID email address for your Apple Developer account.NOTARY_PROFILE: a local nickname for these saved credentials. You choose this name.MyNotaryProfileis a common example.
Save those values in Terminal:
APPLE_ID="<<APPLE_ID>>"NOTARY_PROFILE="<<NOTARY_PROFILE>>"
Example using fake values:
APPLE_ID="developer@example.com"NOTARY_PROFILE="MyNotaryProfile"
Do not define the app-specific password as a Terminal variable. It is a secret, and it is only needed once so notarytool can save it securely in Keychain.
Run this command and replace <<APP_SPECIFIC_PASSWORD>> directly in the command with the app-specific password you just generated.
xcrun notarytool store-credentials "$NOTARY_PROFILE" \ --apple-id "$APPLE_ID" \ --team-id "$TEAM_ID" \ --password "<<APP_SPECIFIC_PASSWORD>>"
Expected result:
Credentials saved to Keychain.
17. Find and Save the Developer ID Certificate
Signing the DMG requires the correct Developer ID Application identity. You can sign with either the certificate name or its SHA-1 hash.
security find-identity -v -p codesigning
Look for:
1) <<SHA_1_HASH>> "Developer ID Application: <<DEVELOPER_NAME>> (<<TEAM_ID>>)"
The SHA-1 hash is not labeled in the output. It is the long 40-character string before the certificate name.
Example using fake values:
1) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA "Developer ID Application: Example Developer (ABCDE12345)"2) BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB "Developer ID Application: Example Developer (ABCDE12345)" 2 valid identities found
In that example, either AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA or BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB is a SHA-1 hash.
Use the Developer ID Application identity, not a Mac Developer, Apple Development, or Installer certificate.
Copy either the SHA-1 hash or the full certificate name and save it in Terminal. If you see more than one Developer ID Application identity with the same certificate name, use one of the SHA-1 hashes instead of the name so codesign knows exactly which identity you mean.
DEVELOPER_ID_CERT="<<DEVELOPER_ID_CERT>>"
Example using a fake SHA-1 hash:
DEVELOPER_ID_CERT="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
Example using a fake certificate name:
DEVELOPER_ID_CERT="Developer ID Application: Example Developer (ABCDE12345)"
18. Sign the Final DMG
The app inside the DMG is already signed, but the DMG itself is a separate downloadable artifact. Signing it lets macOS verify that the disk image was created by your Developer ID certificate and has not been modified.
codesign \ --force \ --sign "$DEVELOPER_ID_CERT" \ "$FINAL_DMG"
Expected result:
# No output usually means success.
19. Verify the DMG Signature
This catches signing mistakes before notarization.
codesign --verify --verbose=2 "$FINAL_DMG"
Expected result:
<<DMGNAME>>.dmg: valid on disk<<DMGNAME>>.dmg: satisfies its Designated Requirement
20. Inspect the DMG Certificate Details
This confirms the DMG was signed by the intended Developer ID identity.
codesign -dv --verbose=4 "$FINAL_DMG"
Look for:
Authority=Developer ID Application: <<DEVELOPER_NAME>> (<<TEAM_ID>>)TeamIdentifier=<<TEAM_ID>>
21. Submit the DMG for Notarization
Notarization lets Apple scan and approve the distributable artifact. The --wait option keeps the command running until Apple returns a final status.
xcrun notarytool submit "$FINAL_DMG" \ --keychain-profile "$NOTARY_PROFILE" \ --wait
Expected successful result:
id: <<SUBMISSION_ID>>status: Accepted
If the status is not Accepted, fetch the log using the submission ID:
xcrun notarytool log "<<SUBMISSION_ID>>" \ --keychain-profile "$NOTARY_PROFILE"
Use the log to identify what Apple rejected. Common issues include unsigned nested code, missing hardened runtime, invalid entitlements, or packaging the wrong build.
22. Staple the Notarization Ticket
Stapling attaches the notarization ticket to the DMG. This lets users install the app even when macOS cannot immediately contact Apple's notarization servers.
xcrun stapler staple "$FINAL_DMG"
Expected result:
The staple and validate action worked!
23. Validate the Stapled Ticket
This confirms the ticket is attached to the DMG and readable.
xcrun stapler validate "$FINAL_DMG"
Expected result:
The validate action worked!
24. Final Gatekeeper Verification
This is the closest command-line check to what users will experience when opening the downloaded DMG.
spctl \ -a \ -t open \ --context context:primary-signature \ -vv \ "$FINAL_DMG"
Expected result:
<<DMGNAME>>.dmg: acceptedsource=Notarized Developer IDorigin=Developer ID Application: <<DEVELOPER_NAME>> (<<TEAM_ID>>)
Now all you have to do is publish the DMG for download. This can one on your website or through some shared folder. You can also publish it via GitHub Releases and the video mentioned at the top of this blog goes through that process.
