Release / Build and publish image (push) Failing after 10m2s
A container cannot replace its own image from the inside, so "update" is really "ask whoever owns the container to recreate it" — Yggdrasil pulls `:latest` on the way, which is what makes a restart an update at all. This removes the second browser tab, not the panel's authority: the panel still does the work and still records who asked. Three settings, needed together, and the page names whichever are missing — an operator reading "not configured" should not have to go and find out which of three. The token is never sent to a browser; the page is told only whether it is set. Answers 202 rather than waiting. The panel stops this container as part of answering, so the reply may never arrive, and a request cut off mid-flight is the successful case wearing a failure's clothes. The interface believes neither answer and polls /healthz until it responds. Administrators only, and sessions only. A leaked API token that could restart the server is a denial of service in one request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
151 lines
5.4 KiB
YAML
151 lines
5.4 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: 2
|
|
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"
|
|
|
|
# Off by default because it is an outbound request nobody asked for. It sends
|
|
# no identifier and reads one public URL — GitHub's releases API — but that is
|
|
# a choice to make rather than a default to inherit.
|
|
- key: VERDANDE_UPDATE_CHECK
|
|
name: "Look for new releases (asks GitHub once a day; sends nothing about you)"
|
|
type: bool
|
|
default: false
|
|
|
|
# Restarting from verdande's own settings, which is also how it picks up a new
|
|
# version: a container cannot replace its own image, so the panel recreates it
|
|
# and pulls :latest on the way. All three are needed together; the page names
|
|
# whichever are missing.
|
|
- key: VERDANDE_PANEL_URL
|
|
name: "Panel address, so verdande can ask to be restarted from its own settings"
|
|
type: string
|
|
default: ""
|
|
- key: VERDANDE_PANEL_TOKEN
|
|
name: "Panel API token — from an account with control of this server"
|
|
type: string
|
|
secret: true
|
|
default: ""
|
|
- key: VERDANDE_PANEL_SERVER_ID
|
|
name: "This server's id in the panel (the one in its URL)"
|
|
type: string
|
|
default: ""
|
|
|
|
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":"([^"]+)"'
|