Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .changes/unreleased/+macos-persistent-install-ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Changed
body: Exercise Docker-managed persistent installs in Linux and macOS integration, including hosted macOS through a Docker-compatible runtime.
time: 2026-07-09T16:40:19+08:00
11 changes: 3 additions & 8 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,13 @@ jobs:
include:
- name: Linux amd64
runner: ubuntu-latest
persistent_install: false
persistent_install: true
- name: Linux arm64
runner: ubuntu-24.04-arm
persistent_install: false
persistent_install: true
- name: macOS amd64
runner: ${{ inputs.macos_runner || 'macos-15-intel' }}
# GitHub-hosted macOS runners use Colima, which provides a Linux
# Docker engine. Reploy's Docker-managed permanent install support
# on macOS is intentionally Docker Desktop-only, so this hosted
# gate validates the macOS staging/runtime path without claiming
# Docker Desktop install coverage.
persistent_install: false
persistent_install: true
Comment on lines 39 to +41
steps:
- name: Check out repository
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ supported runtime.
| Host OS | Docker | Staging | User install | System install |
| --- | --- | --- | --- | --- |
| Linux | Docker Engine | ✅ | ✅ | ✅ |
| macOS | Docker Desktop | ✅ | ✅ | — |
| macOS | Docker-compatible | ✅ | ✅ | — |
| Windows | Docker Desktop | ✅ | ✅ | — |

## Install
Expand Down
9 changes: 1 addition & 8 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,7 @@ This file is the day-to-day queue for design and implementation gaps.

## Now

- [ ] `P0` Revert hosted macOS persistent-install CI policy change.
Revert the CI/docs change that downgraded hosted macOS integration to
runtime-only after the runner exposed a non-Docker-Desktop backend.
Acceptance checks: persistent-install coverage is restored or the workflow
fails clearly at the unsupported runner boundary; the remaining runtime
smoke fix is kept only if it still reflects the intended smoke contract;
and any decision to change macOS install coverage is made explicitly by
the maintainer, not hidden inside a fix.
No active backlog items.

## Pre-release

Expand Down
13 changes: 7 additions & 6 deletions docs/CROSS_PLATFORM_INSTALL_LOCATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ Explicit scopes must be validated against backend capabilities:
candidate mechanisms include `systemd --user`; crontab `@reboot` is only
startup glue and is not equivalent to a service manager for status, restart,
logs, dependency ordering, or health supervision.
- Mac `user` uses the Docker Desktop-backed install path. Docker restart
policies can restart app containers after Docker Desktop starts, but Reploy
still depends on Docker Desktop itself being configured to start at login or
started by the user.
- Mac `user` uses the Docker-managed Compose install path. Docker restart
policies can restart app containers after the Docker-compatible runtime
starts, but Reploy still depends on that runtime being configured to start at
login or started by the user. Hosted CI currently validates this path with
Colima; Docker Desktop remains the primary user-facing runtime.
- Windows `user` uses the Docker Desktop-backed install path. Docker restart
policies can restart app containers after Docker Desktop starts, but Reploy
still depends on Docker Desktop itself being configured to start at login or
Expand Down Expand Up @@ -110,10 +111,10 @@ scope.
Initial target defaults:

| Host/backend | Scope | Default install root |
| --- | --- |
| --- | --- | --- |
| Linux systemd install | `system` | `/opt/{{ app.id }}` |
| Linux Docker-managed install | `user` | `{{ user.data }}/Reploy/installs/{{ app.id }}` |
| Mac Docker Desktop | `user` | `{{ user.data }}/Reploy/installs/{{ app.id }}` |
| Mac Docker-managed install | `user` | `{{ user.data }}/Reploy/installs/{{ app.id }}` |
| Windows Docker Desktop | `user` | `{{ user.local_data }}/Reploy/installs/{{ app.id }}` |

The concrete resolved examples are:
Expand Down
2 changes: 1 addition & 1 deletion docs/FUTURE_DIRECTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ and uninstall would all exercise installed-app behavior without requiring
Linux root/systemd system services for the common user-scope case.

