Skip to content
46 changes: 46 additions & 0 deletions crates/alien-local/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,49 @@ Local platform implementation. "Local" is a full Alien platform — applications
Application code is identical across platforms — the same `storage.put(key, data)` call works on Local, AWS, GCP, and Azure.

Used by `alien dev` for local development, and deployable to any machine via `alien-deploy deploy --platform local`.

## Docker endpoint selection

Local container and sandbox API clients read `DOCKER_CONFIG` (or the home
`.docker` directory) and select an endpoint in this order:

1. A nonempty `DOCKER_HOST`.
2. A nonempty `DOCKER_CONTEXT`.
3. `currentContext` in `config.json`.
4. Docker's platform default socket or named pipe.

The `default` context is synthetic and honors `DOCKER_HOST`. Named contexts use
Docker CLI's `context inspect` output and ignore environment TLS options. Missing or invalid
selected context metadata fails with a structured error; another daemon is never
tried. This matches Docker CLI's source and installed behavior, including host-first
precedence when both environment variables are set.

Unix sockets, Windows named pipes, and plaintext TCP endpoints are supported.
SSH, TLS environment settings, and TCP contexts with TLS material or verification
settings return actionable unsupported-transport errors. Socket contexts ignore
TLS verification settings, as Docker CLI does. TLS is never silently
removed. Image loading through Docker CLI is pinned to the endpoint captured by
the container API client, including after the active context changes.

Docker bridge discovery remains optional for native services on hosts without a
bindable bridge. Configuration and unsupported-transport errors still propagate. Native services
using the default socket do not require Docker CLI; named contexts require the
CLI to inspect their metadata without duplicating its context-store format.

To validate selection against a real engine, create a dedicated empty daemon with
its own Unix socket, data directory, execution directory, and PID file. Use an
isolated `DOCKER_CONFIG`, create and select a context pointing to that socket, and
run the ignored `docker_connection::tests::dedicated_engine_proof` test with
nextest. It must ping the engine, obtain its version, and confirm there are no
containers. `dedicated_engine_baseline` verifies that Bollard's old local-default
connector cannot select that engine. `dedicated_pinned_subprocess` verifies CLI
selection after the persisted context changes. These fixtures perform read-only
API operations and require no application builds or containers.

Host overrides and native default selection work without `HOME`, `USERPROFILE`,
or `DOCKER_CONFIG`. A configuration directory is required only to inspect a
selected named context. Ordinary library tests inject context-inspection JSON
fixtures and do not require Docker CLI or modify process environment. The ignored
named-context integration proofs require Docker CLI, an isolated configuration,
and a dedicated empty engine. Separate child-process proofs validate host and
native default selection with a cleared environment and no Docker executable.
10 changes: 4 additions & 6 deletions crates/alien-local/src/container_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ pub struct LocalRuntimeStatus {
#[derive(Debug)]
pub struct LocalContainerManager {
docker: Docker,
docker_host: String,
state_dir: PathBuf,
/// Tracked containers (container_id → metadata)
containers: Arc<RwLock<HashMap<String, ContainerMetadata>>>,
Expand Down Expand Up @@ -453,11 +454,7 @@ impl LocalContainerManager {
/// # Arguments
/// * `state_dir` - Base directory for container metadata
pub fn new(state_dir: PathBuf) -> Result<Self> {
let docker = Docker::connect_with_local_defaults()
.into_alien_error()
.context(ErrorData::DockerConnectionFailed {
reason: "Failed to connect to Docker daemon. Is Docker running?".to_string(),
})?;
let (docker, docker_host) = crate::docker_connection::connect_docker_with_host()?;

let containers = Self::load_metadata_from_disk(&state_dir)?
.into_iter()
Expand All @@ -466,6 +463,7 @@ impl LocalContainerManager {

Ok(Self {
docker,
docker_host,
state_dir,
containers: Arc::new(RwLock::new(containers)),
image_load_locks: tokio::sync::Mutex::new(HashMap::new()),
Expand Down Expand Up @@ -683,7 +681,7 @@ impl LocalContainerManager {

// Use `docker load` instead of `import_image` to preserve CMD/ENTRYPOINT
// docker import is for filesystem tarballs, docker load is for OCI image tarballs
let output = tokio::process::Command::new("docker")
let output = crate::docker_connection::docker_command(&self.docker_host)
.args(&["load", "-i", &tarball_path.to_string_lossy()])
.output()
.await
Expand Down
Loading
Loading