Mangrullo Update Detection Algorithm
Overview
Mangrullo detects when Docker container images need updates and decides which image reference an update should move to. The algorithm separates moving tags (which are compared by digest) from versioned tags (which are compared by semantic version), and detection is paired with execution: the same logic that detects an update also computes the target image reference to pull and recreate the container with.
Update Decision Flow
1. Tag Type Detection
The algorithm first determines whether the tag is a moving tag:
def needs_update?(container : ContainerInfo, allow_major_upgrade : Bool = false) : Bool
# Moving tags (latest, single-component like postgres:16, digest pins,
# or non-version tags) are compared by digest, not by version
if moving_tag?(container)
status = get_update_status(container)
return status[:needs_pull] || status[:needs_restart]
end
# For versioned tags, find available updates based on version
current_version = extract_version_from_image(container.image)
return false unless current_version
target_version = find_target_update_version(container.image, current_version, allow_major_upgrade)
target_version != nil
end
# A tag that tracks a moving ref rather than a fixed version
private def moving_tag?(container : ContainerInfo) : Bool
return true if container.image.includes?("sha256:")
tag = ImageNameParser.get_tag(container.image)
!tag.includes?(".")
end
Moving tags are: latest, single-component tags (postgres:16, redis:7),
digest pins (image@sha256:…), and non-version tags (stable, alpine). All
of them are compared by digest.
1.1 Enhanced Update Status Detection
For moving tags, the algorithm compares local and remote digests:
def get_update_status(container : ContainerInfo) :
NamedTuple(needs_pull: Bool, needs_restart: Bool,
local_digest: String?, remote_digest: String?)
local_digest = get_local_image_digest(container.image)
remote_digest = get_remote_image_digest(container.image)
{
needs_pull: local_digest != remote_digest,
needs_restart: local_digest == remote_digest && container.image.includes?("latest")
}
end
2. Moving Tag Handling
For moving tags, the update is a re-pull of the same reference:
def image_has_update?(image_name : String) : Bool
local_digest = get_local_image_digest(image_name)
return false unless local_digest
remote_digest = get_remote_image_digest(image_name)
return false unless remote_digest
local_digest != remote_digest
end
Benefits:
- Single API call to get remote manifest digest
- No need to parse hundreds or thousands of tags
- Fast and efficient
- Rolling tags (
postgres:16) keep following their major instead of being "upgraded" to a different tag
3. Versioned Tag Handling
For images with semantic version tags (e.g., nginx:1.2.3):
3.1 Version Extraction
def extract_version_from_image(image_name : String) : Version?
# Skip SHA256 digests (they are image IDs, not versioned images)
return nil if image_name.starts_with?("sha256:")
# Extract tag from image name (format: name:tag or name)
parts = image_name.split(":")
tag = parts.size > 1 ? parts.last : "latest"
Version.parse(tag)
end
Single-component tags parse as major-only versions (16 → 16.0.0), but
since they are moving tags they never reach the version path — they are
handled by digest comparison instead.
3.2 Target Tag Discovery
find_target_update_version delegates to find_target_update_tag, which
returns the actual registry tag string for the target (the tag must exist
upstream to be pullable — version objects alone would lose spellings like a
v prefix):
def find_target_update_tag(image_name : String, current_version : Version,
allow_major_upgrade : Bool) : String?
candidates = registry_version_tags(image_name).select do |pair|
pair[:version] > current_version &&
(allow_major_upgrade || pair[:version].major == current_version.major)
end
best = candidates.max_by? { |pair| pair[:version] }
best.try &.[:tag]
end
3.3 Version Collection
def registry_version_tags(image_name : String) : Array(NamedTuple(tag: String, version: Version))
# Single API call to get all tags
response = fetch_registry_tags(registry_host, repository_path)
json = JSON.parse(response.body)
tags = json["tags"].as_a.map(&.as_s)
tags.compact_map { |tag|
version = Version.parse(tag)
version ? {tag: tag, version: version} : nil
}
end
From Detection to Update
Detection alone is not enough — the update must know which reference to
pull. ImageChecker#target_image_for_update mirrors the detection logic:
def target_image_for_update(container : ContainerInfo,
allow_major_upgrade : Bool = false) : String?
return container.image if moving_tag?(container)
current_version = extract_version_from_image(container.image)
return container.image unless current_version
target_tag = find_target_update_tag(container.image, current_version, allow_major_upgrade)
return container.image unless target_tag
repository = ImageNameParser.parse(container.image)[:repository]
ImageNameParser.format_with_tag(repository, target_tag)
end
UpdateManager#update_container then:
- Pulls
repository:target_tag(name and tag passed separately to the Docker API so the registry port cannot be mistaken for a tag) - Recreates the container with the pulled image — digest-pinned
(
repo@sha256:…) when the local digest matches the remote, otherwise the target reference - Rolls back safely if recreation fails: the old container is renamed to a backup and restored if the replacement cannot be created or started
Data Sources
Local Information
- Docker Daemon via Docr API
- Container running state and image references
- Local image digests and metadata
Remote Information
- Registry APIs with authentication support:
- Docker Hub:
registry-1.docker.io - GitHub Container Registry:
ghcr.io - Other registries: Dynamic detection
Authentication:
- JWT token authentication with caching
- Support for both Docker Hub and ghcr.io token endpoints
- Graceful fallback to unauthenticated requests
Message Generation
The algorithm generates clean, user-friendly messages:
Latest Tags
"Update available for ghcr.io/home-assistant/home-assistant:latest (current: latest)"
Versioned Tags
"Version update available: 1.2.0 -> 1.4.5"
Key Methods and Their Roles
Core Methods
needs_update?(container, allow_major_upgrade)
Main entry point - routes to appropriate detection strategy based on tag type
moving_tag?(container)
Decides whether a tag tracks a moving ref (digest comparison) or a fixed version (semver comparison)
target_image_for_update(container, allow_major_upgrade)
Computes the image reference an update should pull and recreate with: the newer tag for versioned images, the same reference for moving tags
find_target_update_tag(image_name, current_version, allow_major_upgrade)
Returns the actual registry tag string of the best available update
find_target_update_version(image_name, current_version, allow_major_upgrade)
Parses the target tag into a Version (a thin wrapper over find_target_update_tag)
registry_version_tags(image_name)
Performs a single API call to get all registry tags paired with their parsed versions
extract_version_from_image(image_name)
Parses semantic version from image tag
Authentication Methods
get_registry_token(registry_host, repository_path)
Fetches JWT tokens for registry authentication with caching
create_authenticated_client(registry_host, repository_path)
Creates HTTP client with proper authorization headers
Registry Support
Supported Registries
- Docker Hub (
registry-1.docker.io) - GitHub Container Registry (
ghcr.io) - Generic registries with standard API v2
Registry Detection
# Automatic registry host detection: a first path segment containing "." or
# ":" marks a custom registry host (e.g. ghcr.io/user/image, localhost:5000/app)
first_slash = base_name.index('/')
prefix = first_slash ? base_name[0...first_slash] : nil
if prefix && (prefix.includes?(".") || prefix.includes?(":"))
registry_host = prefix
repository_path = base_name[(prefix.size + 1)..]
elsif prefix
# Docker Hub namespace/image (e.g. user/nginx)
registry_host = "registry-1.docker.io"
repository_path = base_name
else
# Simple image name, assume Docker Hub library
registry_host = "registry-1.docker.io"
repository_path = "library/#{base_name}"
end
Special Mappings
lscr.io→ redirects toghcr.io/linuxserver/(with double-prefix prevention)
The algorithm handles the lscr.io to ghcr.io mapping with special logic to avoid double "linuxserver" prefixes:
# Handle special registry mappings
if registry_host == "lscr.io"
# lscr.io is a vanity URL that redirects to ghcr.io
# Images are actually hosted at ghcr.io/linuxserver
registry_host = "ghcr.io"
# Don't double-prepend linuxserver if it's already there
unless repository_path.starts_with?("linuxserver/")
repository_path = "linuxserver/#{repository_path}"
end
end
Version Parsing Support
Supported Formats
- Standard semver:
1.2.3 - Two-component versions:
1.2(patch defaults to 0) - Single-component tags:
16(parsed as16.0.0, but treated as a moving tag) - Prereleases:
1.2.3-alpha,1.2.3-beta.1 - Build metadata:
1.2.3+build.123(ignored in comparison) - 'v' prefix:
v1.2.3
Digest-Based Handling
latesttags (handled by digest comparison)- SHA256 digest pins (handled by digest comparison)
- Non-semantic version tags like
stable(handled by digest comparison) - Rolling single-number tags like
postgres:16(handled by digest comparison)
Major Upgrade Control
The allow_major_upgrade parameter controls upgrade behavior:
true: Any version increase (1.2.3 → 2.0.0)false: Only minor/patch updates (1.2.3 → 1.3.0, NOT 1.2.3 → 2.0.0)
Performance Characteristics
API Efficiency
- Latest tags: 2 API calls (local digest + remote manifest)
- Versioned tags: 1 API call (tags list) + local version parsing
- No individual tag checking: Eliminated the N+1 query problem
Authentication Caching
- JWT tokens cached with 4-minute expiration
- Reduces authentication overhead for multiple checks
Network Optimization
- Single HTTP request per image for versioned tags
- Proper error handling and graceful degradation
- Minimal external dependencies
Error Handling
Graceful Degradation
- Network failures → return false (no update detected)
- API errors → log debug information and continue
- Authentication failures → fall back to unauthenticated requests
- Parsing failures → skip problematic containers
Resilience Features
- Rescue blocks around all external API calls
- Container-level error isolation
- Comprehensive debug logging
- Authentication token caching
Security Considerations
Authentication
- JWT tokens from official registry endpoints
- Token caching with proper expiration
- Credentials from the user's
~/.docker/config.jsonare used when an entry matches the registry (including the legacyhttps://index.docker.io/v1/spelling), enabling private images and avoiding Docker Hub anonymous rate limits;credsStore-delegated entries are skipped - No hardcoded credentials
Registry Communication
- HTTPS-only communication
- Standard Docker Registry API v2
- Support for private registries with authentication
Future Improvements
Enhanced Registry Support
- Additional registry types (GitLab, ECR, GCR)
- Registry-specific configuration
- Custom authentication methods
Performance Optimizations
- Parallel container checking
- Response caching for repeated checks
- Batch operations for multiple images
User Experience
- More detailed update information
- Change log integration
- Security vulnerability reporting
Configuration Options
- Per-image update policies
- Custom version filtering rules
- Registry-specific settings