Initial commit
CI / test (push) Canceled after 0s

This commit is contained in:
2026-09-12 22:22:17 +02:00
commit 904d14b64c
314 changed files with 31884 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
[Unit]
# Description is a human-readable name for the service.
Description=Gardomatic API service
# Wait until PostgreSQL is running and the network is "up" before starting the service.
After=postgresql.service
After=network-online.target
Wants=network-online.target
# Configure service start rate limiting. If the service is (re)started more than 5 times
# in 600 seconds then don't permit it to start anymore.
StartLimitIntervalSec=600
StartLimitBurst=5
[Service]
# Execute the API binary as the gardomatic user, loading its dedicated environment file
# and using its writable state directory.
Type=exec
User=gardomatic
Group=gardomatic
EnvironmentFile=/etc/gardomatic/gardomatic.env
WorkingDirectory=/var/lib/gardomatic
ExecStart=/usr/local/bin/gardomatic-api
# Automatically restart the service after a 5-second wait if it exits with a non-zero
# exit code. If it restarts more than 5 times in 600 seconds, then the rate limit we
# configured above will be hit and it won't be restarted anymore.
Restart=on-failure
RestartSec=5
[Install]
# Start the service automatically at boot time (the 'multi-user.target' describes a boot
# state when the system will accept logins).
WantedBy=multi-user.target
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
set -Eeuo pipefail
# Installs artifacts uploaded by `make production/deploy`. Run as root or as an
# administrator with passwordless sudo.
if [[ $(id -u) -ne 0 ]]; then
exec sudo -n "$0" "$@"
fi
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
APPLICATION_DIR=/opt/gardomatic
STATE_DIR=/var/lib/gardomatic
ENV_FILE=/etc/gardomatic/gardomatic.env
required_files=(
api
web
cli
api.service
web.service
create-admin.sh
)
for file in "${required_files[@]}"; do
[[ -f "$SCRIPT_DIR/$file" ]] || {
printf 'Missing deployment artifact: %s\n' "$file" >&2
exit 1
}
done
[[ -d "$SCRIPT_DIR/migrations" ]] || {
printf 'Missing deployment artifact: migrations\n' >&2
exit 1
}
[[ -f "$ENV_FILE" ]] || {
printf 'Missing runtime configuration: %s; run production/provision first.\n' "$ENV_FILE" >&2
exit 1
}
id gardomatic >/dev/null 2>&1 || {
printf 'Missing service user gardomatic; run production/provision first.\n' >&2
exit 1
}
install -d -m 0755 -o root -g root "$APPLICATION_DIR"
install -d -m 0750 -o gardomatic -g gardomatic "$STATE_DIR"
install -m 0755 -o root -g root "$SCRIPT_DIR/api" /usr/local/bin/gardomatic-api
install -m 0755 -o root -g root "$SCRIPT_DIR/web" /usr/local/bin/gardomatic-web
install -m 0755 -o root -g root "$SCRIPT_DIR/cli" /usr/local/bin/gardomatic-cli
install -d -m 0755 -o root -g root "$APPLICATION_DIR/migrations"
rsync --archive --delete "$SCRIPT_DIR/migrations/" "$APPLICATION_DIR/migrations/"
install -m 0644 -o root -g root "$SCRIPT_DIR/api.service" /etc/systemd/system/api.service
install -m 0644 -o root -g root "$SCRIPT_DIR/web.service" /etc/systemd/system/web.service
install -m 0755 -o root -g root "$SCRIPT_DIR/create-admin.sh" /usr/local/sbin/gardomatic-create-admin
# provision-server.sh writes a file compatible with systemd and Bash. Loading it
# here keeps the database DSN out of command-line arguments and process listings.
set -a
# shellcheck disable=SC1091
source "$ENV_FILE"
set +a
migrate -path "$APPLICATION_DIR/migrations" -database "$GARDOMATIC_DB_DSN" up
systemctl daemon-reload
systemctl enable api web
systemctl restart api web
printf 'Gardomatic deployment complete.\n'
+17
View File
@@ -0,0 +1,17 @@
[Unit]
Description=Gardomatic web service
After=network-online.target api.service
Wants=network-online.target
[Service]
Type=exec
User=gardomatic
Group=gardomatic
EnvironmentFile=/etc/gardomatic/gardomatic.env
WorkingDirectory=/var/lib/gardomatic
ExecStart=/usr/local/bin/gardomatic-web
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
+84
View File
@@ -0,0 +1,84 @@
# Copy this file to .env next to provision-server.sh and restrict it to the
# administrator. The file is sourced as Bash configuration and must be trusted.
# cp .env.example .env
# chmod 600 .env
# Server provisioning ---------------------------------------------------------
# IANA timezone accepted by timedatectl. List available values with:
# timedatectl list-timezones
GARDOMATIC_SETUP_TIMEZONE='Europe/Berlin'
# golang-migrate release number without a leading "v". The script supports
# Linux AMD64 and ARM64 release archives.
GARDOMATIC_MIGRATE_VERSION='4.19.1'
# Allowed exactly: true or false. false leaves rebooting to the administrator.
GARDOMATIC_REBOOT='false'
# PostgreSQL provisioning -----------------------------------------------------
# Database and role names. Allowed: lowercase letters, digits and underscores;
# the first character must be a letter or underscore.
GARDOMATIC_DB_NAME='gardomatic'
GARDOMATIC_DB_USER='gardomatic'
# Required non-empty password used to create the PostgreSQL role. This separate
# value is not installed in the service environment, but the DSN below is also a
# secret because it normally contains the same password.
GARDOMATIC_DB_PASSWORD=''
# Application runtime ---------------------------------------------------------
# Allowed: development, test, production. Keep production on public servers.
GARDOMATIC_ENV='production'
# Required PostgreSQL connection string. Percent-encode URI-reserved characters
# in the password. Example form:
# postgres://gardomatic:ENCODED_PASSWORD@localhost:5432/gardomatic?sslmode=disable
GARDOMATIC_DB_DSN=''
# Connection-pool limits. Open must be at least 1; idle must be between 0 and open.
GARDOMATIC_DB_MAX_OPEN_CONNS='25'
GARDOMATIC_DB_MAX_IDLE_CONNS='25'
# Go duration, for example 30s, 15m or 1h30m.
GARDOMATIC_DB_MAX_IDLE_TIME='15m'
# Listener hosts. Use 127.0.0.1 behind a reverse proxy; an empty value listens on
# all available interfaces. Ports must be integers from 1 to 65535.
GARDOMATIC_API_HOST='127.0.0.1'
GARDOMATIC_API_PORT='4000'
GARDOMATIC_WEB_HOST='127.0.0.1'
GARDOMATIC_WEB_PORT='4040'
# Absolute internal API URL used by the web process.
GARDOMATIC_API_BASE_URL='http://127.0.0.1:4000'
# Required absolute public HTTP(S) URL. Use HTTPS and normally no path in production.
GARDOMATIC_WEB_BASE_URL=''
# Non-empty cookie name shared by API and web.
GARDOMATIC_SESSION_COOKIE_NAME='gardomatic_session'
# Go durations, for example 30m, 12h or 168h. Use positive values.
GARDOMATIC_SESSION_LIFETIME='12h'
GARDOMATIC_SESSION_IDLE_TIMEOUT='30m'
# Boolean. Accepted true values: 1, t, T, TRUE, true, True. Accepted false
# values: 0, f, F, FALSE, false, False. Must be true in production.
GARDOMATIC_COOKIE_SECURE='true'
# Boolean with the same accepted forms as GARDOMATIC_COOKIE_SECURE.
GARDOMATIC_RATE_LIMIT_ENABLED='true'
# Positive requests per second; decimal values such as 0.5 are accepted.
GARDOMATIC_RATE_LIMIT_RPS='10'
# Positive integer defining the permitted request burst.
GARDOMATIC_RATE_LIMIT_BURST='40'
# Comma-separated absolute URLs; every entry requires a scheme and host. Empty
# allows no cross-origin browser access. Use origins without paths, for example:
# https://garden.example.com,https://admin.example.com
GARDOMATIC_CORS_TRUSTED_ORIGINS=''
# Delivery mode. Allowed exactly: file or smtp. Production normally uses smtp.
GARDOMATIC_SMTP_MODE='smtp'
# Required in smtp mode: resolvable SMTP hostname and TCP port.
GARDOMATIC_SMTP_HOST=''
GARDOMATIC_SMTP_PORT='587'
# Required non-empty credentials in smtp mode.
GARDOMATIC_SMTP_USERNAME=''
GARDOMATIC_SMTP_PASSWORD=''
# Required sender mailbox in smtp mode, for example gardomatic@example.com.
GARDOMATIC_SMTP_SENDER=''
# Writable absolute path required only when GARDOMATIC_SMTP_MODE=file.
GARDOMATIC_SMTP_FILE_PATH='/tmp/gardomatic-mails.log'
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
set -Eeuo pipefail
# Creates an active application administrator and prints a generated password
# exactly once. Installed during deployment as gardomatic-create-admin.
if [[ $(id -u) -ne 0 ]]; then
exec sudo -n "$0" "$@"
fi
ENV_FILE=/etc/gardomatic/gardomatic.env
CLI=/usr/local/bin/gardomatic-cli
[[ -f "$ENV_FILE" ]] || {
printf 'Missing runtime configuration: %s\n' "$ENV_FILE" >&2
exit 1
}
[[ -x "$CLI" ]] || {
printf 'Missing Gardomatic CLI: %s\n' "$CLI" >&2
exit 1
}
read -r -p 'Administrator name: ' admin_name
read -r -p 'Administrator email: ' admin_email
[[ -n "$admin_name" && -n "$admin_email" ]] || {
printf 'Name and email must not be empty.\n' >&2
exit 1
}
set -a
# shellcheck disable=SC1091
source "$ENV_FILE"
set +a
"$CLI" --yes users create \
--name "$admin_name" \
--email "$admin_email" \
--role application:admin \
--active \
--generate-password
+242
View File
@@ -0,0 +1,242 @@
#!/bin/bash
set -Eeuo pipefail
# Run this script as root or as an administrator with passwordless sudo.
# Configuration is read from a trusted shell-style .env file or standard input.
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
ENV_FILE="${SCRIPT_DIR}/.env"
usage() {
cat <<'EOF'
Usage: ./provision-server.sh [--env-file PATH|-]
Provision the Gardomatic server and install its runtime configuration.
The default configuration file is .env next to this script. Use - to stream the
configuration over SSH without storing the source file on the server.
EOF
}
die() {
printf 'Error: %s\n' "$*" >&2
exit 1
}
while (($# > 0)); do
case "$1" in
--env-file)
(($# >= 2)) || die "--env-file requires a path"
ENV_FILE=$2
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown argument: $1"
;;
esac
done
if [[ "$ENV_FILE" == - ]]; then
ENV_SOURCE=/dev/stdin
ENV_LABEL='standard input'
else
[[ -f "$ENV_FILE" ]] || die "configuration file not found: $ENV_FILE"
env_permissions=$(stat -c '%a' "$ENV_FILE")
(( (8#$env_permissions & 077) == 0 )) || die "$ENV_FILE must not be readable or writable by group or others (run: chmod 600 '$ENV_FILE')"
ENV_SOURCE=$ENV_FILE
ENV_LABEL=$ENV_FILE
fi
# The file is deliberately sourced so quoted values work. It must therefore be
# controlled by the administrator running this script.
set -a
# shellcheck disable=SC1090
source "$ENV_SOURCE"
set +a
require_variable() {
local name=$1
[[ -n "${!name:-}" ]] || die "required variable $name is missing in $ENV_LABEL"
}
require_identifier() {
local name=$1
local value=${!name:-}
[[ "$value" =~ ^[a-z_][a-z0-9_]*$ ]] || die "$name must be a lowercase PostgreSQL identifier"
}
require_variable GARDOMATIC_DB_PASSWORD
require_variable GARDOMATIC_DB_DSN
require_variable GARDOMATIC_WEB_BASE_URL
require_identifier GARDOMATIC_DB_NAME
require_identifier GARDOMATIC_DB_USER
case "${GARDOMATIC_SMTP_MODE:-file}" in
smtp)
require_variable GARDOMATIC_SMTP_HOST
require_variable GARDOMATIC_SMTP_USERNAME
require_variable GARDOMATIC_SMTP_PASSWORD
require_variable GARDOMATIC_SMTP_SENDER
;;
file)
require_variable GARDOMATIC_SMTP_FILE_PATH
;;
*)
die "GARDOMATIC_SMTP_MODE must be smtp or file"
;;
esac
GARDOMATIC_SETUP_TIMEZONE=${GARDOMATIC_SETUP_TIMEZONE:-Europe/Berlin}
GARDOMATIC_MIGRATE_VERSION=${GARDOMATIC_MIGRATE_VERSION:-4.19.1}
GARDOMATIC_REBOOT=${GARDOMATIC_REBOOT:-false}
[[ "$GARDOMATIC_REBOOT" == true || "$GARDOMATIC_REBOOT" == false ]] || die "GARDOMATIC_REBOOT must be true or false"
readonly GARDOMATIC_SERVICE_USER=gardomatic
run_as_root() {
if [[ $(id -u) -eq 0 ]]; then
"$@"
else
sudo -n "$@"
fi
}
run_as_postgres() {
if [[ $(id -u) -eq 0 ]]; then
runuser -u postgres -- "$@"
else
sudo -n -u postgres "$@"
fi
}
if [[ $(id -u) -ne 0 ]]; then
sudo -n true || die "the SSH administrator needs passwordless sudo"
fi
# Force consistent command output while locales are being installed.
export LC_ALL=en_US.UTF-8
run_as_root apt update
run_as_root apt install --yes software-properties-common locales curl rsync ufw
run_as_root add-apt-repository --yes universe
run_as_root apt update
run_as_root timedatectl set-timezone "$GARDOMATIC_SETUP_TIMEZONE"
run_as_root apt --yes install locales-all
# Gardomatic runs under a dedicated service account. It has no login shell, SSH
# keys, password or sudo privileges; deployments continue through the configured
# server administrator account.
if id "$GARDOMATIC_SERVICE_USER" >/dev/null 2>&1; then
if ! getent group "$GARDOMATIC_SERVICE_USER" >/dev/null 2>&1; then
run_as_root groupadd --system "$GARDOMATIC_SERVICE_USER"
fi
run_as_root usermod --lock --shell /usr/sbin/nologin "$GARDOMATIC_SERVICE_USER"
run_as_root usermod --gid "$GARDOMATIC_SERVICE_USER" "$GARDOMATIC_SERVICE_USER"
run_as_root deluser --quiet "$GARDOMATIC_SERVICE_USER" sudo >/dev/null 2>&1 || true
else
run_as_root useradd --system --user-group --create-home --home-dir /var/lib/gardomatic \
--shell /usr/sbin/nologin "$GARDOMATIC_SERVICE_USER"
fi
run_as_root install -d -m 0750 -o gardomatic -g gardomatic /var/lib/gardomatic
run_as_root ufw allow 22
run_as_root ufw allow 4040/tcp
run_as_root ufw --force enable
run_as_root apt --yes install fail2ban
# Install the migrate CLI for the host architecture.
case "$(uname -m)" in
x86_64|amd64) migrate_arch=amd64 ;;
aarch64|arm64) migrate_arch=arm64 ;;
*) die "unsupported architecture for migrate: $(uname -m)" ;;
esac
download_dir=$(mktemp -d)
runtime_env=$(mktemp)
cleanup() {
rm -rf -- "$download_dir"
rm -f -- "$runtime_env"
}
trap cleanup EXIT
migrate_archive="$download_dir/migrate.tar.gz"
curl --fail --location --show-error \
"https://github.com/golang-migrate/migrate/releases/download/v${GARDOMATIC_MIGRATE_VERSION}/migrate.linux-${migrate_arch}.tar.gz" \
--output "$migrate_archive"
tar -xzf "$migrate_archive" -C "$download_dir"
run_as_root install -m 0755 "$download_dir/migrate" /usr/local/bin/migrate
run_as_root apt --yes install postgresql postgresql-contrib
if ! run_as_postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname = '${GARDOMATIC_DB_NAME}'" | grep -qx 1; then
run_as_postgres createdb "$GARDOMATIC_DB_NAME"
fi
run_as_postgres psql -d "$GARDOMATIC_DB_NAME" -c "CREATE EXTENSION IF NOT EXISTS citext"
run_as_postgres psql -d "$GARDOMATIC_DB_NAME" -c "CREATE EXTENSION IF NOT EXISTS pgcrypto"
if ! run_as_postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname = '${GARDOMATIC_DB_USER}'" | grep -qx 1; then
printf '%s\n' "CREATE ROLE \"${GARDOMATIC_DB_USER}\" WITH LOGIN PASSWORD :'db_password';" | \
run_as_postgres psql -v db_password="$GARDOMATIC_DB_PASSWORD"
else
printf '%s\n' "ALTER ROLE \"${GARDOMATIC_DB_USER}\" WITH LOGIN PASSWORD :'db_password';" | \
run_as_postgres psql -v db_password="$GARDOMATIC_DB_PASSWORD"
fi
run_as_postgres psql -c "ALTER DATABASE \"${GARDOMATIC_DB_NAME}\" OWNER TO \"${GARDOMATIC_DB_USER}\";"
# Generate a dedicated systemd environment file. Setup-only values such as the
# raw database password are intentionally not copied into the service environment.
write_environment_variable() {
local name=$1
local value=${!name:-}
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "$name must not contain newlines"
value=${value//\\/\\\\}
value=${value//\"/\\\"}
value=${value//\$/\\$}
value=${value//\`/\\\`}
printf '%s="%s"\n' "$name" "$value" >>"$runtime_env"
}
runtime_variables=(
GARDOMATIC_ENV
GARDOMATIC_DB_DSN
GARDOMATIC_DB_MAX_OPEN_CONNS
GARDOMATIC_DB_MAX_IDLE_CONNS
GARDOMATIC_DB_MAX_IDLE_TIME
GARDOMATIC_API_HOST
GARDOMATIC_API_PORT
GARDOMATIC_WEB_HOST
GARDOMATIC_WEB_PORT
GARDOMATIC_API_BASE_URL
GARDOMATIC_WEB_BASE_URL
GARDOMATIC_SESSION_COOKIE_NAME
GARDOMATIC_SESSION_LIFETIME
GARDOMATIC_SESSION_IDLE_TIMEOUT
GARDOMATIC_COOKIE_SECURE
GARDOMATIC_RATE_LIMIT_ENABLED
GARDOMATIC_RATE_LIMIT_RPS
GARDOMATIC_RATE_LIMIT_BURST
GARDOMATIC_CORS_TRUSTED_ORIGINS
GARDOMATIC_SMTP_MODE
GARDOMATIC_SMTP_HOST
GARDOMATIC_SMTP_PORT
GARDOMATIC_SMTP_USERNAME
GARDOMATIC_SMTP_PASSWORD
GARDOMATIC_SMTP_SENDER
GARDOMATIC_SMTP_FILE_PATH
)
for variable in "${runtime_variables[@]}"; do
write_environment_variable "$variable"
done
run_as_root install -D -m 0600 -o root -g root "$runtime_env" /etc/gardomatic/gardomatic.env
run_as_root apt --yes -o Dpkg::Options::="--force-confnew" upgrade
printf 'Server setup complete. Runtime configuration installed at /etc/gardomatic/gardomatic.env.\n'
if [[ "$GARDOMATIC_REBOOT" == true ]]; then
run_as_root reboot
else
printf 'Reboot skipped. Set GARDOMATIC_REBOOT=true in %s to reboot automatically.\n' "$ENV_LABEL"
fi