Skip to content

Commit 413e3d6

Browse files
author
Augustus Otu
committed
feat: initial open-source release
Dory — a native macOS app for Docker and Linux containers on Apple silicon. A free, open-source alternative to OrbStack and Docker Desktop, licensed under GPL-3.0.
0 parents  commit 413e3d6

113 files changed

Lines changed: 14109 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/benchmark.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Benchmark
2+
3+
# Memory benchmark: Dory's shared VM vs one VM per container.
4+
#
5+
# IMPORTANT: this needs hardware virtualization and macOS 26 (Tahoe). GitHub-HOSTED macOS runners
6+
# are themselves VMs without nested virtualization, so they CANNOT run the container engine — this
7+
# job only produces real numbers on a SELF-HOSTED macOS 26 runner with Dory installed. Label such a
8+
# runner `[self-hosted, macOS, dory]` and it will pick this up.
9+
10+
on:
11+
workflow_dispatch:
12+
inputs:
13+
count:
14+
description: 'Number of containers'
15+
default: '2'
16+
image:
17+
description: 'Image to run'
18+
default: 'alpine:latest'
19+
schedule:
20+
- cron: '0 9 * * 1' # weekly, Monday 09:00 UTC
21+
22+
jobs:
23+
benchmark:
24+
runs-on: [self-hosted, macOS, dory]
25+
steps:
26+
- uses: actions/checkout@v4
27+
- name: Run memory benchmark
28+
run: scripts/benchmark.sh "${{ github.event.inputs.count || '2' }}" "${{ github.event.inputs.image || 'alpine:latest' }}"
29+
- name: Summarize
30+
run: |
31+
{
32+
echo "### Dory memory benchmark"
33+
echo ""
34+
echo '```json'
35+
cat benchmark-results.json
36+
echo '```'
37+
} >> "$GITHUB_STEP_SUMMARY"
38+
- uses: actions/upload-artifact@v4
39+
with:
40+
name: benchmark-results
41+
path: benchmark-results.json

.github/workflows/pages.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Deploy Pages
2+
3+
# Publishes the landing page in docs/ to GitHub Pages on every push to main.
4+
# One-time setup: repo Settings → Pages → Source → "GitHub Actions".
5+
6+
on:
7+
push:
8+
branches: [main]
9+
paths: ['docs/**', '.github/workflows/pages.yml']
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
pages: write
15+
id-token: write
16+
17+
concurrency:
18+
group: pages
19+
cancel-in-progress: true
20+
21+
jobs:
22+
deploy:
23+
runs-on: ubuntu-latest
24+
environment:
25+
name: github-pages
26+
url: ${{ steps.deployment.outputs.page_url }}
27+
steps:
28+
- uses: actions/checkout@v4
29+
- uses: actions/configure-pages@v5
30+
- uses: actions/upload-pages-artifact@v3
31+
with:
32+
path: docs
33+
- id: deployment
34+
uses: actions/deploy-pages@v4