This should be evaluated as a backend option, not assumed as a replacement for
the current Docker/systemd and Docker Desktop paths. On macOS and Windows,
the current Docker/systemd and Docker-managed paths. On macOS and Windows,
Podman containers still run on the host machine inside a Linux VM, so the
security and lifecycle promises are VM-backed userland promises rather than
native OS service promises. On Linux, rootless Podman depends on host
Expand Down
20 changes: 12 additions & 8 deletions docs/INSTALL_SCOPE_RUNTIME_SUPPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ unit, Compose project lifecycle, or host runtime authority belong to the
| Linux | Podman rootful with systemd/Quadlet | Partial | Yes | Partial | Good system substrate. Can set container user IDs, but the runtime is still rootful unless the unit is installed in a rootless user path. |
| Linux | Podman rootless with user systemd/Quadlet | Yes | No | Yes | Best candidate for a dedicated non-root app-user backend. Admin can create `arbiter`, place Quadlet files under the user's rootless unit path, and enable user-session persistence. |
| macOS | Docker Desktop | Yes | No | No | Docker Desktop is a user-session/VM-backed runtime. It can need privileged setup for helper configuration, but app containers are not native macOS services or per-app macOS users. |
| macOS | Colima/Lima Docker-compatible VM | Yes | No | Not as a host guarantee | Hosted CI validates this as a current-user Docker-managed install substrate. Reboot resistance depends on the VM/runtime startup policy outside Reploy. |
| macOS | Podman Machine | Yes | No | Not as a host guarantee | Containers run on the Mac inside a Podman-managed Linux VM. Per-app users inside the VM are possible, but they are not native macOS service-user isolation. |
| Windows | Docker Desktop per-user install | Yes | No | No | Per-user Docker Desktop can install/update without admin and uses WSL 2 for Linux containers. It is not a per-app Windows service-user model. |
| Windows | Docker Desktop all-users install | Partial | No | No | All-users install requires admin and can use WSL 2 or Hyper-V. It has broader host privileges but still does not provide a clean per-app user install scope for Linux containers. |
Expand Down Expand Up @@ -92,14 +93,15 @@ Linux containers.
Docker Desktop is a user-facing app backed by a Linux-container VM. It can be
installed and operated for a user, and Docker documents privileged helper setup
options, but this does not create a native macOS service-user boundary for each
installed app. It is best modeled as `current-user` Docker-managed persistence,
with reboot resistance depending on Docker Desktop starting at login.
installed app. It is best modeled as one `current-user` Docker-managed
persistence runtime, with reboot resistance depending on Docker Desktop
starting at login.

Podman Machine is similar from an install-scope perspective. It can provide a
local user-owned Linux VM on the Mac. Containers run on the Mac inside that VM,
not directly as macOS launchd services or macOS service users. A VM-internal
app user may still be useful, but it is not the same host guarantee as a Linux
dedicated app user.
Colima, Podman Machine, and similar Docker-compatible VM runtimes are similar
from an install-scope perspective. They can provide a local user-owned Linux VM
on the Mac. Containers run on the Mac inside that VM, not directly as macOS
launchd services or macOS service users. A VM-internal app user may still be
useful, but it is not the same host guarantee as a Linux dedicated app user.

## Windows

Expand Down Expand Up @@ -132,8 +134,10 @@ service-user isolation.
- Linux `dedicated app user` scope is most promising with rootless Podman plus
user systemd/Quadlet. It requires privileged setup, then unprivileged runtime
under the app account.
- macOS and Windows Docker Desktop should be treated as `current-user`
- macOS Docker-compatible VM runtimes should be treated as `current-user`
Docker-managed installs, not native system installs.
- Windows Docker Desktop should be treated as a `current-user` Docker-managed
install, not a native system install.
- macOS and Windows Podman Machine may improve runtime uniformity, but their
app-user isolation is VM-backed, not native host service-user isolation.

Expand Down
11 changes: 5 additions & 6 deletions docs/MAINTAINING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,11 @@ nox -s cli-integration

