Commit graph

30 commits

Author SHA1 Message Date
4865efd078
acme_forward: send services' ACME challenges to Traefik on ops
All checks were successful
deploy / ansible (push) Successful in 6m25s
The XMPP certificate covers xmpp.asxp.io and conference.asxp.io, which
point at asxpio-services, so Let's Encrypt validates them there. A small
nginx on port 80 passes /.well-known/acme-challenge/ over the VPC to
Traefik, which holds all certificates, and answers 404 to the rest.

Assisted-by: Claude:opus-5-5
2026-11-13 08:07:36 +04:00
c54bf6993c
crawler_alert: let the dry run pass with a new timer
All checks were successful
deploy / ansible (push) Successful in 6m8s
The pipeline's check-mode pass doesn't write the new unit files, so
enabling crawler-metrics.timer failed there and stopped the apply.

Assisted-by: Claude:opus-5-5
2026-11-13 07:33:37 +04:00
bc0dac9188
crawler_alert, monitoring: AI crawler metrics and dashboards
Some checks failed
deploy / ansible (push) Failing after 1m17s
crawler-alert.py --metrics (5-minute timer) reads what the Traefik
access log gained since the last run, judges every request that names
an AI bot exactly like the weekly report (vendor IP ranges, forward-
confirmed rDNS, Meta's ASN) and writes cumulative counters to the
node_exporter textfile directory. Verdicts are cached per bot and IP
for a week, so DNS only sees new addresses; range files are refetched
once a day. It covers every router, while the mails keep watching
Forgejo only.

New dashboard "AI crawlers", and a row of it on the overview.

The overview's load gauge divided node_load1 by a count with fewer
labels, which matches nothing; aggregate both sides by host.

Assisted-by: Claude:opus-5-5
2026-11-13 07:21:57 +04:00
27d4b73eb0
monitoring: keep Loki chunks in S3, 90 days of logs and metrics
Some checks failed
deploy / ansible (push) Has been cancelled
ops has 12 GB free on a 38 GB disk and the Traefik access log alone
is ~600 MB, so logs can't grow there. From 2026-11-14 Loki writes
chunks and index to Hetzner Object Storage (asxpio-loki-main, hel1)
with the existing project key, passed through loki.env and
-config.expand-env. Today's filesystem period ages out by retention.

Log retention goes from 14 to 90 days. Prometheus metrics are small
(~20 MB so far) and stay local, now for 90 days instead of 30.

Assisted-by: Claude:opus-5-5
2026-11-13 07:04:00 +04:00
1664975d4c
monitoring: drop the stale STARTTLS comment
Some checks failed
deploy / ansible (push) Has been cancelled
Assisted-by: Claude:opus-5-5
2026-11-13 06:59:41 +04:00
3197f5fc16
monitoring: probe XMPP c2s with a plain connect
Some checks failed
deploy / ansible (push) Has been cancelled
blackbox_exporter matches query_response line by line, and ejabberd
writes its stream features and <proceed/> without newlines, so the
STARTTLS dialogue always timed out and the probe alert fired while
XMPP was fine. Port 5222 gets a TCP connect; the certificate is still
checked through direct TLS on 5223.

Assisted-by: Claude:opus-5-5
2026-11-13 06:59:31 +04:00
2359370e24
monitoring: collapse session scopes, fix cadvisor healthcheck
All checks were successful
deploy / ansible (push) Successful in 7m59s
Each SSH login from Ansible and CI deploys creates its own
session-N.scope unit. As a journal label that made 3400+ streams and
Loki rejected new ones with 429, dropping container logs. Map them all
to session.scope.

cAdvisor's image healthcheck probes localhost:8080, but it listens on
the VPC address on 9180, so both containers showed unhealthy.

Assisted-by: Claude:opus-5-5
2026-11-13 06:42:10 +04:00
e20f5a7d07
ansible: monitoring with Prometheus, Loki and Grafana
All checks were successful
deploy / ansible (push) Successful in 7m23s
Nothing watched the servers: tonight's asxpio crash loop was found by
hand. On asxpio-ops, Prometheus (30 days), Loki (14 days) and Grafana
at gf.asxp.io (WireGuard only), with blackbox_exporter probing every
service from outside and postgres_exporter. On both hosts node_exporter
and cAdvisor on the VPC address, and Alloy shipping container logs, the
journal and Traefik's access log to Loki over the VPC.

Traefik and Forgejo expose their metrics; restic and the bucket replica
record their last success for node_exporter. Dashboards: an overview as
home, plus hosts, containers, Traefik, probes, Postgres and logs.
Alerts by email to me@asxp.io: host or service down, certificate under
14 days, disk or memory nearly full, a container restart-looping,
Postgres down, backup or replica stale.

Assisted-by: Claude:opus-5-5
2026-11-13 04:59:11 +04:00
5289542dca
docker_service: skip health checks in a dry run
A dry run changes nothing to verify, and for a service that is not on
the host yet the check waited a minute and failed the dry run.

Assisted-by: Claude:opus-5-5
2026-11-13 04:59:11 +04:00
d0f6bcf6cf
ansible: run the another-im/interop runner on asxpio-services
All checks were successful
deploy / ansible (push) Successful in 4m43s
The interop project kept a cx23 (claude-test-interop-runner, about
EUR 6/month with its IPv4) only to run its Forgejo runner. Run it here
instead, with the same registration (UUID + token, token in the vault)
and the same three labels, pinned.

Its PR checks are untrusted, so unlike our own runner its jobs get
neither the Docker socket nor host volumes. nix-host, which on the old
host used the host's Nix store as a cache, is a throwaway container for
now; a second, trusted-only registration can bring the cache back.

Rollback: remove the role from site.yaml (or stop the container) and
start the old runner VM again.

Assisted-by: Claude:opus-5-5
2026-11-13 03:47:17 +04:00
428df5896d
Pin exact versions of images and actions
Some checks failed
deploy / ansible (push) Failing after 17m57s
Floating tags (17-alpine, 3.4-slim, nginx:alpine, :latest, :debug, @v7)
change under a deploy without anyone deciding it: the storage deploy
moved Postgres to 17.12 on its own because of postgres:17-alpine. Pin
every image and action to its exact current version, so each change
arrives as a reviewed Renovate PR and a rollback is a revert.

Assisted-by: Claude:opus-5-5
2026-11-13 02:46:54 +04:00
967ce9e5f6
terraform: drop the DNS import blocks
Both zones and all record sets are in state now; the import blocks
did their job. A plan without them shows no changes.

Assisted-by: Claude:opus-5-5
2026-11-12 23:23:56 +04:00
2e9d4649a0 ansible: daily replica of the hel1 buckets into fsn1
restic writes both regions itself, but the ad-hoc backups under
asxpio-ops/, the Terraform state and the k8s state only ever went to
hel1. Sync each hel1 bucket into its fsn1 twin every morning from
asxpio-services, with rclone pinned like everything else.

restic/ is excluded, so the independent fsn1 restic repositories are
never touched. A run that would delete more than 1000 objects stops and
mails, so a mistake on the hel1 side cannot empty the replica the next
morning. The first dry run copied 23 objects and deleted none.

Assisted-by: Claude:opus-5-5
2026-11-12 22:52:13 +04:00
b4d6852a80 ansible: run Xray on asxpio-services
The VLESS Reality proxy ran from a hand-installed binary on the
temporary server. Run the upstream image instead, pinned and on the
host network, with only CAP_NET_BIND_SERVICE; its config (keys and
client id) is in the vault.

Assisted-by: Claude:opus-5-5
2026-11-12 20:56:42 +04:00
f07eb95030 ansible: run the asxp.io XMPP server on asxpio-services
ejabberd, apushd and the tackygram gateway ran on the temporary
claude-test-xmpp server, a separate CX23 billed on its own. Move them
to asxpio-services, which has the room now that Fossils is gone, and
manage them like the other stacks.

Compose follows anotherim-server's compose.yaml and
compose.tackygram.yaml with CERTS=files; image versions are variables
so they can be tracked. The TURN and call addresses come from the
host's public IP instead of a hand-edited env file. Secrets, keys and
the certificate stay on the host under /opt/xmpp/state as data and are
covered by the restic backup.

Assisted-by: Claude:opus-5-5
2026-11-12 20:56:42 +04:00
2f50229428 docker_service: let the dry run of a new service pass
In check mode nothing is written, so for a service that is not on the
host yet the restart task finds no project directory and fails the
whole dry run. Ignore errors there in check mode, as the compose task
already does.

Assisted-by: Claude:opus-5-5
2026-11-12 20:55:34 +04:00
f2aa6375fd ansible: retire Minecraft Fossils
The Fabric server holds 8.8 GB of asxpio-services' 15 GB of RAM, more
than everything else on the host together, and the XMPP server moves
here next. Save the world, remove its containers and the daily restart
timer.

The world stays in /opt/minecraft-fossils/data, and in the restic
backups, until it is deleted by hand.

Assisted-by: Claude:opus-5-5
2026-11-12 20:54:22 +04:00
59b5b4468a ansible: nightly restic backups to both object storage regions
Hetzner's server backups cost 20% of each server's price and only
restore whole disks. Back up what matters with restic instead, into the
backup buckets we already pay for: one repository per host in hel1
(asxpio-backups-main) and one in fsn1 (asxpio-backups-repl), written
independently so either region alone is a full copy.

Postgres goes in as a dump, Forgejo's and vhpbot's SQLite databases
through the online backup API, Garage with a fresh metadata snapshot,
ejabberd through its own backup command. Keeps 7 daily, 4 weekly and
6 monthly snapshots, prunes on Sundays, and mails the journal when a
run fails.

restic is the pinned upstream release, so its version is tracked in
the repo like everything else. The vault gains the restic password
and the Xray config for the role that follows.

Assisted-by: Claude:opus-5-5
2026-11-12 20:53:44 +04:00
f00505a27d base: drop the one-off crontab migration
The prune job now lives in /etc/cron.d on both hosts and root's crontab
entry is gone, so the tasks that removed it have nothing left to do.

Assisted-by: Claude:fable-5-2
2026-11-12 07:01:07 +04:00
33fbc06681 base: do not fail when the prune entry is root's only cron line
grep -v exits 1 when it prints nothing, and with pipefail that failed
the task on both hosts, although the crontab had been rewritten
correctly. Ignore grep's exit status.

Assisted-by: Claude:fable-5-2
2026-11-12 06:50:37 +04:00
27ccdc4255 ansible: describe what runs on the servers
The roles had drifted far from production: Forgejo was pinned at 12
while 15.0.11 runs, its config lacked the mailer, packages and the real
database path, and several services existed only on the hosts. Running
a role would have rolled production back. Take the servers as the truth
and make the repository match them.

site.yaml now holds the desired state of both hosts:

 - base: unattended upgrades, fail2ban, Docker daemon config and the
   weekly image prune, as found on the hosts. New: a 500M journal cap,
   and the Docker systemd override is kept absent. That override once
   opened dockerd on tcp/2375 without authentication.
 - traefik: templates as deployed, version in a variable (v3.7.18).
   The Coraza WAF files are removed; no router ever used them.
 - forgejo: 15.0.11, app.ini generated from the live file with the
   secrets taken from the vault.
 - crawler_alert, sillytavern, minecraft, minecraft_fossils: added
   from the hosts.

Forgejo's canonical name becomes git.asxp.io; git.czsk.it stays as an
alias on the same router.

docker_service restarts a stack when its config changes, no longer
forces ownership on existing data directories, and hides template
diffs because they carry secrets. Containers listed in
service_graceful_stop are saved and stopped with a long timeout before
compose recreates them: Docker's 10 s default is too short for a
Minecraft world. Both game servers get stop_grace_period for the same
reason.

Removed: the full-deployment and runner playbooks with their prompts,
the Forgejo backup and restore roles for the old bare-metal host, and
inventory entries for services that were never deployed. SSH hardening
stays a manual playbook, since a mistake there locks everyone out.

New secrets (SMTP, RCON, CurseForge key) went into the vault.

Assisted-by: Claude:fable-5-2
2026-11-12 06:47:08 +04:00
a79b110259 forgejo-runner: match the live runner and pin the nix image
The role had drifted from what runs on asxpio-services: capacity 4,
a bridge network per workflow so service containers start, the Docker
socket automounted into jobs, and a nix-latest label backed by a
persistent nix-store volume.

That volume caused a CI outage on 2026-10-18. nixos/nix:latest moved on,
and since the image's /bin/sh and /etc/nix/nix.conf link into /nix/store,
the old volume left them dangling. Pin the image through
forgejo_runner_nix_version and name the volume after it, so a bump is
deliberate and always gets a store seeded from its own image.

Assisted-by: Claude:opus-5-5
2026-10-18 09:48:14 +04:00
0af23425f9 swap: add a swapfile role, 2 GB on asxpio-ops
The ops server has 3.7 GB RAM and no swap. Forgejo, MinIO and the apps
fit with room to spare, but a spike (a big push, git gc, parallel
image pulls) would OOM-kill a service instead of paging out.

swappiness 10, so it only swaps under real pressure.

Assisted-by: Claude:opus-5
2026-10-12 05:31:32 +04:00
66ab6d8db3 s3fs: allow turning the mounts off, off on asxpio-ops
On the 38 GB ops disk the s3fs cache reserve (ensure_diskfree) never
fit, so all three mounts failed at every boot since 2026-09-22. Backups
now go to Hetzner S3 with rclone instead.

Add s3fs_enabled (default true); when false the role unmounts the
buckets and drops their fstab entries, keeping the credentials file
that the rclone wrapper reads.

Assisted-by: Claude:opus-5
2026-10-12 05:03:11 +04:00
ad056c97dc ssh: add user zerrtika 2026-08-13 07:04:04 +04:00
f6c583ddfc ssh managment 2025-11-09 11:58:19 +02:00
8ef380bc9b ansible cleanup 2025-11-04 14:23:16 +02:00
eecff4345a ansible rework 2025-11-04 11:06:35 +02:00
67414814c0 ansible 2025-11-01 22:46:38 +02:00
b81f7e15b2 ansible 2025-11-01 17:15:42 +02:00