Skip to content

Commit 6862c0d

Browse files
authored
feat: honor DOCKER_HOST for Docker daemon resolution (#4)
Resolve the Docker endpoint from DOCKER_HOST (with the legacy DOCKER_SOCK override and an OS default fallback) so Podman, rootless setups, and remote Docker contexts work without assuming /var/run/docker.sock. start/az start/gcp start bind-mount the resolved unix socket or pass a remote tcp:// daemon through via DOCKER_HOST; doctor's docker.socket check reports the resolved endpoint. Adds DockerHostResolveTest and README documentation. Closes #3
1 parent 6746896 commit 6862c0d

8 files changed

Lines changed: 204 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This project uses [Semantic Versioning](https://semver.org/).
99

1010
### Added
1111

12+
- `floci start` (and `floci az`/`floci gcp start`) now honor the standard `DOCKER_HOST` environment variable, so Podman, rootless setups, and remote Docker contexts work without assuming `/var/run/docker.sock`. `DOCKER_HOST` is resolved into a unix socket, remote TCP daemon, or Windows named pipe — unix sockets are bind-mounted, remote TCP daemons are passed through to the container via `DOCKER_HOST`. Precedence is `DOCKER_HOST``DOCKER_SOCK` (legacy override) → OS default. `floci doctor`'s `docker.socket` check reports the resolved endpoint ([#3](https://github.com/floci-io/floci-cli/issues/3))
1213
- Release ships a `darwin/amd64` (Intel macOS) native binary again — built with an x86_64 GraalVM under Rosetta 2 on the Apple Silicon runner, avoiding the unreliable/queue-bound `macos-13` Intel runner that previously caused the target to be dropped (a past release sat 9.5h on `macos-13` before being cancelled). The Homebrew formula bump again wires the `darwin/amd64` SHA, so `brew install` and the install script resolve a real Intel binary instead of 404ing ([#2](https://github.com/floci-io/floci-cli/issues/2))
1314
### Fixed
1415

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,38 @@ floci az start --port 4578 # custom host port
209209
floci az start --persist ./data # persist state to a host directory
210210
```
211211

212+
#### Docker daemon resolution (Podman, rootless, remote contexts)
213+
214+
The Floci container needs access to a Docker-compatible daemon (for Lambda, EC2,
215+
EKS, MSK, ECR, CodeBuild, and Kafka/Redpanda support). By default `floci start`
216+
bind-mounts `/var/run/docker.sock`, but it honors the standard `DOCKER_HOST`
217+
environment variable, so Podman, rootless setups, and remote Docker contexts work
218+
without extra flags:
219+
220+
```sh
221+
# Rootless Podman
222+
export DOCKER_HOST=unix:///run/user/1000/podman/podman.sock
223+
floci start
224+
225+
# Rootful Podman
226+
export DOCKER_HOST=unix:///run/podman/podman.sock
227+
floci start
228+
229+
# Remote daemon over TCP
230+
export DOCKER_HOST=tcp://10.0.0.5:2375
231+
floci start
232+
```
233+
234+
Resolution precedence:
235+
236+
1. **`DOCKER_HOST`** — the standard Docker/Podman variable (`unix://` socket, `tcp://` daemon, or `npipe://` on Windows)
237+
2. **`DOCKER_SOCK`** — legacy override (a bare socket path)
238+
3. **OS default**`/var/run/docker.sock` on Linux/macOS, or the Docker named pipe on Windows
239+
240+
For a `unix://` socket the resolved path is bind-mounted into the container; for a
241+
remote `tcp://` daemon the `DOCKER_HOST` value is passed through to the container
242+
instead. Run `floci doctor` to see which endpoint was resolved.
243+
212244
### `floci stop` / `floci az stop`
213245

214246
```sh
@@ -387,6 +419,11 @@ services:
387419
- /var/run/docker.sock:/var/run/docker.sock
388420
```
389421
422+
> Using Podman or a non-default daemon? Swap the host side of the socket mount for
423+
> your daemon's socket (e.g. `/run/user/1000/podman/podman.sock:/var/run/docker.sock`).
424+
> With the CLI, setting `DOCKER_HOST` is enough — see
425+
> [Docker daemon resolution](#docker-daemon-resolution-podman-rootless-remote-contexts).
426+
390427
### Azure CI
391428

392429
```sh

src/main/java/io/floci/cli/commands/StartCommand.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public Integer call() {
8181
List<String> args = new ArrayList<>();
8282
args.addAll(List.of("-d", "--name", global.container));
8383
args.addAll(List.of("-p", port + ":4566"));
84-
args.addAll(List.of("-v", "/var/run/docker.sock:/var/run/docker.sock"));
84+
args.addAll(DockerClient.dockerSocketRunArgs());
8585
if (persistDir != null) {
8686
args.addAll(List.of("-v", persistDir + ":/var/lib/floci"));
8787
}

src/main/java/io/floci/cli/commands/az/AzStartCommand.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public Integer call() {
7676
List<String> args = new ArrayList<>();
7777
args.addAll(List.of("-d", "--name", global.container));
7878
args.addAll(List.of("-p", port + ":4577"));
79-
args.addAll(List.of("-v", "/var/run/docker.sock:/var/run/docker.sock"));
79+
args.addAll(DockerClient.dockerSocketRunArgs());
8080
if (persistDir != null) {
8181
args.addAll(List.of("-v", persistDir + ":/app/data"));
8282
}

src/main/java/io/floci/cli/commands/gcp/GcpStartCommand.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public Integer call() {
7676
List<String> args = new ArrayList<>();
7777
args.addAll(List.of("-d", "--name", global.container));
7878
args.addAll(List.of("-p", port + ":4588"));
79-
args.addAll(List.of("-v", "/var/run/docker.sock:/var/run/docker.sock"));
79+
args.addAll(DockerClient.dockerSocketRunArgs());
8080
if (persistDir != null) {
8181
args.addAll(List.of("-v", persistDir + ":/app/data"));
8282
}

src/main/java/io/floci/cli/docker/DockerClient.java

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,74 @@ public static boolean isInstalled() {
173173
}
174174
}
175175

176+
/** How the Docker daemon is reached, resolved from the environment. */
177+
public enum Kind { UNIX, TCP, NPIPE }
178+
179+
/**
180+
* Parsed Docker daemon endpoint. {@code socketPath} is the local socket/pipe path
181+
* for {@link Kind#UNIX}/{@link Kind#NPIPE}, and null for {@link Kind#TCP}.
182+
*/
183+
public record DockerHost(Kind kind, String socketPath, String raw) {}
184+
185+
private static final String DEFAULT_UNIX_SOCKET = "/var/run/docker.sock";
186+
private static final String DEFAULT_WINDOWS_PIPE = "\\\\.\\pipe\\docker_engine";
187+
188+
/** Resolve the Docker endpoint from the current process environment and OS. */
189+
public static DockerHost dockerHost() {
190+
return parseDockerHost(
191+
System.getenv("DOCKER_HOST"),
192+
System.getenv("DOCKER_SOCK"),
193+
System.getProperty("os.name", ""));
194+
}
195+
196+
/**
197+
* Pure resolver for the Docker endpoint. Precedence: {@code DOCKER_HOST}
198+
* (standard) → {@code DOCKER_SOCK} (legacy override) → OS default.
199+
*/
200+
public static DockerHost parseDockerHost(String dockerHostEnv, String dockerSockEnv, String osName) {
201+
boolean windows = osName != null && osName.toLowerCase().contains("win");
202+
203+
if (dockerHostEnv != null && !dockerHostEnv.isBlank()) {
204+
String value = dockerHostEnv.trim();
205+
if (value.startsWith("unix://")) {
206+
return new DockerHost(Kind.UNIX, value.substring("unix://".length()), value);
207+
}
208+
if (value.startsWith("tcp://") || value.startsWith("http://") || value.startsWith("https://")) {
209+
return new DockerHost(Kind.TCP, null, value);
210+
}
211+
if (value.startsWith("npipe://")) {
212+
return new DockerHost(Kind.NPIPE, value.substring("npipe://".length()), value);
213+
}
214+
// No scheme — treat as a bare socket/pipe path.
215+
return new DockerHost(windows ? Kind.NPIPE : Kind.UNIX, value, value);
216+
}
217+
218+
if (dockerSockEnv != null && !dockerSockEnv.isBlank()) {
219+
return new DockerHost(Kind.UNIX, dockerSockEnv.trim(), dockerSockEnv.trim());
220+
}
221+
222+
if (windows) {
223+
return new DockerHost(Kind.NPIPE, DEFAULT_WINDOWS_PIPE, null);
224+
}
225+
return new DockerHost(Kind.UNIX, DEFAULT_UNIX_SOCKET, null);
226+
}
227+
228+
/** Back-compat accessor for the resolved local socket/pipe path (null for TCP). */
176229
public static String socketPath() {
177-
String os = System.getProperty("os.name", "").toLowerCase();
178-
if (os.contains("win")) return "\\\\.\\pipe\\docker_engine";
179-
return System.getenv().getOrDefault("DOCKER_SOCK", "/var/run/docker.sock");
230+
return dockerHost().socketPath();
231+
}
232+
233+
/**
234+
* The {@code docker run} arguments needed to give a container access to the host
235+
* Docker daemon. Unix sockets are bind-mounted at the canonical in-container path;
236+
* remote TCP daemons are passed through via {@code DOCKER_HOST}.
237+
*/
238+
public static List<String> dockerSocketRunArgs() {
239+
DockerHost host = dockerHost();
240+
return switch (host.kind()) {
241+
case TCP -> List.of("-e", "DOCKER_HOST=" + host.raw());
242+
case UNIX -> List.of("-v", host.socketPath() + ":" + DEFAULT_UNIX_SOCKET);
243+
case NPIPE -> List.of("-v", DEFAULT_UNIX_SOCKET + ":" + DEFAULT_UNIX_SOCKET);
244+
};
180245
}
181246
}

src/main/java/io/floci/cli/doctor/checks/DockerSocketCheck.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,23 @@ public class DockerSocketCheck implements Check {
1111

1212
@Override
1313
public CheckResult run(String endpoint, String container) {
14-
String socketPath = DockerClient.socketPath();
15-
String os = System.getProperty("os.name", "").toLowerCase();
16-
if (os.contains("win")) {
17-
return CheckResult.ok("docker.socket", "Windows named pipe (" + socketPath + ")");
14+
DockerClient.DockerHost host = DockerClient.dockerHost();
15+
switch (host.kind()) {
16+
case NPIPE:
17+
return CheckResult.ok("docker.socket", "Windows named pipe (" + host.socketPath() + ")");
18+
case TCP:
19+
return CheckResult.ok("docker.socket", "remote daemon (" + host.raw() + ")");
20+
default:
21+
break;
1822
}
23+
String socketPath = host.socketPath();
1924
if (Files.exists(Path.of(socketPath))) {
2025
return CheckResult.ok("docker.socket", socketPath + " accessible");
2126
}
27+
String os = System.getProperty("os.name", "").toLowerCase();
2228
String fix = os.contains("mac")
2329
? "Open Docker Desktop — the socket is created when Docker Desktop is running"
24-
: "sudo chmod 666 /var/run/docker.sock OR add your user to the docker group";
30+
: "sudo chmod 666 " + socketPath + " OR add your user to the docker group";
2531
return CheckResult.fail("docker.socket", socketPath + " not found or not accessible", fix);
2632
}
2733
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package io.floci.cli.unit;
2+
3+
import io.floci.cli.docker.DockerClient;
4+
import io.floci.cli.docker.DockerClient.DockerHost;
5+
import io.floci.cli.docker.DockerClient.Kind;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
10+
class DockerHostResolveTest {
11+
12+
@Test
13+
void rootlessPodmanUnixSocket() {
14+
DockerHost h = DockerClient.parseDockerHost(
15+
"unix:///run/user/1000/podman/podman.sock", null, "Linux");
16+
assertEquals(Kind.UNIX, h.kind());
17+
assertEquals("/run/user/1000/podman/podman.sock", h.socketPath());
18+
}
19+
20+
@Test
21+
void rootfulDockerUnixSocket() {
22+
DockerHost h = DockerClient.parseDockerHost("unix:///var/run/docker.sock", null, "Linux");
23+
assertEquals(Kind.UNIX, h.kind());
24+
assertEquals("/var/run/docker.sock", h.socketPath());
25+
}
26+
27+
@Test
28+
void remoteTcpDaemon() {
29+
DockerHost h = DockerClient.parseDockerHost("tcp://10.0.0.1:2375", null, "Linux");
30+
assertEquals(Kind.TCP, h.kind());
31+
assertNull(h.socketPath());
32+
assertEquals("tcp://10.0.0.1:2375", h.raw());
33+
}
34+
35+
@Test
36+
void barePathTreatedAsUnixSocket() {
37+
DockerHost h = DockerClient.parseDockerHost("/run/user/1000/podman/podman.sock", null, "Linux");
38+
assertEquals(Kind.UNIX, h.kind());
39+
assertEquals("/run/user/1000/podman/podman.sock", h.socketPath());
40+
}
41+
42+
@Test
43+
void npipeScheme() {
44+
DockerHost h = DockerClient.parseDockerHost("npipe:////./pipe/podman", null, "Windows 11");
45+
assertEquals(Kind.NPIPE, h.kind());
46+
assertEquals("//./pipe/podman", h.socketPath());
47+
}
48+
49+
@Test
50+
void dockerHostTakesPrecedenceOverDockerSock() {
51+
DockerHost h = DockerClient.parseDockerHost(
52+
"unix:///run/podman/podman.sock", "/var/run/docker.sock", "Linux");
53+
assertEquals(Kind.UNIX, h.kind());
54+
assertEquals("/run/podman/podman.sock", h.socketPath());
55+
}
56+
57+
@Test
58+
void fallsBackToDockerSock() {
59+
DockerHost h = DockerClient.parseDockerHost(null, "/custom/docker.sock", "Linux");
60+
assertEquals(Kind.UNIX, h.kind());
61+
assertEquals("/custom/docker.sock", h.socketPath());
62+
}
63+
64+
@Test
65+
void blankEnvIgnored() {
66+
DockerHost h = DockerClient.parseDockerHost(" ", " ", "Linux");
67+
assertEquals(Kind.UNIX, h.kind());
68+
assertEquals("/var/run/docker.sock", h.socketPath());
69+
}
70+
71+
@Test
72+
void defaultLinuxSocket() {
73+
DockerHost h = DockerClient.parseDockerHost(null, null, "Linux");
74+
assertEquals(Kind.UNIX, h.kind());
75+
assertEquals("/var/run/docker.sock", h.socketPath());
76+
}
77+
78+
@Test
79+
void defaultWindowsPipe() {
80+
DockerHost h = DockerClient.parseDockerHost(null, null, "Windows 11");
81+
assertEquals(Kind.NPIPE, h.kind());
82+
assertEquals("\\\\.\\pipe\\docker_engine", h.socketPath());
83+
}
84+
}

0 commit comments

Comments
 (0)