Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
eb51358
feat(components): add verified removable component store
Augani Jul 16, 2026
b771925
feat(app): add focused component management UI
Augani Jul 16, 2026
50c0ddc
fix(components): restore CLI flag parsing
Augani Jul 16, 2026
517095d
feat(release): split Docker Core from optional payloads
Augani Jul 16, 2026
913b02c
feat(site): preview focused component downloads
Augani Jul 16, 2026
22714d1
chore(release): prepare 0.3.2 build 43
Augani Jul 16, 2026
9072aec
fix(shares): scope safe denylist to home root
Augani Jul 16, 2026
68038d7
fix(network): unblock guided authorization
Augani Jul 16, 2026
9aae9b8
fix(app): stop engine on quit by default
Augani Jul 16, 2026
d86260e
feat(network): support custom local domains
Augani Jul 16, 2026
d8c0284
docs(release): summarize 0.3.2 changes
Augani Jul 16, 2026
bfdec66
fix(ui): clear stale container details
Augani Jul 16, 2026
afad97b
fix(network): make local HTTPS authorization interactive
Augani Jul 16, 2026
6d58480
build(release): remove duplicate core vm payloads
Augani Jul 16, 2026
713f3b5
fix(app): reopen the main window
Augani Jul 17, 2026
3d89a09
docs(release): publish focused component metadata
Augani Jul 17, 2026
8fcde85
docs(release): finalize focused component launch
Augani Jul 17, 2026
55309d3
fix(app): reliably restore closed window
Augani Jul 17, 2026
0b83506
docs(release): refresh 0.3.2 signed metadata
Augani Jul 17, 2026
5d48704
fix(network): make IPv6 DNS host-route aware
Augani Jul 17, 2026
eba42e6
feat(components): carry website selections into Dory
Augani Jul 17, 2026
06d8472
docs(release): present one focused Dory app
Augani Jul 17, 2026
f40e55a
ci: install Go for network helper tests
Augani Jul 17, 2026
d85af52
docs(release): bind focused 0.3.2 metadata
Augani Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ jobs:
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
- name: Install Protocol Buffers compiler
run: brew install protobuf
- name: Install Go toolchain
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version: '1.26.5'
- name: Network helper
run: scripts/test.sh gvproxy
- name: Swift packages
run: scripts/test.sh swift
- name: App tests
Expand Down
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
# Changelog

## 0.3.2 - 2026-07-17

### Added

- Replaced fixed app editions with a smaller Docker Core app and signed, removable Kubernetes,
Linux Machines, Linux Desktop runtime, Debian, Ubuntu, and Kali components stored on the selected
Dory data drive.
- Added a pre-download component selector that safely carries the chosen optional payloads into
Dory for signed-size review and explicit installation confirmation.
- Retired the separate public lite app so direct downloads present one Docker Core app and optional
signed components instead of overlapping app editions.
- Added exact and leftmost-wildcard custom domain mappings in Settings > Network and the CLI. Dory
now routes nginx-style `/etc/hosts` domains through its built-in HTTP and trusted HTTPS proxies.

### Changed

- Reused Docker Core's signed engine kernel and rootfs for the macOS 14 fallback instead of storing
duplicate compatibility copies, removing about 112 MB from the installed Core app.
- Quitting Dory now stops its background engine by default. People who want an always-running
engine can explicitly enable **Keep engine running after quit**.

### Fixed

- Fixed built-in safe home sharing so names such as `library` are hidden only at the shared home
root, while nested project directories such as Composer package paths remain visible.
- Fixed the Network authorization button so the guided admin operation succeeds before optional
background-service registration and Login Items approval.
- Fixed local HTTPS authorization failing after the admin prompt. Dory now adds its CA to the
current user's login keychain through an interactive macOS trust prompt, while the privileged
helper is limited to resolver and PF changes.
- Fixed custom local domains returning 502 after ports 80 and 443 were authorized.
- Fixed local HTTPS identity refreshes accumulating certificates and private keys in the user's
login keychain.
- Fixed stale container details remaining open after the selected container disappeared or no
longer matched the current view, and added toolbar controls for hiding navigation and details.
- Fixed reopening Dory from the Dock or menu bar when the app was running without a visible window.
- Fixed `linux/amd64` builds on IPv4-only host networks by withholding unreachable IPv6 DNS
answers while preserving native IPv6 when the Mac has a routable IPv6 path.

## 0.3.1 - 2026-07-16

### Added
Expand Down
12 changes: 6 additions & 6 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dory compatibility

This document describes the Dory 0.3.1 public product surface. Dory is under active development, so
This document describes the Dory 0.3.2 public product surface. Dory is under active development, so
please report workflows that behave differently from a standard Docker engine.

## Platform
Expand All @@ -12,10 +12,10 @@ please report workflows that behave differently from a standard Docker engine.
| Intel Mac | Not included in current releases; planned after dedicated hardware validation |
| Windows or Linux host | Not supported by the macOS app |

