Skip to content

Fix: improve Docker startup and compose validation#78

Open
austin047 wants to merge 6 commits intoflatrun:mainfrom
austin047:fix/docker-startup-recommended
Open

Fix: improve Docker startup and compose validation#78
austin047 wants to merge 6 commits intoflatrun:mainfrom
austin047:fix/docker-startup-recommended

Conversation

@austin047
Copy link

  • Set DOCKER_HOST from config to environment and verify Docker is reachable at startup
  • Update config.example deployments_path to ./deployment
  • Create deployments directory on startup if not exist; fail fast if Docker is unavailable
  • Use the Default docker_socket from the Docker client env when unset
  • Validate compose in updateDeployment via Docker Compose config
  • Add moby/moby docker client dependency for platform socket detection

@sourceant
Copy link

sourceant bot commented Feb 18, 2026

Code Review Summary

This PR improves Docker integration by verifying reachability at startup, creating necessary directories, and implementing robust Compose validation using compose-go.

🚀 Key Improvements

  • Switched to compose-go for schema-aware Docker Compose validation.
  • Added startup check to fail fast if Docker is unreachable.
  • Automated creation of the deployments directory.

💡 Minor Suggestions

  • Remove unused _ parameter in ensureDockerReachable.
  • Cleanup unused os/exec and runtime imports if switching to library-based detection.

🚨 Critical Issues

  • Repeated use of exec.Command for Docker checks instead of using the already-imported Moby client library.

Copy link

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

@@ -42,6 +43,15 @@ func main() {
log.Fatalf("Failed to load config from %s: %v", resolvedConfigPath, err)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using exec.Command("docker", "info") relies on the docker CLI being installed in the PATH. Since you've added the moby/moby client dependency, it is more efficient and reliable to check connectivity using the library's Ping() or Info() methods instead of spawning a shell process.

Suggested change
}
func ensureDockerReachable(socket string) {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.WithHost(socket), client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("Failed to create Docker client: %v", err)
}
defer cli.Close()
if _, err := cli.Ping(ctx); err != nil {
log.Fatalf("Docker is not reachable: %v", err)
}
}

return nil
}

func validateComposeWithCLI(content string) error {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closing the file before deleting it is safer on some operating systems (like Windows). Also, consider using defer os.Remove(tmp.Name()) immediately after creation to ensure cleanup even if subsequent steps fail.

Suggested change
func validateComposeWithCLI(content string) error {
tmp, err := os.CreateTemp("", "compose-*.yml")
if err != nil {
log.Printf("Warning: skipping CLI validation, failed to create temp file: %v", err)
return nil
}
defer os.Remove(tmp.Name())
defer tmp.Close()

@austin047 austin047 marked this pull request as draft February 18, 2026 23:42
@austin047 austin047 changed the title fix: improve Docker startup and compose validation Fix: improve Docker startup and compose validation Feb 19, 2026
@austin047 austin047 marked this pull request as ready for review February 19, 2026 00:49
Copy link

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

log.Fatalf("Failed to load config from %s: %v", resolvedConfigPath, err)
}

os.Setenv("DOCKER_HOST", cfg.DockerSocket)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting DOCKER_HOST globally via os.Setenv can cause side effects if other parts of the application or third-party libraries expect a different environment. Since you've already added the Moby client, it is better to pass the socket explicitly to constructors rather than relying on global environment variables.

Suggested change
os.Setenv("DOCKER_HOST", cfg.DockerSocket)
ensureDockerReachable(cfg.DockerSocket)

}
if cfg.DockerSocket == "" {
cfg.DockerSocket = "unix:///var/run/docker.sock"
apiClient, err := client.New(client.FromEnv)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a new client just to detect the default host is slightly heavy. Note that client.FromEnv will look at the DOCKER_HOST environment variable first. Ensure this fallback logic matches the expected behavior if the agent is running inside a container.

Suggested change
apiClient, err := client.New(client.FromEnv)
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err == nil {
cfg.DockerSocket = cli.DaemonHost()
cli.Close()
} else {
cfg.DockerSocket = "unix:///var/run/docker.sock"
}

@austin047 austin047 marked this pull request as draft February 19, 2026 00:49
@austin047 austin047 marked this pull request as ready for review February 20, 2026 09:24
Copy link

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

@@ -98,15 +101,15 @@ type InfrastructureConfig struct {
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relying on docker context inspect via exec.Command adds unnecessary overhead and a dependency on the CLI binary being in the PATH at this specific moment. Since you've added the moby/moby client dependency, it is better to use the library's environment detection logic, which handles context, environment variables, and platform-specific defaults correctly.

Suggested change
}
func detectDockerHost() string {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
// Fallback to platform defaults if client creation fails
if runtime.GOOS == "windows" {
return "npipe:////./pipe/docker_engine"
}
return "unix:///var/run/docker.sock"
}
host := cli.DaemonHost()
cli.Close()
return host
}

_ = os.Remove(tmpName)
}()