This integration test is intentionally outside the default CI session. It runs
before publishing and can be triggered manually from the Integration workflow.
On Linux, it covers the real-Docker staging runtime path. On GitHub-hosted
macOS runners, it covers the staging runtime path through Colima; macOS
Docker-managed permanent install remains Docker Desktop-only and needs
separate host evidence. On Windows, when run through `tools/e2e/smoke_windows.ps1`,
it covers the Docker-managed persistent install path with generated control
scripts.
On Linux, it covers the real-Docker staging runtime and Docker-managed
persistent-install path. On GitHub-hosted macOS runners, it covers the same
runtime and persistent-install path through Colima. On Windows, when run
through `tools/e2e/smoke_windows.ps1`, it covers the Docker-managed persistent
install path with generated control scripts.

For Windows release follow-up coverage, use the focused PowerShell wrapper:

Expand Down
29 changes: 16 additions & 13 deletions internal/dockerdeploy/doctor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func TestDoctorPreinstallPassesForInitializedDeployment(t *testing.T) {
}
}

func TestDoctorPreinstallOnDarwinRequiresDockerDesktopRuntime(t *testing.T) {
func TestDoctorPreinstallOnDarwinUserScopeAcceptsDockerRuntime(t *testing.T) {
disableDoctorColor(t)
restorePlatform := stubHostPlatform(t, hostPlatform{GOOS: "darwin"})
defer restorePlatform()
Expand All @@ -168,7 +168,7 @@ func TestDoctorPreinstallOnDarwinRequiresDockerDesktopRuntime(t *testing.T) {
if timeout != 2*time.Second {
t.Fatalf("docker timeout = %s, want 2s", timeout)
}
return dockerRuntimeInfo{Runtime: dockerRuntimeDockerDesktop, OperatingSystem: "Docker Desktop", ServerVersion: "29.5.3"}, nil
return dockerRuntimeInfo{Runtime: dockerRuntimeLinuxEngine, OperatingSystem: "Colima", ServerVersion: "29.5.3"}, nil
}

packDir := makeTestPack(t)
Expand All @@ -182,12 +182,12 @@ func TestDoctorPreinstallOnDarwinRequiresDockerDesktopRuntime(t *testing.T) {
}

var stdout strings.Builder
code := Doctor(DoctorOptions{Dir: deployDir, Preinstall: true, Stdout: &stdout, DockerPreflightTimeout: 2 * time.Second})
code := Doctor(DoctorOptions{Dir: deployDir, Preinstall: true, Scope: InstallScopeUser, Stdout: &stdout, DockerPreflightTimeout: 2 * time.Second})
if code != 0 {
t.Fatalf("doctor exit = %d\n%s", code, stdout.String())
}
for _, want := range []string{
"ok: Docker Desktop runtime detected: Docker Desktop",
"ok: Docker runtime detected: Colima",
"ok: preinstall checks passed",
} {
if !strings.Contains(stdout.String(), want) {
Expand Down Expand Up @@ -225,16 +225,16 @@ func TestDoctorPreinstallOnDarwinFailsWhenDockerRuntimeUnavailable(t *testing.T)
}

var stdout strings.Builder
code := Doctor(DoctorOptions{Dir: deployDir, Preinstall: true, Stdout: &stdout})
code := Doctor(DoctorOptions{Dir: deployDir, Preinstall: true, Scope: InstallScopeUser, Stdout: &stdout})
if code != 1 {
t.Fatalf("doctor exit = %d\n%s", code, stdout.String())
}
if !strings.Contains(stdout.String(), "fail: Docker Desktop runtime is required for Docker-managed permanent install: docker is not running") {
t.Fatalf("stdout missing Docker Desktop failure:\n%s", stdout.String())
if !strings.Contains(stdout.String(), "fail: Docker runtime is required for Docker-managed permanent install: docker is not running") {
t.Fatalf("stdout missing Docker runtime failure:\n%s", stdout.String())
}
}

func TestDoctorPreinstallOnDarwinFailsWhenRuntimeIsNotDockerDesktop(t *testing.T) {
func TestDoctorPreinstallOnDarwinUserScopeAcceptsNonDockerDesktopRuntime(t *testing.T) {
disableDoctorColor(t)
restorePlatform := stubHostPlatform(t, hostPlatform{GOOS: "darwin"})
defer restorePlatform()
Expand All @@ -257,15 +257,18 @@ func TestDoctorPreinstallOnDarwinFailsWhenRuntimeIsNotDockerDesktop(t *testing.T
}

var stdout strings.Builder
code := Doctor(DoctorOptions{Dir: deployDir, Preinstall: true, Stdout: &stdout})
if code != 1 {
code := Doctor(DoctorOptions{Dir: deployDir, Preinstall: true, Scope: InstallScopeUser, Stdout: &stdout})
if code != 0 {
t.Fatalf("doctor exit = %d\n%s", code, stdout.String())
}
if !strings.Contains(stdout.String(), "fail: Docker Desktop runtime is required for Docker-managed permanent install; detected: Ubuntu 24.04") {
t.Fatalf("stdout missing Docker Desktop runtime failure:\n%s", stdout.String())
if !strings.Contains(stdout.String(), "ok: Docker runtime detected: Ubuntu 24.04") {
t.Fatalf("stdout missing Docker runtime success:\n%s", stdout.String())
}
if !strings.Contains(stdout.String(), "ok: preinstall checks passed") {
t.Fatalf("stdout missing preinstall success:\n%s", stdout.String())
}
if strings.Contains(stdout.String(), "warn:") {
t.Fatalf("wrong runtime should fail without advisory warnings:\n%s", stdout.String())
t.Fatalf("non-Docker-Desktop runtime should pass without advisory warnings:\n%s", stdout.String())
}
}

Expand Down
10 changes: 8 additions & 2 deletions internal/dockerdeploy/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestInstallDryRunPrintsPlan(t *testing.T) {
}
}

func TestInstallDryRunOnDarwinPrintsDockerDesktopPlan(t *testing.T) {
func TestInstallDryRunOnDarwinPrintsDockerManagedPlan(t *testing.T) {
disableDoctorColor(t)
restorePlatform := stubHostPlatform(t, hostPlatform{GOOS: "darwin"})
defer restorePlatform()
Expand All @@ -83,7 +83,7 @@ func TestInstallDryRunOnDarwinPrintsDockerDesktopPlan(t *testing.T) {
detectDockerRuntimeForDoctor = previousDetector
})
detectDockerRuntimeForDoctor = func(context.Context, CommandSpec, time.Duration) (dockerRuntimeInfo, error) {
return dockerRuntimeInfo{Runtime: dockerRuntimeDockerDesktop, OperatingSystem: "Docker Desktop"}, nil
return dockerRuntimeInfo{Runtime: dockerRuntimeLinuxEngine, OperatingSystem: "Colima"}, nil
}
packDir := makeTestPackWithManifest(t, strings.Replace(testPackManifest(), ` owner:
user: "1000"
Expand Down Expand Up @@ -635,9 +635,15 @@ func TestInstallScopeValidationRejectsUnsupportedCombinations(t *testing.T) {
if got := (hostPlatform{GOOS: "linux"}).installBackendForScope(InstallScopeUser); got != installBackendDockerManaged {
t.Fatalf("linux user backend = %s, want %s", got, installBackendDockerManaged)
}
if got := (hostPlatform{GOOS: "darwin"}).installBackendForScope(InstallScopeUser); got != installBackendDockerManaged {
t.Fatalf("macos user backend = %s, want %s", got, installBackendDockerManaged)
}
if err := validateInstallScopeForBackend(InstallScopeUser, installBackendDockerManaged, hostPlatform{GOOS: "linux"}); err != nil {
t.Fatalf("linux user docker-managed error = %v", err)
}
if err := validateInstallScopeForBackend(InstallScopeUser, installBackendDockerManaged, hostPlatform{GOOS: "darwin"}); err != nil {
t.Fatalf("macos user docker-managed error = %v", err)
}
if err := validateInstallScopeForBackend(InstallScopeSystem, installBackendDockerDesktop, hostPlatform{GOOS: "darwin"}); err == nil || !strings.Contains(err.Error(), "--scope system is not supported on macos with Docker Desktop") {
t.Fatalf("macos system error = %v", err)
}
Expand Down
10 changes: 5 additions & 5 deletions internal/dockerdeploy/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (platform hostPlatform) installBackend() installBackend {
}

func (platform hostPlatform) installBackendForScope(scope InstallScope) installBackend {
if platform.GOOS == "linux" && scope == InstallScopeUser {
if (platform.GOOS == "linux" || platform.GOOS == "darwin") && scope == InstallScopeUser {
return installBackendDockerManaged
}
return platform.installBackend()
Expand All @@ -118,7 +118,7 @@ func (platform hostPlatform) commandSupport(command platformCommand) platformCom
case "linux":
return linuxCommandSupport(command)
case "darwin":
return dockerDesktopCommandSupport(command)
return dockerManagedPOSIXCommandSupport(command)
case "windows":
return windowsCommandSupport(command)
default:
Expand Down Expand Up @@ -157,7 +157,7 @@ func linuxCommandSupport(command platformCommand) platformCommandSupport {
}
}

func dockerDesktopCommandSupport(command platformCommand) platformCommandSupport {
func dockerManagedPOSIXCommandSupport(command platformCommand) platformCommandSupport {
switch command {
case platformCommandHelp,
platformCommandIndex,
Expand All @@ -177,13 +177,13 @@ func dockerDesktopCommandSupport(command platformCommand) platformCommandSupport
platformCommandTest,
platformCommandDoctorPreinstall,
platformCommandInstall:
return supportedCommandSupport(true)
return supportedCommandSupport(false)
case platformCommandUninstallServiceName,
platformCommandUninstallList:
return unsupportedCommandSupport("Linux/systemd service discovery is not part of Docker-managed install")
case platformCommandInstalledPowerShell,
platformCommandWindowsService:
return unsupportedCommandSupport("not part of the macOS Docker Desktop support path")
return unsupportedCommandSupport("not part of the macOS Docker-managed support path")
default:
return unsupportedCommandSupport("unknown command surface")
}
Expand Down
18 changes: 8 additions & 10 deletions internal/dockerdeploy/platform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,18 +354,16 @@ func TestPlatformCommandSupportSelectedNonWindowsContracts(t *testing.T) {
wantReasonSubstring: "Linux support path",
},
{
name: "macOS install requires Docker Desktop",
platform: hostPlatform{GOOS: "darwin"},
command: platformCommandInstall,
wantStatus: platformSupportSupported,
wantDockerDesktop: true,
name: "macOS install uses Docker-managed backend",
platform: hostPlatform{GOOS: "darwin"},
command: platformCommandInstall,
wantStatus: platformSupportSupported,
},
{
name: "macOS runtime commands require Docker Desktop",
platform: hostPlatform{GOOS: "darwin"},
command: platformCommandStagingRuntime,
wantStatus: platformSupportSupported,
wantDockerDesktop: true,
name: "macOS runtime commands use Docker-managed backend",
platform: hostPlatform{GOOS: "darwin"},
command: platformCommandStagingRuntime,
wantStatus: platformSupportSupported,
},
{
name: "macOS list uninstall is unsupported",
Expand Down
2 changes: 1 addition & 1 deletion website/docs/blueprint-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ blueprint omits target defaults, Reploy chooses a built-in host default:

| Field | Value |
| --- | --- |
| Host/backend | Mac Docker Desktop |
| Host/backend | Mac Docker-managed runtime |
| Built-in default | `{{ user.data }}/Reploy/installs/{{ app.id }}` |
| For `app.id: example-app` | `$HOME/Library/Application Support/Reploy/installs/example-app` |

Expand Down
2 changes: 1 addition & 1 deletion website/docs/support-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ Reploy currently supports Docker-backed staging and installs.
| Host OS | Release targets | Docker runtime | Staging | User install | System install |
| --- | --- | --- | --- | --- | --- |
| Linux | `linux-amd64`, `linux-arm64` | Docker Engine | ✅ | ✅ Docker-managed | ✅ systemd |
| macOS | `darwin-amd64`, `darwin-arm64` | Docker Desktop | ✅ | ✅ Docker-managed | — |
| macOS | `darwin-amd64`, `darwin-arm64` | Docker-compatible | ✅ | ✅ Docker-managed | — |
| Windows | `windows-amd64`, `windows-arm64` | Docker Desktop | ✅ | ✅ Docker-managed | — |
Loading