The standard Apple Silicon build omits the large graphical guest images. The all-inclusive Desktop
build adds the managed Debian, Ubuntu, and Kali images. Both builds include containers, Kubernetes,
and headless Linux servers; an existing graphical machine remains manageable if the lean build is
installed later.
Dory ships one Apple Silicon Docker Core app. Kubernetes, Linux Machines, the shared Linux Desktop
runtime, and the managed Debian, Ubuntu, and Kali images are signed optional components. Their
payloads live on the selected Dory data drive and can be removed independently without deleting
workload data.

## Docker workflow

Expand Down Expand Up @@ -54,7 +54,7 @@ operations, but it is not a substitute for a normal backup strategy.
| Capability | Status |
|---|---|
| k3s provisioning | Supported in the shared engine |
| `kubectl` | Bundled |
| `kubectl` | Installed by the optional Kubernetes component |
| Resource browser | Pods, deployments, services, config maps, secrets, and ingresses |
| Workload actions | Logs, exec, scale, restart, rollout, and apply |
| Multiple Kubernetes versions | Selectable from supported Dory presets |
Expand Down
15 changes: 15 additions & 0 deletions Config/Dory-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,25 @@
<string>https://augani.github.io/dory/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>AFetajNbqZty68rRY7OMWYNt6suUsrokQmYMhDJtnP4=</string>
<key>DoryComponentCatalogURL</key>
<string>https://augani.github.io/dory/components/arm64/catalog.json</string>
<key>SUEnableInstallerLauncherService</key>
<true/>
<key>LSMultipleInstancesProhibited</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>dev.dory.component-selection</string>
<key>CFBundleURLSchemes</key>
<array>
<string>dory</string>
</array>
</dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
Expand Down
24 changes: 12 additions & 12 deletions Dory.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@
CODE_SIGN_ENTITLEMENTS = Dory/Dory.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 42;
CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 864H636QW4;
ENABLE_APP_SANDBOX = NO;
ENABLE_DEBUG_DYLIB = NO;
Expand All @@ -431,7 +431,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 0.3.1;
MARKETING_VERSION = 0.3.2;
PRODUCT_BUNDLE_IDENTIFIER = com.pythonxi.Dory;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand All @@ -452,7 +452,7 @@
CODE_SIGN_ENTITLEMENTS = Dory/Dory.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 42;
CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 864H636QW4;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -468,7 +468,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 0.3.1;
MARKETING_VERSION = 0.3.2;
PRODUCT_BUNDLE_IDENTIFIER = com.pythonxi.Dory;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand All @@ -486,11 +486,11 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 42;
CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 864H636QW4;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.3.1;
MARKETING_VERSION = 0.3.2;
PRODUCT_BUNDLE_IDENTIFIER = com.pythonxi.DoryTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -507,11 +507,11 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 42;
CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 864H636QW4;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.3.1;
MARKETING_VERSION = 0.3.2;
PRODUCT_BUNDLE_IDENTIFIER = com.pythonxi.DoryTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -527,10 +527,10 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 42;
CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 864H636QW4;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 0.3.1;
MARKETING_VERSION = 0.3.2;
PRODUCT_BUNDLE_IDENTIFIER = com.pythonxi.DoryUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -546,10 +546,10 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 42;
CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 864H636QW4;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 0.3.1;
MARKETING_VERSION = 0.3.2;
PRODUCT_BUNDLE_IDENTIFIER = com.pythonxi.DoryUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand Down
48 changes: 46 additions & 2 deletions Dory/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import SwiftUI

