feat: add custom health check commands per app
All checks were successful
Check / check (pull_request) Successful in 1m53s

Add configurable health check commands per app via a new
'healthcheck_command' field. When set, the command is passed
to Docker as a CMD-SHELL health check on the container.
When empty, the image's default health check is used.

Changes:
- Add migration 007 for healthcheck_command column on apps table
- Add HealthcheckCommand field to App model with full CRUD support
- Add buildHealthcheck() to docker client for CMD-SHELL config
- Pass health check command through CreateContainerOptions
- Add health check command input to app create/edit UI forms
- Extract optionalNullString helper to reduce handler complexity
- Update README features list

closes #81
This commit is contained in:
user
2026-03-17 02:11:08 -07:00
parent fd110e69db
commit e2522f2017
12 changed files with 342 additions and 92 deletions

View File

@@ -57,15 +57,17 @@ func (h *Handlers) HandleAppCreate() http.HandlerFunc { //nolint:funlen // valid
dockerNetwork := request.FormValue("docker_network")
ntfyTopic := request.FormValue("ntfy_topic")
slackWebhook := request.FormValue("slack_webhook")
healthcheckCommand := request.FormValue("healthcheck_command")
data := h.addGlobals(map[string]any{
"Name": name,
"RepoURL": repoURL,
"Branch": branch,
"DockerfilePath": dockerfilePath,
"DockerNetwork": dockerNetwork,
"NtfyTopic": ntfyTopic,
"SlackWebhook": slackWebhook,
"Name": name,
"RepoURL": repoURL,
"Branch": branch,
"DockerfilePath": dockerfilePath,
"DockerNetwork": dockerNetwork,
"NtfyTopic": ntfyTopic,
"SlackWebhook": slackWebhook,
"HealthcheckCommand": healthcheckCommand,
}, request)
if name == "" || repoURL == "" {
@@ -102,13 +104,14 @@ func (h *Handlers) HandleAppCreate() http.HandlerFunc { //nolint:funlen // valid
createdApp, createErr := h.appService.CreateApp(
request.Context(),
app.CreateAppInput{
Name: name,
RepoURL: repoURL,
Branch: branch,
DockerfilePath: dockerfilePath,
DockerNetwork: dockerNetwork,
NtfyTopic: ntfyTopic,
SlackWebhook: slackWebhook,
Name: name,
RepoURL: repoURL,
Branch: branch,
DockerfilePath: dockerfilePath,
DockerNetwork: dockerNetwork,
NtfyTopic: ntfyTopic,
SlackWebhook: slackWebhook,
HealthcheckCommand: healthcheckCommand,
},
)
if createErr != nil {
@@ -208,6 +211,11 @@ func (h *Handlers) HandleAppEdit() http.HandlerFunc {
}
}
// optionalNullString returns a valid NullString if the value is non-empty, or an empty NullString.
func optionalNullString(value string) sql.NullString {
return sql.NullString{String: value, Valid: value != ""}
}
// HandleAppUpdate handles app updates.
func (h *Handlers) HandleAppUpdate() http.HandlerFunc { //nolint:funlen // validation adds necessary length
tmpl := templates.GetParsed()
@@ -257,24 +265,10 @@ func (h *Handlers) HandleAppUpdate() http.HandlerFunc { //nolint:funlen // valid
application.RepoURL = request.FormValue("repo_url")
application.Branch = request.FormValue("branch")
application.DockerfilePath = request.FormValue("dockerfile_path")
if network := request.FormValue("docker_network"); network != "" {
application.DockerNetwork = sql.NullString{String: network, Valid: true}
} else {
application.DockerNetwork = sql.NullString{}
}
if ntfy := request.FormValue("ntfy_topic"); ntfy != "" {
application.NtfyTopic = sql.NullString{String: ntfy, Valid: true}
} else {
application.NtfyTopic = sql.NullString{}
}
if slack := request.FormValue("slack_webhook"); slack != "" {
application.SlackWebhook = sql.NullString{String: slack, Valid: true}
} else {
application.SlackWebhook = sql.NullString{}
}
application.DockerNetwork = optionalNullString(request.FormValue("docker_network"))
application.NtfyTopic = optionalNullString(request.FormValue("ntfy_topic"))
application.SlackWebhook = optionalNullString(request.FormValue("slack_webhook"))
application.HealthcheckCommand = optionalNullString(request.FormValue("healthcheck_command"))
saveErr := application.Save(request.Context())
if saveErr != nil {