if _, err := tmp.WriteString(content); err != nil {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation writes to the file, syncs, closes it, and then re-opens it via the docker compose CLI. While functional, it's cleaner to handle errors on WriteString and Sync more strictly if you want to ensure validation occurs. More importantly, since the file is already closed on line 2853, the deferred tmp.Close() on line 2841 will return an error (which is ignored), but it's redundant.

Suggested change
if _, err := tmp.WriteString(content); err != nil {
if _, err := tmp.WriteString(content); err != nil {
return fmt.Errorf("failed to write temp file for validation: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("failed to close temp file: %w", err)
}

Copy link
Contributor

@nfebe nfebe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, thanks for this a few things to address:

  • The PR description mentions adding moby/moby but it's not in the diff. Let's use the official Docker Go SDK (github.com/docker/docker/client) and compose-go SDK (github.com/compose-spec/compose-go) instead of shelling out to the CLI they handle socket detection, context resolution, and compose validation natively.
  • None of the new exec.Command calls have timeouts a hanging Docker daemon blocks the agent indefinitely.
  • ./deployment in the example config should be an absolute path outside the code directory and naming should be consistent with .gitignore.

_ = apiServer.Stop()
}

func ensureDockerReachable(_ string) {
Copy link
Contributor

@nfebe nfebe Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The os.Setenv("DOCKER_HOST") above unconditionally overwrites the env var even when empty, and this function accepts a socket param but ignores it (_ string). There is also no timeout a hanging daemon blocks startup forever.

Let's use the Docker Go SDK instead: client.NewClientWithOpts(client.FromEnv) + client.Ping(ctx) handles host resolution, timeouts via context, and doesn't need the docker binary in PATH.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use github.com/compose-spec/compose-go just did not want add to do just one thing

}
}

if err := validateComposeWithCLI(content); err != nil {
Copy link
Contributor

@nfebe nfebe Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's replace this with the compose-go SDK (github.com/compose-spec/compose-go/loader). It validates compose files in-process no temp files, no subprocess per request, proper Go error types, and no dependency on docker compose CLI being installed.

As-is this also has no timeout, so a hung daemon blocks the request handler.

@@ -1,4 +1,4 @@
deployments_path: /home/nfebe/work/deployments
deployments_path: ./deployment
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

./deployment (singular) doesn't match .gitignore which has deployments/ (plural). Also a relative path puts deployment data inside the project directory — something like /var/lib/flatrun/deployments would be a better default.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should stick to ./deployments, /var/lib/flatrun/deployments will require permission. If the user then wants to keep in that directory, they should create it. But for now lets stick to the ./deployments

}
}

func detectDockerHost() string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running docker context inspect as a side-effect of config.Load() makes config loading slower, silently broken if docker isn't installed, and harder to test.

The Docker Go SDK handles this natively — client.NewClientWithOpts(client.FromEnv) resolves DOCKER_HOST, Docker context, and platform defaults automatically. We can drop detectDockerHost() and dockerContextHost() entirely.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we have to rely on the DOCKER_HOST from client.FromEnv, then we will have to remove the socket from the config. It is kind of misleading in a way, because it doesn't get used in the load config.

@austin047
Copy link
Author

austin047 commented Feb 20, 2026

The Docker client has been migrated to moby/moby, even though you can still use the previous one. You can check out this moby/moby#50472 , I evaluated it, and we could safely use moby/moby as of this date and when the migration was done. You can have a look and then we settle.

@nfebe
Copy link
Contributor

nfebe commented Feb 21, 2026

The Docker client has been migrated to moby/moby, even though you can still use the previous one. You can check out this moby/moby#50472 , I evaluated it, and we could safely use moby/moby as of this date and when the migration was done. You can have a look and then we settle.

Thanks for this! But you did not add moby/moby so it will be good if we introduce it.

Copy link

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

_ = apiServer.Stop()
}

func ensureDockerReachable(_ string) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous review suggested using the Moby client library's Ping() or Info() instead of exec.Command("docker", "info"). This implementation still uses the shell command, which depends on the CLI being installed in the PATH and carries more overhead than an API call.

Suggested change
func ensureDockerReachable(_ string) {
func ensureDockerReachable(socket string) {
log.Println("Checking if Docker is reachable...")
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.WithHost(socket), client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("Failed to create Docker client: %v", err)
}
defer cli.Close()
if _, err := cli.Ping(ctx); err != nil {
log.Fatalf("Docker is not reachable: %v", err)
}
log.Println("Docker is reachable")
}

Copy link

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

return "unix:///var/run/docker.sock"
}

func dockerContextHost() string {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relying on docker context inspect via exec.Command adds unnecessary overhead and a dependency on the CLI binary. Use the Moby client's environment detection logic instead, which is already designed to handle contexts and platform defaults correctly.

Suggested change
func dockerContextHost() string {
func detectDockerHost() string {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
if runtime.GOOS == "windows" {
return "npipe:////./pipe/docker_engine"
}
return "unix:///var/run/docker.sock"
}
host := cli.DaemonHost()
cli.Close()
return host
}

@austin047
Copy link
Author

We will have to use the CLI to detect the host. The default implementation of the Moby client always falls back to platform-specific paths, and in some cases, the current context is the default OS socket context. For example, on my system the default path is unix:///home/fuh-austin/.docker/desktop/docker.sock. However, when DOCKER_HOST is not set in the environment, the Moby client defaults to using unix:///var/run/docker.sock. This behavior is hardcoded in the SDK.

So for now, using the CLI for this particular operation before setting DOCKER_HOST is the most optimal solution we have at the moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants