in internal/compose/compose.go [348:411]
func (p *Project) WaitForHealthy(ctx context.Context, opts CommandOptions) error {
// Read container IDs
args := p.baseArgs()
args = append(args, "ps", "-a", "-q")
var b bytes.Buffer
if err := p.runDockerComposeCmd(ctx, dockerComposeOptions{args: args, env: opts.Env, stdout: &b}); err != nil {
return err
}
ctx, stop := context.WithTimeout(ctx, waitForHealthyTimeout)
defer stop()
containerIDs := strings.Fields(b.String())
for {
// NOTE: healthy must be reinitialized at each iteration
healthy := true
logger.Debugf("Wait for healthy containers: %s", strings.Join(containerIDs, ","))
descriptions, err := docker.InspectContainers(containerIDs...)
if err != nil {
return err
}
for _, d := range descriptions {
switch {
// No healthcheck defined for service
case d.State.Status == "running" && d.State.Health == nil:
logger.Debugf("Container %s (%s) status: %s (no health status)", d.Config.Labels.ComposeService, d.ID, d.State.Status)
// Service is up and running and it's healthy
case d.State.Status == "running" && d.State.Health.Status == "healthy":
logger.Debugf("Container %s (%s) status: %s (health: %s)", d.Config.Labels.ComposeService, d.ID, d.State.Status, d.State.Health.Status)
// Container started and finished with exit code 0
case d.State.Status == "exited" && d.State.ExitCode == 0:
logger.Debugf("Container %s (%s) status: %s (exit code: %d)", d.Config.Labels.ComposeService, d.ID, d.State.Status, d.State.ExitCode)
// Container exited with code > 0
case d.State.Status == "exited" && d.State.ExitCode > 0:
logger.Debugf("Container %s (%s) status: %s (exit code: %d)", d.Config.Labels.ComposeService, d.ID, d.State.Status, d.State.ExitCode)
return fmt.Errorf("container (ID: %s) exited with code %d", d.ID, d.State.ExitCode)
// Any different status is considered unhealthy
default:
logger.Debugf("Container %s (%s) status: unhealthy", d.Config.Labels.ComposeService, d.ID)
healthy = false
}
}
// end loop before timeout if healthy
if healthy {
break
}
select {
case <-ctx.Done():
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return errors.New("timeout waiting for healthy container")
}
return ctx.Err()
// NOTE: using after does not guarantee interval but it's ok for this use case
case <-time.After(waitForHealthyInterval):
}
}
return nil
}