Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
.gitignore
.dockerignore
Dockerfile
tests
Dockerfile.legacy
tests
guardian
docker-entrypoint
22 changes: 22 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,29 @@ env:
REGISTRY: ghcr.io

jobs:
go:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'

- name: Build
run: go build ./cmd/guardian

- name: Lint
uses: golangci/golangci-lint-action@v6
with:
version: latest

- name: Unit tests
run: go test -race -count=1 ./...

test:
needs: go
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
*.swo
*~
.DS_Store

# Go build output
/guardian
20 changes: 20 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
run:
timeout: 5m

linters:
enable:
- errcheck
- govet
- staticcheck
- unused
- gosimple
- ineffassign
- bodyclose
- gosec
- misspell
- gofmt

linters-settings:
gosec:
excludes:
- G304 # file inclusion via variable (needed for POST_RESTART_SCRIPT)
17 changes: 12 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

ARG ALPINE_VERSION=3.20

# ── Builder ───────────────────────────────────────────────────────────
FROM golang:1.24-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /guardian ./cmd/guardian

# ── Runtime ───────────────────────────────────────────────────────────
FROM alpine:${ALPINE_VERSION}

RUN apk add --no-cache curl jq

ENV AUTOHEAL_CONTAINER_LABEL=autoheal \
AUTOHEAL_START_PERIOD=0 \
AUTOHEAL_INTERVAL=5 \
Expand Down Expand Up @@ -42,10 +49,10 @@ ENV AUTOHEAL_CONTAINER_LABEL=autoheal \
NOTIFY_EMAIL_USER="" \
NOTIFY_EMAIL_PASS=""

COPY docker-entrypoint /
COPY --from=builder /guardian /guardian

HEALTHCHECK --interval=5s CMD pgrep -f autoheal || exit 1
HEALTHCHECK --interval=5s CMD pgrep guardian || exit 1

ENTRYPOINT ["/docker-entrypoint"]
ENTRYPOINT ["/guardian"]

CMD ["autoheal"]
51 changes: 51 additions & 0 deletions Dockerfile.legacy
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# syntax = docker/dockerfile:latest

ARG ALPINE_VERSION=3.20

FROM alpine:${ALPINE_VERSION}

RUN apk add --no-cache curl jq

ENV AUTOHEAL_CONTAINER_LABEL=autoheal \
AUTOHEAL_START_PERIOD=0 \
AUTOHEAL_INTERVAL=5 \
AUTOHEAL_DEFAULT_STOP_TIMEOUT=10 \
AUTOHEAL_ONLY_MONITOR_RUNNING=false \
AUTOHEAL_MONITOR_DEPENDENCIES=true \
AUTOHEAL_DEPENDENCY_START_DELAY=5 \
AUTOHEAL_BACKUP_LABEL="docker-volume-backup.stop-during-backup" \
AUTOHEAL_BACKUP_CONTAINER="" \
AUTOHEAL_GRACE_PERIOD=300 \
AUTOHEAL_WATCHTOWER_COOLDOWN=300 \
AUTOHEAL_WATCHTOWER_SCOPE=all \
AUTOHEAL_WATCHTOWER_EVENTS=orchestration \
DOCKER_SOCK=/var/run/docker.sock \
CURL_TIMEOUT=30 \
WEBHOOK_URL="" \
WEBHOOK_JSON_KEY="content" \
APPRISE_URL="" \
POST_RESTART_SCRIPT="" \
NOTIFY_EVENTS="actions" \
NOTIFY_GOTIFY_URL="" \
NOTIFY_GOTIFY_TOKEN="" \
NOTIFY_DISCORD_WEBHOOK="" \
NOTIFY_SLACK_WEBHOOK="" \
NOTIFY_TELEGRAM_TOKEN="" \
NOTIFY_TELEGRAM_CHAT_ID="" \
NOTIFY_PUSHOVER_TOKEN="" \
NOTIFY_PUSHOVER_USER="" \
NOTIFY_PUSHBULLET_TOKEN="" \
NOTIFY_LUNASEA_WEBHOOK="" \
NOTIFY_EMAIL_SMTP="" \
NOTIFY_EMAIL_FROM="" \
NOTIFY_EMAIL_TO="" \
NOTIFY_EMAIL_USER="" \
NOTIFY_EMAIL_PASS=""

COPY docker-entrypoint /

HEALTHCHECK --interval=5s CMD pgrep -f autoheal || exit 1

ENTRYPOINT ["/docker-entrypoint"]

CMD ["autoheal"]
31 changes: 31 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.PHONY: build test lint integration-test acceptance-test clean all

BINARY := guardian
IMAGE := docker-guardian

all: lint test build

build:
CGO_ENABLED=0 go build -o bin/$(BINARY) ./cmd/guardian

build-linux:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(BINARY)-linux-amd64 ./cmd/guardian
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/$(BINARY)-linux-arm64 ./cmd/guardian

test:
go test -race ./...

lint:
golangci-lint run

integration-test:
go test -tags=integration -race ./...

docker-build:
docker build -t $(IMAGE) .

acceptance-test: docker-build
GUARDIAN_IMAGE=$(IMAGE) bash tests/test-all.sh

clean:
rm -rf bin/
69 changes: 69 additions & 0 deletions cmd/guardian/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"context"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"

"github.com/Will-Luck/Docker-Guardian/internal/config"
"github.com/Will-Luck/Docker-Guardian/internal/docker"
"github.com/Will-Luck/Docker-Guardian/internal/guardian"
"github.com/Will-Luck/Docker-Guardian/internal/logging"
"github.com/Will-Luck/Docker-Guardian/internal/notify"
)

func main() {
// Accept "autoheal" arg for backward compat with shell version's CMD ["autoheal"]
if len(os.Args) > 1 && os.Args[1] != "autoheal" {
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
os.Exit(1)
}

cfg := config.Load()
log := logging.New(cfg.LogJSON)

// Banner: plain stdout for acceptance test compatibility
fmt.Println("Docker-Guardian (Go rewrite)")
fmt.Println("=============================================")
cfg.PrintBanner()

ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer cancel()

client, err := docker.NewClient(cfg.DockerSock)
if err != nil {
log.Error("failed to create Docker client", "error", err)
os.Exit(1)
}
defer client.Close()

dispatcher := notify.NewDispatcher(cfg, log)

// Notification banner: tests grep for "NOTIFICATIONS=.*gotify" and "NOTIFY_EVENTS=..."
fmt.Println("NOTIFICATIONS=" + dispatcher.ConfiguredServices())
resolved := cfg.ResolvedNotifyEvents()
fmt.Printf("NOTIFY_EVENTS=%s (resolved: %s)\n", cfg.NotifyEvents, strings.Join(resolved, ","))

g := guardian.New(cfg, client, dispatcher, log)

if cfg.StartPeriod > 0 {
fmt.Printf("Monitoring containers in %d second(s)\n", cfg.StartPeriod)
select {
case <-time.After(time.Duration(cfg.StartPeriod) * time.Second):
case <-ctx.Done():
return
}
}

dispatcher.Startup(fmt.Sprintf("Docker-Guardian started. Monitoring active. Services: %s",
dispatcher.ConfiguredServices()))

if err := g.Run(ctx); err != nil {
log.Error("guardian exited with error", "error", err)
os.Exit(1)
}
}
31 changes: 31 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module github.com/Will-Luck/Docker-Guardian

go 1.24.0

toolchain go1.24.13

require (
github.com/moby/moby/api v1.53.0
github.com/moby/moby/client v0.2.2
)

require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
golang.org/x/sys v0.33.0 // indirect
)
61 changes: 61 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM=
github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
Loading