Arcane Updater is the standalone Go module behind Arcane's Docker auto-updater. It pulls newer images, recreates the containers running them, and restarts whatever depended on those containers — with Compose awareness, registry digest checks, and a path for containers that must update themselves.
It works out of the box against the local Docker environment. Persistence, notifications, and self-upgrade behavior stay adapter-driven, so a host application plugs in only the parts it owns.
Important
This module is in early development. The API is not stable and may change without notice.
Note
This module uses the experimental encoding/json/v2 package. Build and test with GOEXPERIMENT=jsonv2 set.
go get go.getarcane.app/updater@latestEverything runs through a Service. The zero Config is valid:
import "go.getarcane.app/updater"
service, err := updater.New(updater.Config{})
if err != nil {
return err
}
defer service.Close()
result, err := service.UpdateContainer(ctx, containerID, updater.Options{})To work through recorded pending updates instead of one named container:
store := updater.NewMemoryPendingStore(updater.ImageUpdateRecord{
ID: "sha256:old-image-id",
Repository: "nginx",
Tag: "1.27",
HasUpdate: true,
UpdateType: updater.UpdateTypeDigest,
})
service, err := updater.New(updater.Config{PendingStore: store})
if err != nil {
return err
}
defer service.Close()
result, err := service.ApplyPending(ctx, updater.Options{})ApplyPending applies each pending update and then restarts the containers still running the images it replaced. Options{DryRun: true} reports what would change without touching anything.
Every Config field is a seam. Leave one nil and the updater supplies a Docker-backed default — or, for the purely optional ones, skips that behavior entirely. Override only what you own:
service, err := updater.New(updater.Config{
// Replace a default.
DockerClientProvider: updater.NewDockerClientProvider(client.WithHost("tcp://docker-proxy:2375")),
PendingStore: myStore,
// Add optional behavior.
Notifier: myNotifier, // told about each successful update
EventRecorder: myEvents, // receives lifecycle events
SelfUpdater: myUpgrader, // handles the container we run in
// Restrict which containers are in scope.
LabelPolicy: updater.LabelPolicy{
IsUpdateDisabledFunc: func(l map[string]string) bool { return l["updates"] == "off" },
},
OperationTimeout: 30 * time.Second,
})LabelPolicy merges per field: overriding IsUpdateDisabledFunc above keeps the default behavior for self-update, agent, server, swarm, and stop-signal detection. See DefaultLabelPolicy.
Service.Close shuts down the Docker client New created. If you supplied your own DockerClientProvider, its lifecycle stays yours and Close is a no-op.
- Resolve a pullable image reference for each in-scope container, skipping image IDs and digest-pinned references — re-pulling those can never yield a newer image.
- Skip containers the
LabelPolicydisables, containers the host excludes viaSettingsProvider, and Swarm tasks. - Check the registry digest, so an image that has not actually changed is not needlessly recreated (
Options{Force: true}bypasses this). - Pull the target image.
- Recreate the container — through the
ProjectUpdaterfor Compose services (grouped by project, then verified to no longer run the old image), or by a direct stop/create/start with rollback for standalone containers. - Restart containers that depend on what just changed, in dependency order.
- Hand self-update targets to the
SelfUpdater, last, since updating them may stop the calling process.
Scheduling, durable persistence, and user-facing notifications are not the module's job; supply adapters for those.
| Package | Contents |
|---|---|
updater |
The Service, Config, the port interfaces, and the result types. Everything a host application normally needs. |
updater/refs |
Image reference normalization and pullability checks. |
updater/digest |
OCI digest parsing and canonicalization. |
updater/labels |
The Arcane container labels and the predicates that read them. |
updater/registry |
Registry HTTP digest and pull-rate-limit lookups. |
Implementation details live under internal/ and are not part of the public API.
v0.7.0 collapses api and types into the module root, so one import replaces two.
| Before | After |
|---|---|
api.NewService(cfg) |
updater.New(cfg), which now returns (*Service, error) |
api.NewDefaultService() |
updater.New(updater.Config{}) |
api.Service, api.Config, the port interfaces |
same names in updater |
types.Options, types.Result, types.Status, … |
same names in updater |
api.ResolvePullableImageRef |
refs.PullableImageRef, without the second return |
api.UpdateStandaloneContainer |
unexported; use UpdateContainer |
pkg/digest.RemoteResolver (GetImageDigest) |
updater.RegistryDigestResolver (ImageDigest) |
pkg/labels.DefaultLabelPolicy |
updater.DefaultLabelPolicy |
pkg/labels.GetStopSignal |
labels.StopSignal |
pkg/registry.NewRegistryHTTPClient |
registry.NewHTTPClient |
pkg/{refs,digest,labels,registry} |
{refs,digest,labels,registry} — drop the pkg/ |
pkg/{match,deps,utils}, digest Checker/RefIDCache |
moved to internal/; no longer public |
pkg/logs |
removed |
types.HistoryRecord, types.ContextHook |
removed; nothing consumed them |
Reshaped data types:
Result.StartTime/EndTimeare nowtime.Time; the pre-formattedDurationstring is now theDuration()method.ActivityIDis gone.ResourceResult.OldImages/NewImagesmaps are now theOldImage/NewImagestrings they always held.Options.TypeandOptions.ResourceIDsare gone; nothing read them.ResourceType,ResourceStatus, andUpdateTypeare now named string types. The constant values are unchanged, so persisted data stays valid.
New also fixes a trap: LabelPolicy now merges per field, so overriding one func no longer silently drops the defaults for the others.
just format all
just test
just lintArcane Updater is released under the BSD 3-Clause License.