.github/workflows/release.yml

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
name: Release
2+
3+
# Builds, signs, notarizes, and publishes a Dory release when a version tag (e.g. v0.1.0) is
4+
# pushed, or on manual dispatch.
5+
#
6+
# Required repository secrets:
7+
# DEVELOPER_ID_CERT_P12_BASE64 base64 of your "Developer ID Application" .p12
8+
# DEVELOPER_ID_CERT_PASSWORD the .p12 export password
9+
# KEYCHAIN_PASSWORD any string — used for the throwaway CI keychain
10+
# NOTARY_APPLE_ID Apple ID email for notarytool
11+
# NOTARY_TEAM_ID Apple Developer Team ID
12+
# NOTARY_APPLE_PASSWORD app-specific password for that Apple ID
13+
#
14+
# NOTE: Dory targets macOS 26 (Tahoe) and needs Xcode 27. GitHub-hosted runners do not offer that
15+
# toolchain yet, so point `runs-on` at a self-hosted macOS 26 runner (label it `[self-hosted, macOS]`
16+
# and update below). `macos-15` keeps the workflow valid out of the box but will fail to compile
17+
# until hosted runners catch up.
18+
19+
on:
20+
push:
21+
tags: ['v*']
22+
workflow_dispatch:
23+
inputs:
24+
version:
25+
description: 'Version to release (e.g. 0.1.0)'
26+
required: true
27+
28+
permissions:
29+
contents: write
30+
31+
jobs:
32+
release:
33+
runs-on: macos-15
34+
outputs:
35+
version: ${{ steps.ver.outputs.version }}
36+
sha256: ${{ steps.build.outputs.sha256 }}
37+
steps:
38+
- uses: actions/checkout@v4
39+
40+
- name: Resolve version
41+
id: ver
42+
run: |
43+
if [ -n "${{ github.event.inputs.version }}" ]; then
44+
V="${{ github.event.inputs.version }}"
45+
else
46+
V="${GITHUB_REF_NAME#v}"
47+
fi
48+
echo "version=$V" >> "$GITHUB_OUTPUT"
49+
50+
- name: Select newest installed Xcode
51+
run: sudo xcode-select -s "$(ls -d /Applications/Xcode*.app | sort -V | tail -1)/Contents/Developer"
52+
53+
- name: Import Developer ID certificate
54+
env:
55+
CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_P12_BASE64 }}
56+
CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }}
57+
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
58+
run: |
59+
KEYCHAIN="$RUNNER_TEMP/dory-signing.keychain-db"
60+
echo "$CERT_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12"
61+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
62+
security set-keychain-settings -lut 21600 "$KEYCHAIN"
63+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
64+
security import "$RUNNER_TEMP/cert.p12" -P "$CERT_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN"
65+
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
66+
security list-keychains -d user -s "$KEYCHAIN" login.keychain
67+
rm -f "$RUNNER_TEMP/cert.p12"
68+
69+
- name: Build, sign, and notarize
70+
id: build
71+
env:
72+
NOTARY_APPLE_ID: ${{ secrets.NOTARY_APPLE_ID }}
73+
# Team ID is not secret (it appears in every signed app); fall back to the project's team.
74+
NOTARY_TEAM_ID: ${{ secrets.NOTARY_TEAM_ID || '864H636QW4' }}
75+
NOTARY_PASSWORD: ${{ secrets.NOTARY_APPLE_PASSWORD }}
76+
run: scripts/release.sh "${{ steps.ver.outputs.version }}"
77+
78+
- name: Publish GitHub Release
79+
uses: softprops/action-gh-release@v2
80+
with:
81+
tag_name: v${{ steps.ver.outputs.version }}
82+
name: Dory ${{ steps.ver.outputs.version }}
83+
files: ${{ steps.build.outputs.zip }}
84+
generate_release_notes: true
85+
body: |
86+
Native, lightweight Docker & Linux containers on Apple silicon — a free, open-source
87+
alternative to OrbStack and Docker Desktop. Requires macOS 26 (Tahoe) or later.
88+
89+
**Install**
90+
91+
```sh
92+
brew install --cask Augani/dory/dory
93+
```
94+
95+
…or download `Dory-${{ steps.ver.outputs.version }}.zip` below.
96+
97+
```
98+
sha256: ${{ steps.build.outputs.sha256 }}
99+
```
100+
101+
# Keeps the Homebrew cask in this repo (the tap) current after every release.
102+
bump-cask:
103+
needs: release
104+
runs-on: ubuntu-latest
105+
permissions:
106+
contents: write
107+
steps:
108+
- uses: actions/checkout@v4
109+
with:
110+
ref: main
111+
- name: Update cask version + checksum
112+
run: |
113+
V="${{ needs.release.outputs.version }}"
114+
S="${{ needs.release.outputs.sha256 }}"
115+
sed -i -E "s/ version \"[^\"]+\"/ version \"$V\"/" Casks/dory.rb
116+
sed -i -E "s/ sha256 \"[0-9a-f]+\"/ sha256 \"$S\"/" Casks/dory.rb
117+
if git diff --quiet Casks/dory.rb; then echo "cask already current"; exit 0; fi
118+
git config user.name "github-actions[bot]"
119+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
120+
git commit -am "chore: update Homebrew cask to v$V"
121+
git push