final class DoryAppDelegate: NSObject, NSApplicationDelegate {
fileprivate static let mainWindowIdentifier = NSUserInterfaceItemIdentifier("dory.main-window")
private static let openMainWindowNotification = Notification.Name("dev.dory.open-main-window")
private static let instanceLock = NSLock()
private static var instanceLockFD: Int32 = -1
private static let statusItemController = DoryStatusItemController()
Expand Down Expand Up @@ -31,6 +32,11 @@ final class DoryAppDelegate: NSObject, NSApplicationDelegate {
guard !isNetworkHelperMaintenance() else { return }
guard !isTestHost, let bundleIdentifier = Bundle.main.bundleIdentifier else { return }
guard acquireInstanceLock() else {
DistributedNotificationCenter.default().postNotificationName(
openMainWindowNotification,
object: nil,
deliverImmediately: true
)
runningInstance(bundleIdentifier: bundleIdentifier)?.activate(options: [.activateAllWindows])
exit(EXIT_SUCCESS)
}
Expand Down Expand Up @@ -126,6 +132,12 @@ final class DoryAppDelegate: NSObject, NSApplicationDelegate {
}
return
}
DistributedNotificationCenter.default().addObserver(
self,
selector: #selector(openMainWindowFromNotification(_:)),
name: Self.openMainWindowNotification,
object: nil
)
NSApp.setActivationPolicy(.accessory)
Self.refreshMenuBarVisibility()
Task { @MainActor in
Expand All @@ -138,11 +150,31 @@ final class DoryAppDelegate: NSObject, NSApplicationDelegate {
false
}

@MainActor func applicationShouldHandleReopen(
_ sender: NSApplication,
hasVisibleWindows flag: Bool
) -> Bool {
Self.openMainWindow()
return false
}

func applicationDidBecomeActive(_ notification: Notification) {
Task { @MainActor in Self.closeDuplicateMainWindows() }
}

func application(_ application: NSApplication, open urls: [URL]) {
Task { @MainActor in
guard urls.contains(where: Self.statusItemController.handleComponentSelectionURL) else { return }
Self.openMainWindow()
}
}

func applicationWillTerminate(_ notification: Notification) {
DistributedNotificationCenter.default().removeObserver(
self,
name: Self.openMainWindowNotification,
object: nil
)
if !Self.isTestHost,
!AppStore.resolvedKeepDorydRunningAfterQuit(defaults: .standard) {
DorydLaunchAgent.stopAndRemoveCurrentSynchronously()
Expand All @@ -157,6 +189,10 @@ final class DoryAppDelegate: NSObject, NSApplicationDelegate {
}
}

@objc private func openMainWindowFromNotification(_ notification: Notification) {
Task { @MainActor in Self.openMainWindow() }
}

@MainActor static func markMainWindow(_ window: NSWindow) {
window.identifier = mainWindowIdentifier
window.title = "Dory"
Expand Down Expand Up @@ -220,8 +256,10 @@ private final class DoryStatusItemController: NSObject, NSPopoverDelegate, NSWin
func openMainWindow() {
guard let store else { return }
if let existing = NSApp.windows.first(where: { $0.identifier == DoryAppDelegate.mainWindowIdentifier }) {
existing.makeKeyAndOrderFront(nil)
DoryActivation.setForeground(true)
existing.deminiaturize(nil)
existing.makeKeyAndOrderFront(nil)
existing.orderFrontRegardless()
return
}

Expand Down Expand Up @@ -252,8 +290,14 @@ private final class DoryStatusItemController: NSObject, NSPopoverDelegate, NSWin
}

DoryAppDelegate.markMainWindow(window)
window.makeKeyAndOrderFront(nil)
DoryActivation.setForeground(true)
window.deminiaturize(nil)
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
}

func handleComponentSelectionURL(_ url: URL) -> Bool {
store?.handleComponentSelectionURL(url) == true
}

private func installStatusItemIfNeeded() {
Expand Down
43 changes: 38 additions & 5 deletions Dory/App/AppInfo.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,47 @@
import DoryOperations
import Foundation

/// The app's own version and build, read from the bundle (driven by the project's
/// `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`) so the displayed version is always correct.
enum AppInfo {
nonisolated enum AppInfo {
static let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0"
static let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "0"
/// Build scripts set this explicitly. Defaulting to true keeps direct Xcode and test builds useful.
static let includesDesktopLinux = desktopLinuxIncluded(
from: Bundle.main.object(forInfoDictionaryKey: "DoryIncludesDesktopLinux")
)
static var componentCatalogURL: URL {
if let override = ProcessInfo.processInfo.environment["DORY_COMPONENT_CATALOG_URL"],
let url = URL(string: override),
url.scheme == "https" || url.isFileURL {
return url
}
if let value = Bundle.main.object(forInfoDictionaryKey: "DoryComponentCatalogURL") as? String,
let url = URL(string: value),
url.scheme == "https" {
return url
}
return DoryComponentDefaults.catalogURL
}
/// Legacy 0.3.x builds used one Desktop boolean. New builds declare every payload in
/// DoryBundledComponents; optional installed components are resolved from the selected data drive.
static var includesDesktopLinux: Bool {
componentAvailable(.linuxDesktop)
&& [.desktopDebian, .desktopUbuntu, .desktopKali].contains(where: componentAvailable)
}

static func componentAvailable(_ id: DoryComponentID) -> Bool {
if bundledComponents.contains(id) { return true }
guard id.isRemovable, let store = try? DoryComponentStore.selected() else { return false }
return store.isInstalledAndValid(id)
}

static var bundledComponents: Set<DoryComponentID> {
if let raw = Bundle.main.object(forInfoDictionaryKey: "DoryBundledComponents") as? [String] {
return Set(raw.compactMap(DoryComponentID.init(rawValue:))).union([.dockerCore])
}
var legacy: Set<DoryComponentID> = [.dockerCore, .kubernetes, .linuxMachines]
if desktopLinuxIncluded(from: Bundle.main.object(forInfoDictionaryKey: "DoryIncludesDesktopLinux")) {
legacy.formUnion([.linuxDesktop, .desktopDebian, .desktopUbuntu, .desktopKali])
}
return legacy
}

static func desktopLinuxIncluded(from bundleValue: Any?) -> Bool {
if let value = bundleValue as? Bool {
Expand Down
Loading
Loading