nektos/act · error

failed to parse service

Error message

failed to parse service %s ports: %w

What it means

Thrown when Docker's nat.ParsePortSpecs fails to parse the interpolated `ports:` entries of a job's service container definition. Each port string (after ${{ }} interpolation) must match Docker's port-spec grammar (e.g. '8080:80', '127.0.0.1:8080:80/tcp'); any malformed entry makes ParsePortSpecs return an error, which act wraps with the service name.

Solutions

  1. Check the exact ports list printed for the failing service and fix or remove malformed entries; every entry must be '[hostIP:]hostPort[:containerPort][/proto|/range...]'
  2. >=8080
  3. '80'
  4. '53/udp'.
  5. If the port comes from an expression like ${{ matrix.port }}, verify the matrix key exists and the value is numeric — an empty result after interpolation fails parsing.
  6. Remove host-IP or range syntax that is unsupported and use the simplest 'host:container' form.
  7. Lint the workflow with actionlint or `act --list` to catch syntax problems before execution.

Example fix

# before
services:
  redis:
    image: redis
    ports:
      - '${{ matrix.port }}'   # matrix.port undefined -> ''
# after
services:
  redis:
    image: redis
    ports:
      - '6379:6379'
Defensive patterns

Strategy: validation

Validate before calling

// Validate service port specs before running act (Go, mirroring Docker nat rules)
import (
  "fmt"
  "regexp"
)
var portRe = regexp.MustCompile(`^([0-9.]+:)?([0-9]+|[0-9]+-[0-9]+)(:[0-9]+(-[0-9]+)?)?(/tcp|/udp|/sctp)?$`)
func validPorts(ports []string) error {
  for _, p := range ports {
    if !portRe.MatchString(p) {
      return fmt.Errorf("bad port spec %q", p)
    }
  }
  return nil
}

Prevention

When it happens

Trigger: A `services.<id>.ports:` list entry in the job definition that is empty after interpolation (e.g. `${{ matrix.port }}` resolves to ''), has an invalid protocol suffix, a non-numeric port, malformed host:container:range syntax, or unparseable ranges like '80-:80'.

Common situations: Matrix-driven port lists where the matrix key is misspelled so interpolation yields an empty string; copying docker-compose range syntax ('8080-8090:80-90' is valid but typos like '8080:' are not); using UDP with ranges incorrectly; passing 'tcp://' style URLs instead of '/tcp' suffix.

Understand the failure class

Related errors


AI-assisted analysis of nektos/act@4f41128141 (2026-08-15). Data as JSON: /api/errors/ba3c37ef0893d7b5. Report an issue: GitHub.

Appendix: source

Thrown at pkg/runner/run_context.go:321

			}
			username, password, err = rc.handleServiceCredentials(ctx, spec.Credentials)
			if err != nil {
				return fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err)
			}

			interpolatedVolumes := make([]string, 0, len(spec.Volumes))
			for _, volume := range spec.Volumes {
				interpolatedVolumes = append(interpolatedVolumes, rc.ExprEval.Interpolate(ctx, volume))
			}
			serviceBinds, serviceMounts := rc.GetServiceBindsAndMounts(interpolatedVolumes)

			interpolatedPorts := make([]string, 0, len(spec.Ports))
			for _, port := range spec.Ports {
				interpolatedPorts = append(interpolatedPorts, rc.ExprEval.Interpolate(ctx, port))
			}
			exposedPorts, portBindings, err := nat.ParsePortSpecs(interpolatedPorts)
			if err != nil {
				return fmt.Errorf("failed to parse service %s ports: %w", serviceID, err)
			}

			imageName := rc.ExprEval.Interpolate(ctx, spec.Image)
			if imageName == "" {
				logger.Infof("The service '%s' will not be started because the container definition has an empty image.", serviceID)
				continue
			}

			serviceContainerName := createContainerName(rc.jobContainerName(), serviceID)
			c := container.NewContainer(&container.NewContainerInput{
				Name:           serviceContainerName,
				WorkingDir:     ext.ToContainerPath(rc.Config.Workdir),
				Image:          imageName,
				Username:       username,
				Password:       password,
				Env:            envs,
				Mounts:         serviceMounts,
				Binds:          serviceBinds,

View on GitHub (pinned to 4f41128141)