gotify/server · error

cannot read config file %s: %w

Error message

cannot read config file %s: %w

What it means

config/migrate.Config reads the old YAML config file given on the command line so it can be converted to the new .env format. If os.ReadFile fails (missing path, permission denied, path is a directory), the OS error is wrapped as 'cannot read config file %s: %w' and returned to the migrate command.

Source

Thrown at config/migrate/migrate.go:80

	OIDC              struct {
		Enabled       *bool
		Issuer        *string
		ClientID      *string
		ClientSecret  *string
		UsernameClaim *string
		RedirectURL   *string
		AutoRegister  *bool
		Scopes        []string
	}
}

func Config(file string) (string, error) {
	if file == "" {
		return "", errors.New("migrate-config requires one argument: the path to the old config.yml")
	}
	data, err := os.ReadFile(file)
	if err != nil {
		return "", fmt.Errorf("cannot read config file %s: %w", file, err)
	}

	var migrated oldConfig
	if err := yaml.Unmarshal(data, &migrated); err != nil {
		return "", fmt.Errorf("cannot parse config file %s: %w", file, err)
	}

	content, err := godotenv.Marshal(buildEnv(migrated))
	if err != nil {
		return "", fmt.Errorf("cannot render config: %w", err)
	}

	return content, nil
}

func buildEnv(c oldConfig) map[string]string {
	out := map[string]string{}
	str := func(key string, value *string) {

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Check the path exists and is a regular file: ls -l <path>; fix typos or pass an absolute path.
  2. Fix permissions so the process user can read the file (chmod/chown or run as the right user).
  3. In containers, verify the old config is volume-mounted into the container at the given path.
  4. Handle the returned error in runMigrate by printing a clear message with the wrapped os error (e.g. os.IsNotExist) to guide the operator.

Example fix

// before
migrated, err := migrate.Config("config.old.yml") // relative path, wrong cwd
// after
migrated, err := migrate.Config("/etc/app/config.old.yml") // absolute, verified path
Defensive patterns

Strategy: validation

Validate before calling

func assertReadable(path string) error {
    info, err := os.Stat(path)
    if err != nil {
        return fmt.Errorf("old config %s: %w", path, err)
    }
    if info.IsDir() {
        return fmt.Errorf("%s is a directory, want a file", path)
    }
    f, err := os.Open(path)
    if err != nil { return err }
    return f.Close()
}

Try / catch

out, err := migrate.Config(oldPath)
if err != nil {
    if errors.Is(err, fs.ErrNotExist) {
        return fmt.Errorf("old config not found at %s", oldPath)
    }
    if errors.Is(err, fs.ErrPermission) {
        return fmt.Errorf("cannot read %s: check permissions", oldPath)
    }
    return err
}

Prevention

When it happens

Trigger: Running the migrate-config command (run/runMigrate) with a path that does not exist, a path the process cannot read (permissions), or a directory instead of a file; also empty-file argument is rejected earlier with a different error.

Common situations: Typo in the config.yml path, running the migration from a different working directory with a relative path, Docker container not mounting the old config, or file owned by another user after deployment.

Understand the failure class

Background: "Config file not found": what it means and how to fix it in docker-sync, Maven, Vagrant, Turborepo and other tools — this error's family across 60 libraries.

Related errors


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/bcccab1bf5cfea60. Report an issue: GitHub.