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
This commit is contained in:
Sergei Poljanski 2026-11-12 06:47:08 +04:00
commit 27ccdc4255
52 changed files with 1554 additions and 1031 deletions

View file

@ -0,0 +1,87 @@
services:
minecraft-fossils:
image: itzg/minecraft-server:java21
container_name: minecraft-fossils
restart: unless-stopped
stop_grace_period: 2m
stdin_open: true
tty: true
ports:
- "25566:25565"
env_file:
- secrets.env
environment:
EULA: "TRUE"
TYPE: FABRIC
VERSION: "1.20.1"
FABRIC_LOADER_VERSION: "0.18.4"
MEMORY: "8G"
ONLINE_MODE: "FALSE"
ENFORCE_WHITELIST: "TRUE"
ENFORCE_SECURE_PROFILE: "FALSE"
WHITELIST_FILE: /data/whitelist.json
OPS_FILE: /data/ops.json
ENABLE_RCON: "TRUE"
MOTD: "[mc.czsk.dev] Fossils Archaeology Revival: Genesis"
USE_AIKAR_FLAGS: "TRUE"
volumes:
- /opt/minecraft-fossils/data:/data
networks:
- fossils
whitelist-sync:
image: alpine:3
container_name: minecraft-fossils-wlsync
restart: unless-stopped
depends_on:
- minecraft-fossils
volumes:
- /opt/minecraft:/src:ro
- /opt/minecraft-fossils/data:/dst
env_file:
- secrets.env
environment:
RCON_HOST: minecraft-fossils
INTERVAL: "15"
command:
- sh
- -c
- |
apk add --no-cache python3 >/dev/null
cat > /rcon.py <<'PY'
import socket, struct, sys, os
host, port, pw, cmd = os.environ["RCON_HOST"], 25575, os.environ["RCON_PASSWORD"], sys.argv[1]
def pkt(i, t, b): p = struct.pack("<ii", i, t) + b.encode() + b"\x00\x00"; return struct.pack("<i", len(p)) + p
def recv(s):
ln = struct.unpack("<i", s.recv(4))[0]; d = b""
while len(d) < ln: d += s.recv(ln - len(d))
return struct.unpack("<ii", d[:8])[0], d[8:-2].decode(errors="replace")
s = socket.create_connection((host, port), timeout=5)
s.send(pkt(1, 3, pw))
rid, _ = recv(s)
if rid == -1: print("auth failed"); sys.exit(1)
s.send(pkt(2, 2, cmd))
_, out = recv(s); print(out)
PY
echo "[wlsync] started, interval=$${INTERVAL}s"
while true; do
for f in whitelist.json ops.json banned-players.json banned-ips.json; do
if [ -f /src/$$f ] && ! cmp -s /src/$$f /dst/$$f 2>/dev/null; then
cp /src/$$f /dst/$$f
echo "[wlsync] copied $$f"
case $$f in
whitelist.json) CMD="whitelist reload" ;;
ops.json) CMD="reload" ;;
banned-players.json|banned-ips.json) CMD="kick @a Ban list updated" ;;
esac
python3 /rcon.py "$$CMD" 2>&1 | sed 's/^/[wlsync] rcon: /' || echo "[wlsync] rcon failed"
fi
done
sleep $$INTERVAL
done
networks:
- fossils
networks:
fossils:
driver: bridge

View file

@ -0,0 +1,8 @@
[Unit]
Description=Daily restart of minecraft-fossils container
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
ExecStart=/usr/bin/docker restart minecraft-fossils

View file

@ -0,0 +1,10 @@
[Unit]
Description=Restart minecraft-fossils daily at 08:00 Europe/Tallinn
[Timer]
OnCalendar=*-*-* 08:00:00 Europe/Tallinn
Persistent=true
Unit=minecraft-fossils-restart.service
[Install]
WantedBy=timers.target

View file

@ -0,0 +1,4 @@
---
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true

View file

@ -0,0 +1,49 @@
---
# Fabric server plus a sidecar that mirrors the whitelist, ops and ban
# lists from the Paper server. World data in /opt/minecraft-fossils/data
# is not managed here. A compose change recreates the container, so it
# is saved and stopped with time to finish first.
- name: Deploy Minecraft Fossils
ansible.builtin.include_role:
name: docker_service
vars:
service_name: "minecraft-fossils"
service_base_path: "{{ services_base_path }}/minecraft-fossils"
service_templates:
- src: "secrets.env.j2"
dest: "secrets.env"
owner: "{{ minecraft_fossils_owner }}"
group: "{{ minecraft_fossils_owner }}"
mode: "0600"
service_files:
- src: "docker-compose.yml"
dest: "docker-compose.yml"
owner: "{{ minecraft_fossils_owner }}"
group: "{{ minecraft_fossils_owner }}"
# secrets.env only matters at container start; never restart the
# game server as a side effect of a deploy
service_restart_on_change: false
service_graceful_stop:
- container: minecraft-fossils
timeout: 120
pre_stop: docker exec minecraft-fossils rcon-cli save-all flush
health_check_enabled: false
- name: Install the daily restart units
ansible.builtin.copy:
src: "{{ item }}"
dest: "/etc/systemd/system/{{ item }}"
mode: "0644"
loop:
- minecraft-fossils-restart.service
- minecraft-fossils-restart.timer
notify: Reload systemd
- name: Flush handlers
ansible.builtin.meta: flush_handlers
- name: Enable the daily restart timer
ansible.builtin.systemd:
name: minecraft-fossils-restart.timer
enabled: true
state: started

View file

@ -0,0 +1,2 @@
RCON_PASSWORD={{ minecraft_fossils_rcon_password }}
CF_API_KEY={{ minecraft_fossils_cf_api_key }}