The HTTP server serves /healthz, the API namespace and the PWA, with the frontend embedded behind a build tag so a clean checkout compiles without a Node toolchain. /healthz reads the database rather than just answering: a process that cannot reach its own data is not healthy, and a 200 there would keep a broken container in rotation. The image is distroless and static — the SQLite driver is pure Go, so CGO stays off. The Rune manifest uses keep_entrypoint because there is no shell in the image to run a startup command with, and its log watchers match the JSON log format the container actually emits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
4.3 KiB
YAML
125 lines
4.3 KiB
YAML
gameskill:
|
|
id: verdande
|
|
name: "verdande"
|
|
category: "Apps"
|
|
description: >
|
|
Self-hosted tasks and projects, shared with other people. Natural-language
|
|
quick add in Danish and English, sub-tasks, labels, saved filters, board and
|
|
calendar views, recurring tasks, reminders, comments and attachments. One
|
|
binary with SQLite inside it — no database to set up. Set BASE_URL to the
|
|
address you reach it on, point the SMTP settings at your mail server for
|
|
invites and reminders, then start it and open the URL to create the first
|
|
admin account.
|
|
author: "kristianwind"
|
|
version: 1
|
|
icon: "app"
|
|
|
|
# The image is distroless — no shell — so keep_entrypoint runs the binary the
|
|
# image already declares as its ENTRYPOINT, and startup.exec passes it argv
|
|
# directly rather than through /bin/sh, which does not exist in there.
|
|
docker:
|
|
image: "ghcr.io/kristianwind/verdande:latest"
|
|
data_path: /data
|
|
keep_entrypoint: true
|
|
|
|
variables:
|
|
# verdande cannot work out its own public address: the host port is not chosen
|
|
# until the server is created. {{PUBLIC_URL}} is what the panel knows and this
|
|
# is what invite links, password resets and ICS feeds are built from — get it
|
|
# wrong and the links in your email point somewhere that does not answer.
|
|
- key: VERDANDE_BASE_URL
|
|
name: "Public address (used in invite and reset links)"
|
|
type: string
|
|
default: "{{PUBLIC_URL}}"
|
|
required: true
|
|
|
|
- key: VERDANDE_SMTP_HOST
|
|
name: "SMTP host (blank = show invite links in the app instead of mailing them)"
|
|
type: string
|
|
default: ""
|
|
- key: VERDANDE_SMTP_PORT
|
|
name: "SMTP port"
|
|
type: int
|
|
default: 587
|
|
min: 1
|
|
max: 65535
|
|
- key: VERDANDE_SMTP_USER
|
|
name: "SMTP username"
|
|
type: string
|
|
default: ""
|
|
- key: VERDANDE_SMTP_PASS
|
|
name: "SMTP password"
|
|
type: string
|
|
default: ""
|
|
secret: true
|
|
- key: VERDANDE_SMTP_FROM
|
|
name: "Sender address"
|
|
type: string
|
|
default: ""
|
|
pattern: '^$|^[^@\s]+@[^@\s]+\.[^@\s]+$'
|
|
hint: "must be an email address like verdande@example.dk, or blank to leave it unset"
|
|
- key: VERDANDE_SMTP_STARTTLS
|
|
name: "Use STARTTLS (turn off only for port 465, which is TLS from the start)"
|
|
type: bool
|
|
default: true
|
|
|
|
- key: VERDANDE_SESSION_TTL
|
|
name: "How long a login lasts (Go duration, e.g. 720h for 30 days)"
|
|
type: string
|
|
default: "720h"
|
|
pattern: '^[0-9]+(ns|us|ms|s|m|h)$'
|
|
hint: "must be a duration like 720h, 30m or 45s"
|
|
|
|
install:
|
|
# The image has no shell, so the install step borrows one. It only has to make
|
|
# the directory layout exist; verdande creates and migrates its own database on
|
|
# first start.
|
|
image: "busybox:latest"
|
|
script: |
|
|
set -eu
|
|
mkdir -p /data/files /data/backups
|
|
echo "verdande data directory ready at /data"
|
|
|
|
startup:
|
|
# No command: with keep_entrypoint the image's own ENTRYPOINT (/verdande) runs,
|
|
# and everything is configured through the environment above.
|
|
done_regex: '"msg":"listening"'
|
|
|
|
ports:
|
|
- { name: web, default: 8080, protocol: tcp }
|
|
|
|
# Everything worth keeping is under /data. The -wal and -shm files are included
|
|
# deliberately: a WAL-mode SQLite database copied without them can be missing the
|
|
# most recent writes.
|
|
backup:
|
|
include: ["."]
|
|
|
|
# The container logs JSON — VERDANDE_DEV is off in production — so these patterns
|
|
# match the JSON form, not the human-readable one seen when developing locally.
|
|
watchers:
|
|
- name: "Database errors"
|
|
pattern: 'database is locked|disk I/O error|database disk image is malformed'
|
|
threshold: 1
|
|
window_secs: 300
|
|
action: kvasir
|
|
- name: "Failed logins"
|
|
pattern: '"path":"/api/v1/auth/login".*"status":401'
|
|
threshold: 10
|
|
window_secs: 300
|
|
action: notify
|
|
- name: "Server errors"
|
|
pattern: '"status":5[0-9][0-9]'
|
|
threshold: 5
|
|
window_secs: 300
|
|
action: kvasir
|
|
|
|
events:
|
|
# Capture group 1 is the client address, which the panel rolls up per source —
|
|
# so a brute-force run shows up as one row per address per hour.
|
|
- key: login_failed
|
|
label: "Failed login"
|
|
match: '"path":"/api/v1/auth/login".*"status":401.*"remote":"([^"]+)"'
|
|
- key: http_5xx
|
|
label: "HTTP 5xx"
|
|
match: '"status":5[0-9][0-9].*"remote":"([^"]+)"'
|