.gitignore

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# macOS
2+
.DS_Store
3+
4+
# Xcode
5+
build/
6+
DerivedData/
7+
*.xcuserstate
8+
xcuserdata/
9+
*.xccheckout
10+
*.moved-aside
11+
*.hmap
12+
*.ipa
13+
*.dSYM.zip
14+
*.dSYM
15+
16+
# Xcode project user-specific
17+
*.xcodeproj/*
18+
!*.xcodeproj/project.pbxproj
19+
!*.xcodeproj/xcshareddata/
20+
!*.xcodeproj/project.xcworkspace/
21+
*.xcworkspace/xcuserdata/
22+
23+
# Swift Package Manager
24+
.build/
25+
.swiftpm/
26+
Package.resolved
27+
28+
# Code coverage
29+
*.profraw
30+
*.profdata
31+
32+
# Signing material / secrets — NEVER commit
33+
*.p12
34+
*.p12.b64
35+
*.pem
36+
*.key
37+
*.cer
38+
*.certSigningRequest
39+
*.mobileprovision
40+
*.provisionprofile
41+
*.env
42+
secrets*
43+
ExportOptions.plist
44+
45+
# Release pipeline output
46+
release-build/
47+
48+
# Dory runtime data
49+
.dory/
50+
51+
# Local tooling
52+
.claude/settings.local.json
53+
.claude/*.local.json
54+
.claude/launch.json
55+
.playwright-mcp/
56+
benchmark-results.json

COMPATIBILITY.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Dory Compatibility Matrix
2+
3+
This is the honest, maintained statement of what Dory does. It reflects the **current
4+
implementation**, which talks to a Docker Engine API backend through a pluggable runtime layer
5+
designed to also target Apple's `container` engine when present.
6+
7+
Legend: ✅ works · 🟡 works with Dory-specific behavior · 🛠️ implemented, activation gated ·
8+
⛔ unsupported / not yet · 🔒 blocked by an external gate.
9+
10+
## Docker Engine API (via Dory's socket `~/.dory/dory.sock`)
11+
12+
On the Docker backend, Dory's socket is a **full transparent proxy**: every request is forwarded
13+
verbatim and the response streamed back unchanged — uniformly correct for normal, streaming, and
14+
hijacked (upgrade) endpoints, with all request headers preserved (registry auth, etc.). The
15+
per-endpoint translation below is what the **Apple `container`** and mock backends present, since
16+
they have no Docker socket to forward to.
17+
18+
| Capability | Status | Notes |
19+
|---|---|---|
20+
| `docker version` / `info` / `_ping` || Docker backend: real engine response (transparent passthrough). Apple/mock: Dory-branded. Verified with the real `docker` CLI |
21+
| `docker ps` / list containers || Real containers, correct names/status/ports/timestamps |
22+
| Container start / stop / restart / remove || `POST /containers/{id}/...`, `DELETE /containers/{id}` |
23+
| `docker images` / list || Translated from the runtime snapshot (Apple/mock) |
24+
| Container create (with body) || image, cmd, env, ports, labels, network, restart policy; image refs starting with `-` rejected at the boundary |
25+
| Exec (create + start + inspect) || Used by the Compose health prober |
26+
| Image pull || `POST /images/create` |
27+
| Network create / remove || `POST /networks/create`, `DELETE /networks/{id}` |
28+
| Volume remove || `DELETE /volumes/{id}` |
29+
| Logs (`docker logs`, `-f`) || Docker backend: live follow proxied verbatim. Apple/mock: de-framed via streaming transport |
30+
| Stats (mem live, CPU%) || Docker backend: `docker stats` streamed through the proxy. Apple/mock: two-sample CPU sampler |
31+
| Events (`docker events`) || Docker backend: proxied (live engine events). Apple/mock: synthesized via `EventSynthesizer` |
32+
| `docker exec` (`-i`, `-it` TTY) + `attach` || Bidirectional hijack proxy with correct half-close (stdin EOF) + exit codes; TTY (`/dev/pts/0`) verified |
33+
| `docker cp` (archive get/put) || Both directions verified (incl. chunked request bodies) |
34+
| `docker build` (classic + **BuildKit**) || Both verified end-to-end via Dory's socket (BuildKit gRPC session proxied) |
35+
| Any other Docker endpoint (Docker backend) || Transparent proxy — distribution, swarm, plugins, etc. all pass through |
36+
| Full create-body flag coverage | 🟡 | Apple/mock translation maps common flags; the long tail is iterative (Docker backend forwards everything) |
37+
38+
## Compose
39+
40+
| Capability | Status | Notes |
41+
|---|---|---|
42+
| Parse `compose.yaml` || Block + flow YAML, quotes, comments (subset; no anchors/block scalars) |
43+
| Variable interpolation + `.env` || `$VAR`, `${VAR:-default}`, `${VAR-default}`, `$$` |
44+
| `depends_on` (short + long form) || `service_started` / `service_healthy` / `service_completed_successfully` |
45+
| Dependency ordering || Topological start order, cycle + dangling-dep detection |
46+
| Healthchecks || Exec-based probing + Docker-faithful state machine |
47+
| `up` / `down` || Native engine; AND the real `docker compose up/down` CLI drives Dory's socket (verified) |
48+
| GUI Compose view || Projects grouped by service with per-project + per-service start/stop |
49+
| Named/anonymous volumes | 🟡 | Anonymous-volume tracker built; full volume wiring iterative |
50+
| Profiles / multiple files / overrides || Parsed-aware; merge logic not yet |
51+
| `network_mode: service:` / shared pid/ipc || Co-schedule into one machine — by design, against Apple `container` |
52+
53+
## Engine backends
54+
55+
| Backend | Standalone? | Memory model | Notes |
56+
|---|---|---|---|
57+
| **Shared VM** (`DORY_RUNTIME=shared`) | ✅ yes | **One shared VM for all containers** (OrbStack-style) | Dory provisions one persistent Linux micro-VM on Apple's `container` engine running `dockerd` (DinD), publishes its socket to the host, and drives it with the verified Docker runtime. Verified: standalone (engine 29.5.3, no OrbStack), workloads share one VM. Measured: 2 containers = **1 VM @ ~122 MB** vs **~574 MB** as 3 per-container VMs. Persistent `/var/lib/docker` (overlayfs preserved across restarts); configurable CPUs/memory; idempotent reuse. |
58+
| **Docker** (default) | ❌ proxies host engine | host Docker/OrbStack | Transparent proxy to `/var/run/docker.sock`. Companion GUI, not a replacement. |
59+
| **Apple `container`** | ✅ yes | **One VM per container** | Native per-container micro-VMs; heavier for multi-container stacks. |
60+
61+
## OrbStack parity surface
62+
63+
All verified end-to-end on the shared-VM backend (default). System-wide binds (:53/:80/:443) and the
64+
CA trust install remain consent-gated — the same one-time admin grant OrbStack needs.
65+
66+
| Capability | Status | Notes |
67+
|---|---|---|
68+
| Native GUI (menu bar + main window) || All screens, both themes; one-click toggles for k8s/machines/shared-VM |
69+
| Standalone engine + shared-VM memory || Default backend; Dory runs its own `dockerd` in one VM — no OrbStack/Docker. ~4.7× leaner than per-container |
70+
| `localhost` access to published ports || `HostPortForwarder`; verified `localhost:port → 200`, dynamic add/teardown |
71+
| Automatic `*.dory.local` domains || `DoryDNS` resolver + `DoryReverseProxy`; verified `http://name.dory.local → 200`. System-wide via consent script |
72+
| Automatic local HTTPS || `DoryTLSProxy` terminates TLS with a `LocalCA` identity; verified `https://name.dory.local → 200` |
73+
| **Bind-mount file sharing** || Home dir shared into the VM (virtiofs); verified `docker run -v ~/proj:/app` reads/writes host files live |
74+
| One-click Kubernetes || `KubernetesProvisioner` runs k3s in the shared VM; verified host `kubectl` + pod deploy; GUI "Enable" button |
75+
| Linux machines (Ubuntu/Debian/Fedora/Alpine) || `MachineProvider` via `container machine`; verified real machine create/list/start/stop/delete; GUI picker |
76+
| x86/amd64 emulation | ✅ (qemu) | Auto-installs qemu binfmt; verified `--platform linux/amd64 → x86_64`. Rosetta fast-path is a documented gap |
77+
| Volume file browser || `VolumeBrowser`; verified list + read files inside volumes; GUI sheet |
78+
| Terminal / SSH into containers + machines || `TerminalLauncher` opens Terminal.app against Dory's socket/engine |
79+
| Docker Desktop / OrbStack migration || `MigrationAssistant` imports images + containers into Dory's shared VM |
80+
| `*.k8s.dory.local` service domains | ✅ HTTP + HTTPS | `KubeServiceProxy` runs `kubectl proxy`; the reverse/TLS proxy rewrites `<svc>.<ns>.k8s.dory.local` → the API service proxy. Verified `http`+`https → 200`. TLS cert carries per-namespace wildcard SANs (`*.default.k8s.dory.local`, `*.kube-system.k8s.dory.local`); other namespaces would need their wildcard added |
81+
| `dory` CLI (OrbStack's `orb`) || `scripts/dory` wraps the engine, machines, and kubectl |
82+
83+
### Remaining gaps — all blocked on ONE thing: `apple/containerization` framework integration
84+
85+
Every feature achievable through Apple's `container` CLI + the dind architecture is done. The four
86+
items below were each investigated and shown to need low-level VM control the CLI does not expose —
87+
device passthrough, memory ballooning, Rosetta device, custom mounts. They all become feasible once
88+
Dory links the `apple/containerization` Swift package and drives the VM in-process (the same
89+
integration the [packaging](#packaging) section roadmaps).
90+
91+
**Foundation built + PROVEN END-TO-END.** `Packages/ContainerizationEngine/` is an additive Swift
92+
package (separate from the shipping app) that links `apple/containerization` and drives the Linux VM
93+
directly via Virtualization.framework. It does not just compile — a signed boot harness
94+
(`dory-vmboot`, adhoc-signed with `com.apple.security.virtualization`) **boots a real Linux VM
95+
in-process and runs a container**, verified by exit code:
96+
97+
- `exit 42` — VM booted + container ran (kernel + initfs + image store all working in-process).
98+
- `exit 77` — an **amd64 image ran via Rosetta** (`uname -m == x86_64`) → **Rosetta-fast x86 PROVEN**.
99+
- `exit 99` — same run also read a **host file through a `Mount.share`** (`/shared/marker.txt`) →
100+
**bidirectional file sharing PROVEN**.
101+
102+
**Shipped to users via `dory vm`.** The engine is packaged as a bundled, entitlement-signed helper
103+
(`Helpers/dory-vm`, built + signed by `scripts/bundle-engine.sh`) that the `dory` CLI and the app
104+
invoke — exactly how Dory already invokes `container`/`docker`/`kubectl`, so the app gains the
105+
features without linking the framework's large dependency tree.
106+
107+
| Capability | Status | Delivery |
108+
|---|---|---|
109+
| Rosetta-speed x86 |**delivered** | `dory vm --arch amd64 --rosetta -- <cmd>``uname -m == x86_64`. Verified through the CLI |
110+
| Reverse / bidirectional file mount |**delivered** | `dory vm --mount host:guest -- <cmd>` reads/writes host files in the container. Verified |
111+
| USB / audio passthrough |**delivered** | `dory vm --devices`: a `VZInstanceExtension` injects an XHCI USB controller + `VZVirtioSoundDevice`. Verified `USB controllers attached: 1` |
112+
| Dynamic memory balloon → macOS |**delivered** | `dory vm --devices` attaches a balloon and reclaims RAM at runtime via the public `vzVirtualMachine` — verified `1024MiB → 512MiB reclaimed to macOS` |
113+
114+
**All four are delivered** through the bundled, entitlement-signed `dory-vm` helper, surfaced by the
115+
`dory` CLI (`dory vm`). The default shared-VM engine is untouched. (A GUI entry point for the
116+
in-process engine is not yet wired up.)
117+
118+
## Packaging — does the user need anything besides Dory.app?
119+
120+
The goal is a single download. Status:
121+
122+
| Component | Bundled? | How |
123+
|---|---|---|
124+
| Engine image (`docker:dind`) | ✅ implemented | `scripts/bundle-engine.sh` saves it into `Contents/Resources/dory-engine-image.tar`; `SharedVMProvisioner.ensureImage` loads it offline on first launch (no Docker Hub). Falls back to a registry pull if absent. |
125+
| Engine toolchain (`container` + kernel + plugins) | 🛠️ wired | `containerBinary()` prefers `Contents/Helpers/container`; the release script copies the ~315 MB toolchain in. Relocating the toolchain's launchd services needs hardening — the **production answer is to link the `apple/containerization` Swift package and spawn the VM in-process** (network-verified reachable; roadmapped). |
126+
| `docker` CLI | not needed | Dory's GUI manages everything; the Docker-compatible socket is there for users who want a CLI. |
127+
| **macOS 26+** | requirement, not a download | Apple's virtualization/containerization stack requires it — the unavoidable floor (same as Apple's `container`). |
128+
129+
So: **a self-contained Dory.app is achievable** (image bundling done; engine bundling wired, framework path roadmapped), requiring only macOS 26+ — no Homebrew, no Docker Hub, no Docker Desktop.
130+
131+
## Architectural / environment notes
132+
133+
- **Shared VM vs one-VM-per-container.** Dory offers BOTH: the Apple `container` backend is
134+
one-VM-per-container, while the **Shared VM backend** runs all containers in one VM like
135+
OrbStack — measured ~4.7× less memory for 2 containers (122 MB vs 574 MB), with the gap widening
136+
per container. This closes the headline memory gap and makes Dory a standalone engine.
137+
- **File-sharing performance** under the Apple `container` runtime + a real bind-mount dev loop is
138+
not yet benchmarked here.
139+
- **Distribution.** Signing works locally; **notarization requires an Apple Developer account**
140+
(external gate). The Homebrew Cask and an auto-updater are scaffolding still to add.
141+
- The app runs **unsandboxed** (like Docker Desktop/OrbStack) to reach the engine socket and
142+
host its own socket.

0 commit comments

Comments
 (0)