ci: run Renovate from asxpi/renovate
Some checks failed
nixos / asxpio-services (push) Failing after 54s
Some checks failed
nixos / asxpio-services (push) Failing after 54s
Renovate now runs from its own repository for every repo, with the shared rules in its preset. Extend that preset here and keep only what is specific to this repo (the Ansible var manager, the Paper JRE, the runner and nix notes); drop the workflow that ran it for iac alone. Assisted-by: Claude:opus-5-5
This commit is contained in:
parent
131124e0de
commit
71047d179d
15 changed files with 705 additions and 284 deletions
|
|
@ -1,183 +1,81 @@
|
|||
# Containers run under Podman, one systemd unit each
|
||||
# (podman-<name>.service). Game data stays in /opt and is not managed here.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
# Minecraft and SillyTavern. Their data stays in /opt and is not managed
|
||||
# here.
|
||||
{ config, ... }:
|
||||
let
|
||||
podman = lib.getExe pkgs.podman;
|
||||
# Time for a Minecraft server to save the world before it is killed
|
||||
# Paper needs Java 21; the tag is a floating JRE line
|
||||
javaImage = "docker.io/library/eclipse-temurin:21-jre";
|
||||
paperJar = "paper-1.21.11-54.jar";
|
||||
# Time for the server to save the world before it is killed
|
||||
stopTimeout = 120;
|
||||
|
||||
# Nothing that owns data starts on a freshly installed disk: a game
|
||||
# server would generate a new world where the restore is about to land.
|
||||
# After restoring /opt, `touch /opt/.restored` and start the units.
|
||||
needsData.unitConfig.ConditionPathExists = "/opt/.restored";
|
||||
|
||||
gameServer = needsData // {
|
||||
serviceConfig = {
|
||||
TimeoutStopSec = lib.mkForce (stopTimeout + 30);
|
||||
# A JVM stopped by SIGTERM exits 143 after saving; that is a clean stop
|
||||
SuccessExitStatus = [ 143 ];
|
||||
};
|
||||
};
|
||||
# renovate: datasource=docker depName=ghcr.io/sillytavern/sillytavern
|
||||
sillytavernVersion = "1.19.0";
|
||||
in
|
||||
{
|
||||
virtualisation.podman = {
|
||||
enable = true;
|
||||
dockerCompat = true; # `docker` runs podman
|
||||
dockerSocket.enable = true; # /run/docker.sock, used by the CI runner
|
||||
defaultNetwork.settings.dns_enabled = true;
|
||||
autoPrune = {
|
||||
enable = true;
|
||||
dates = "Mon 03:00";
|
||||
flags = [ "--all" ];
|
||||
};
|
||||
};
|
||||
virtualisation.containers.registries.search = [ "docker.io" ];
|
||||
virtualisation.oci-containers.backend = "podman";
|
||||
|
||||
sops.secrets."minecraft_fossils/env" = { };
|
||||
|
||||
virtualisation.oci-containers.containers = {
|
||||
# Paper server; the jar and the world live in /opt/minecraft
|
||||
minecraft = {
|
||||
image = "docker.io/library/eclipse-temurin:21-jre";
|
||||
workdir = "/data";
|
||||
cmd = [
|
||||
"java"
|
||||
"-Xmx4G"
|
||||
"-Xms2G"
|
||||
"-jar"
|
||||
"paper-1.21.11-54.jar"
|
||||
"nogui"
|
||||
];
|
||||
ports = [
|
||||
"25565:25565"
|
||||
"19132:19132/udp"
|
||||
];
|
||||
volumes = [ "/opt/minecraft:/data" ];
|
||||
extraOptions = [
|
||||
"--tty"
|
||||
"--interactive"
|
||||
"--stop-timeout=${toString stopTimeout}"
|
||||
];
|
||||
};
|
||||
|
||||
minecraft-fossils = {
|
||||
image = "docker.io/itzg/minecraft-server:java21";
|
||||
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";
|
||||
};
|
||||
environmentFiles = [ config.sops.secrets."minecraft_fossils/env".path ];
|
||||
ports = [ "25566:25565" ];
|
||||
volumes = [ "/opt/minecraft-fossils/data:/data" ];
|
||||
extraOptions = [
|
||||
"--tty"
|
||||
"--interactive"
|
||||
"--network=fossils"
|
||||
"--stop-timeout=${toString stopTimeout}"
|
||||
];
|
||||
};
|
||||
|
||||
# Copies whitelist, ops and ban lists from the Paper server
|
||||
minecraft-fossils-wlsync = {
|
||||
image = "docker.io/library/alpine:3";
|
||||
dependsOn = [ "minecraft-fossils" ];
|
||||
environment = {
|
||||
RCON_HOST = "minecraft-fossils";
|
||||
INTERVAL = "15";
|
||||
};
|
||||
environmentFiles = [ config.sops.secrets."minecraft_fossils/env".path ];
|
||||
volumes = [
|
||||
"/opt/minecraft:/src:ro"
|
||||
"/opt/minecraft-fossils/data:/dst"
|
||||
"${./wlsync.sh}:/wlsync.sh:ro"
|
||||
];
|
||||
cmd = [
|
||||
"sh"
|
||||
"/wlsync.sh"
|
||||
];
|
||||
extraOptions = [ "--network=fossils" ];
|
||||
};
|
||||
|
||||
# Reachable on the WireGuard address only
|
||||
sillytavern = {
|
||||
image = "ghcr.io/sillytavern/sillytavern:1.19.0";
|
||||
ports = [ "10.20.0.20:8000:8000" ];
|
||||
volumes = [
|
||||
"/opt/sillytavern/config:/home/node/app/config"
|
||||
"/opt/sillytavern/data:/home/node/app/data"
|
||||
"/opt/sillytavern/plugins:/home/node/app/plugins"
|
||||
];
|
||||
# Paper server; the jar and the world live in /opt/minecraft. Not an
|
||||
# oci-containers unit: the console needs a TTY and an open stdin, and
|
||||
# `docker run -it` refuses to start without a terminal. The container
|
||||
# is created first and then attached to, output only.
|
||||
#
|
||||
# docker attach minecraft console; leave with Ctrl-p Ctrl-q
|
||||
systemd.services.docker-minecraft = {
|
||||
description = "Minecraft (Paper)";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
# requires: Docker restarting stops the server first, with time to save
|
||||
requires = [ "docker.service" ];
|
||||
after = [
|
||||
"docker.service"
|
||||
"network-online.target"
|
||||
];
|
||||
unitConfig.ConditionPathExists = "/opt/.restored";
|
||||
path = [ config.virtualisation.docker.package ];
|
||||
preStart = ''
|
||||
docker rm -f minecraft || true
|
||||
docker create --name minecraft --tty --interactive \
|
||||
--stop-timeout ${toString stopTimeout} \
|
||||
--workdir /data --volume /opt/minecraft:/data \
|
||||
--publish 25565:25565 --publish 19132:19132/udp \
|
||||
${javaImage} java -Xmx4G -Xms2G -jar ${paperJar} nogui
|
||||
'';
|
||||
script = "exec docker start --attach minecraft";
|
||||
preStop = "docker stop minecraft || true";
|
||||
postStop = "docker rm -f minecraft || true";
|
||||
serviceConfig = {
|
||||
TimeoutStartSec = 0; # the first start pulls the image
|
||||
TimeoutStopSec = stopTimeout + 30;
|
||||
# A JVM stopped by SIGTERM exits 143 after saving; that is a clean stop
|
||||
SuccessExitStatus = [ 143 ];
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services = {
|
||||
# Network shared by the Fossils server and its sidecar
|
||||
podman-network-fossils = {
|
||||
description = "Podman network fossils";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [
|
||||
"podman-minecraft-fossils.service"
|
||||
"podman-minecraft-fossils-wlsync.service"
|
||||
];
|
||||
requiredBy = [
|
||||
"podman-minecraft-fossils.service"
|
||||
"podman-minecraft-fossils-wlsync.service"
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = "${podman} network exists fossils || ${podman} network create fossils";
|
||||
};
|
||||
|
||||
podman-minecraft = gameServer;
|
||||
podman-minecraft-fossils = gameServer;
|
||||
podman-minecraft-fossils-wlsync = needsData;
|
||||
|
||||
# The WireGuard address must exist before a port can be bound to it
|
||||
podman-sillytavern = needsData // {
|
||||
after = [ "wireguard-asxpio-services.service" ];
|
||||
requires = [ "wireguard-asxpio-services.service" ];
|
||||
};
|
||||
|
||||
minecraft-fossils-restart = {
|
||||
description = "Daily restart of minecraft-fossils";
|
||||
serviceConfig.Type = "oneshot";
|
||||
script = "systemctl restart podman-minecraft-fossils.service";
|
||||
};
|
||||
# Reachable on the WireGuard address only
|
||||
virtualisation.oci-containers.containers.sillytavern = {
|
||||
image = "ghcr.io/sillytavern/sillytavern:${sillytavernVersion}";
|
||||
ports = [ "10.20.0.20:8000:8000" ];
|
||||
volumes = [
|
||||
"/opt/sillytavern/config:/home/node/app/config"
|
||||
"/opt/sillytavern/data:/home/node/app/data"
|
||||
"/opt/sillytavern/plugins:/home/node/app/plugins"
|
||||
];
|
||||
};
|
||||
|
||||
systemd.timers.minecraft-fossils-restart = {
|
||||
description = "Restart minecraft-fossils daily at 08:00 Europe/Tallinn";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = "*-*-* 08:00:00 Europe/Tallinn";
|
||||
Persistent = true;
|
||||
};
|
||||
# The WireGuard address must exist before a port can be bound to it
|
||||
systemd.services.docker-sillytavern = {
|
||||
unitConfig.ConditionPathExists = "/opt/.restored";
|
||||
after = [ "wireguard-asxpio-services.service" ];
|
||||
requires = [ "wireguard-asxpio-services.service" ];
|
||||
};
|
||||
|
||||
# Published ports reach the containers without passing these rules over
|
||||
# IPv4. Over IPv6 Docker's proxy listens on the host, so they are needed.
|
||||
networking.firewall.allowedTCPPorts = [ 25565 ];
|
||||
networking.firewall.allowedUDPPorts = [ 19132 ];
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /opt 0755 root root -"
|
||||
"d /opt/minecraft 0755 root root -"
|
||||
"d /opt/minecraft-fossils 0755 asxpi users -"
|
||||
"d /opt/minecraft-fossils/data 0755 asxpi users -"
|
||||
"d /opt/sillytavern/config 0755 root root -"
|
||||
"d /opt/sillytavern/data 0755 root root -"
|
||||
"d /opt/sillytavern/plugins 0755 root root -"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
# asxpio-services: CI runner, Minecraft, SillyTavern.
|
||||
# asxpio-services: CI runner, Minecraft, XMPP, Xray, SillyTavern.
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
imports = [
|
||||
./containers.nix
|
||||
./replica.nix
|
||||
./runner.nix
|
||||
./xmpp.nix
|
||||
./xray.nix
|
||||
];
|
||||
|
||||
networking.hostName = lib.mkDefault "asxpio-services";
|
||||
|
|
@ -59,27 +62,26 @@
|
|||
};
|
||||
|
||||
# --- Firewall -----------------------------------------------------------
|
||||
# The Hetzner firewall is the outer layer. Published container ports are
|
||||
# forwarded by Podman and do not pass through these input rules.
|
||||
networking.firewall = {
|
||||
allowedTCPPorts = [
|
||||
25565
|
||||
25566
|
||||
# The Hetzner firewall is the outer layer. Each service opens its own
|
||||
# ports next to its definition.
|
||||
networking.firewall.interfaces = {
|
||||
asxpio-services.allowedTCPPorts = [
|
||||
22
|
||||
8000
|
||||
];
|
||||
enp7s0.allowedTCPPorts = [ 22 ];
|
||||
};
|
||||
|
||||
# --- Backup -------------------------------------------------------------
|
||||
# The Paper world is copied live (RCON is off, so there is no save-off);
|
||||
# its autosave keeps that close enough. What Paper downloads again is
|
||||
# left out.
|
||||
asxpio.backup = {
|
||||
schedule = "*-*-* 04:15:00";
|
||||
excludes = [
|
||||
"/opt/minecraft/cache"
|
||||
"/opt/minecraft/libraries"
|
||||
"/opt/minecraft/versions"
|
||||
];
|
||||
allowedUDPPorts = [ 19132 ];
|
||||
interfaces = {
|
||||
asxpio-services.allowedTCPPorts = [
|
||||
22
|
||||
8000
|
||||
];
|
||||
enp7s0.allowedTCPPorts = [ 22 ];
|
||||
# CI job containers reach the runner's cache server on the host
|
||||
"podman+".allowedTCPPortRanges = [
|
||||
{
|
||||
from = 1024;
|
||||
to = 65535;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
93
nixos/hosts/asxpio-services/replica.nix
Normal file
93
nixos/hosts/asxpio-services/replica.nix
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Daily one-way replica of the hel1 buckets into their fsn1 twins.
|
||||
# restic/ is excluded: restic writes its own, independent fsn1 repository.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
pairs = [
|
||||
{
|
||||
src = "asxpio-backups-main";
|
||||
dst = "asxpio-backups-repl";
|
||||
exclude = [ "restic/**" ];
|
||||
}
|
||||
{
|
||||
src = "asxpio-terraform-main";
|
||||
dst = "asxpio-terraform-repl";
|
||||
}
|
||||
{
|
||||
src = "k8s-asxpio-main";
|
||||
dst = "k8s-asxpio-repl";
|
||||
}
|
||||
];
|
||||
|
||||
# A run that would delete more than this stops and fails (and mails):
|
||||
# a mistake on the hel1 side must not empty the replica the next morning
|
||||
maxDelete = 1000;
|
||||
|
||||
secret = name: config.sops.placeholder.${name};
|
||||
remote = name: region: extra: ''
|
||||
RCLONE_CONFIG_${name}_TYPE=s3
|
||||
RCLONE_CONFIG_${name}_PROVIDER=Other
|
||||
RCLONE_CONFIG_${name}_ENDPOINT=https://${region}.your-objectstorage.com
|
||||
RCLONE_CONFIG_${name}_REGION=${region}
|
||||
RCLONE_CONFIG_${name}_ACCESS_KEY_ID=${secret "s3/access_key"}
|
||||
RCLONE_CONFIG_${name}_SECRET_ACCESS_KEY=${secret "s3/secret_key"}
|
||||
${extra}'';
|
||||
|
||||
replicate = pkgs.writeShellApplication {
|
||||
name = "s3-replicate";
|
||||
runtimeInputs = [ pkgs.rclone ];
|
||||
# No errexit: a failing pair does not stop the others, but fails the run
|
||||
bashOptions = [
|
||||
"nounset"
|
||||
"pipefail"
|
||||
];
|
||||
text = ''
|
||||
failed=0
|
||||
''
|
||||
+ lib.concatMapStrings (p: ''
|
||||
echo "s3-replicate: ${p.src} -> ${p.dst}"
|
||||
rclone sync hel1:${p.src} fsn1:${p.dst} \
|
||||
--fast-list --checkers 16 --transfers 8 --max-delete ${toString maxDelete} \
|
||||
${lib.concatMapStringsSep " " (e: "--exclude ${lib.escapeShellArg e}") (p.exclude or [ ])} \
|
||||
--stats-one-line -v || failed=1
|
||||
'') pairs
|
||||
+ ''
|
||||
exit $failed
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
# The object storage remotes hel1: and fsn1:, as rclone environment
|
||||
# variables. The migration's restore script reads them too.
|
||||
sops.templates."s3-replicate-env".content =
|
||||
remote "HEL1" "hel1" ""
|
||||
+ remote "FSN1" "fsn1" ''
|
||||
RCLONE_CONFIG_FSN1_NO_CHECK_BUCKET=true
|
||||
'';
|
||||
|
||||
systemd.services.s3-replicate = {
|
||||
description = "Replicate the hel1 buckets to fsn1";
|
||||
wants = [ "network-online.target" ];
|
||||
after = [ "network-online.target" ];
|
||||
onFailure = [ "restic-alert@%n.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = lib.getExe replicate;
|
||||
EnvironmentFile = config.sops.templates."s3-replicate-env".path;
|
||||
TimeoutStartSec = "12h";
|
||||
};
|
||||
};
|
||||
systemd.timers.s3-replicate = {
|
||||
description = "Daily hel1 -> fsn1 bucket replica";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = "*-*-* 06:00:00";
|
||||
RandomizedDelaySec = "15m";
|
||||
Persistent = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -1,13 +1,22 @@
|
|||
# Forgejo Actions runner. It runs as a container and starts job containers
|
||||
# through Podman's Docker-compatible socket. Its registration lives in
|
||||
# through the Docker socket. Its registration lives in
|
||||
# /opt/forgejo-runner/data/.runner and is state, not configuration.
|
||||
#
|
||||
# Deploying a change to it restarts the runner under the job that deploys:
|
||||
# the change lands, that job shows as cancelled, and the next run confirms
|
||||
# the result.
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
# renovate: datasource=docker depName=code.forgejo.org/forgejo/runner
|
||||
runnerVersion = "13";
|
||||
|
||||
# Image behind the nix-latest label. Jobs mount a nix-store volume over
|
||||
# /nix, seeded from this image on first use; the image's /bin/sh and
|
||||
# /etc/nix/nix.conf link into that store, so a volume from another
|
||||
# version breaks them. Bumping this also switches to a fresh volume.
|
||||
nixVersion = "2.35.3";
|
||||
# version breaks them. Bumping this also switches to a fresh volume;
|
||||
# remove the old one afterwards: docker volume rm nix-store-<old version>
|
||||
# renovate: datasource=docker depName=nixos/nix
|
||||
nixVersion = "2.35.6";
|
||||
|
||||
settings = {
|
||||
log = {
|
||||
|
|
@ -65,8 +74,8 @@ let
|
|||
in
|
||||
{
|
||||
virtualisation.oci-containers.containers.forgejo-runner = {
|
||||
image = "code.forgejo.org/forgejo/runner:11";
|
||||
user = "0:0"; # needs the container socket
|
||||
image = "code.forgejo.org/forgejo/runner:${runnerVersion}";
|
||||
user = "0:0"; # needs the Docker socket
|
||||
cmd = [
|
||||
"forgejo-runner"
|
||||
"daemon"
|
||||
|
|
@ -77,17 +86,29 @@ in
|
|||
volumes = [
|
||||
"/opt/forgejo-runner/data:/data"
|
||||
"${configFile}:/config.yaml:ro"
|
||||
"/run/podman/podman.sock:/var/run/docker.sock"
|
||||
"/var/run/docker.sock:/var/run/docker.sock"
|
||||
];
|
||||
extraOptions = [ "--network=host" ];
|
||||
};
|
||||
|
||||
systemd.services.podman-forgejo-runner = {
|
||||
after = [ "podman.socket" ];
|
||||
requires = [ "podman.socket" ];
|
||||
# Without its registration the runner only crash-loops
|
||||
unitConfig.ConditionPathExists = "/opt/forgejo-runner/data/.runner";
|
||||
};
|
||||
# Without its registration the runner only crash-loops
|
||||
systemd.services.docker-forgejo-runner.unitConfig.ConditionPathExists =
|
||||
"/opt/forgejo-runner/data/.runner";
|
||||
|
||||
# CI job containers reach the runner's cache server on the host
|
||||
networking.firewall.interfaces =
|
||||
let
|
||||
highPorts.allowedTCPPortRanges = [
|
||||
{
|
||||
from = 1024;
|
||||
to = 65535;
|
||||
}
|
||||
];
|
||||
in
|
||||
{
|
||||
docker0 = highPorts;
|
||||
"br-+" = highPorts;
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [ "d /opt/forgejo-runner/data 0777 root root -" ];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
#!/bin/sh
|
||||
# Mirrors whitelist, ops and ban lists from the Paper server into the
|
||||
# Fossils server and tells it to reload them over RCON.
|
||||
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
|
||||
134
nixos/hosts/asxpio-services/xmpp.nix
Normal file
134
nixos/hosts/asxpio-services/xmpp.nix
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# another.im XMPP server for asxp.io: ejabberd, apushd (push), tackygram
|
||||
# (Telegram gateway). After compose.yaml and compose.tackygram.yaml of
|
||||
# https://dev.another.im/another-im/anotherim-server, CERTS=files.
|
||||
#
|
||||
# Secrets and keys are host data in /opt/xmpp/state, like a game world,
|
||||
# and are covered by the restic backup:
|
||||
# secrets.env ejabberd secrets
|
||||
# certs/server.pem TLS certificate (asxp.io, conference., xmpp.)
|
||||
# apushd/ push settings and VAPID key
|
||||
# tackygram/{env,key} gateway credentials and master key
|
||||
# Data: Docker volumes xmpp_database, xmpp_tackygram.
|
||||
{ config, pkgs, ... }:
|
||||
let
|
||||
# renovate: datasource=docker depName=dev.another.im/another-im/anotherim-server
|
||||
ejabberdVersion = "2026.11.07";
|
||||
# renovate: datasource=docker depName=dev.another.im/another-im/apushd
|
||||
apushdVersion = "0.2";
|
||||
# renovate: datasource=docker depName=dev.another.im/another-im/tackygram
|
||||
tackygramVersion = "0.4.3";
|
||||
|
||||
domain = "asxp.io";
|
||||
publicIp = "46.62.228.216";
|
||||
state = "/opt/xmpp/state";
|
||||
|
||||
tcpPorts = [
|
||||
5222 # clients, STARTTLS
|
||||
5223 # clients, direct TLS
|
||||
5269 # other servers
|
||||
5443 # WebSocket, host-meta
|
||||
8448 # Matrix federation
|
||||
3478 # STUN and TURN
|
||||
7777 # SOCKS5 bytestreams proxy
|
||||
];
|
||||
turnRelay = {
|
||||
from = 49160;
|
||||
to = 49199;
|
||||
};
|
||||
gatewayCalls = {
|
||||
from = 50000;
|
||||
to = 50099;
|
||||
};
|
||||
range = r: "${toString r.from}-${toString r.to}";
|
||||
|
||||
needsData.unitConfig.ConditionPathExists = "/opt/.restored";
|
||||
in
|
||||
{
|
||||
virtualisation.oci-containers.containers = {
|
||||
xmpp-ejabberd = {
|
||||
image = "dev.another.im/another-im/anotherim-server:${ejabberdVersion}";
|
||||
environment = {
|
||||
EJABBERD_MACRO_XMPP_DOMAIN = domain;
|
||||
EJABBERD_MACRO_TURN_IP = publicIp;
|
||||
EJABBERD_MACRO_POW_DIFFICULTY = "6";
|
||||
EJABBERD_MACRO_REGISTRATION_TIMEOUT = "600";
|
||||
EJABBERD_MACRO_UPLOAD_URL = "https://hel1.your-objectstorage.com/asxpio-xmpp-public/";
|
||||
EJABBERD_MACRO_ACME_URL = "https://acme-v02.api.letsencrypt.org/directory";
|
||||
};
|
||||
environmentFiles = [ "${state}/secrets.env" ];
|
||||
ports =
|
||||
map (p: "${toString p}:${toString p}") tcpPorts
|
||||
++ [
|
||||
"3478:3478/udp"
|
||||
"${range turnRelay}:${range turnRelay}/udp"
|
||||
"${range gatewayCalls}:${range gatewayCalls}/udp"
|
||||
];
|
||||
volumes = [
|
||||
"xmpp_database:/opt/ejabberd/database"
|
||||
"xmpp_logs:/opt/ejabberd/logs"
|
||||
"${state}/certs:/opt/ejabberd/conf/certs:ro"
|
||||
];
|
||||
};
|
||||
|
||||
# Push App Server and Telegram gateway share ejabberd's network
|
||||
# namespace: they reach its component port on 127.0.0.1 only. They
|
||||
# restart with it, since the namespace is gone when it stops.
|
||||
xmpp-apushd = {
|
||||
image = "dev.another.im/another-im/apushd:${apushdVersion}";
|
||||
dependsOn = [ "xmpp-ejabberd" ];
|
||||
networks = [ "container:xmpp-ejabberd" ];
|
||||
volumes = [ "${state}/apushd:/etc/apushd:ro" ];
|
||||
};
|
||||
|
||||
xmpp-tackygram = {
|
||||
image = "dev.another.im/another-im/tackygram:${tackygramVersion}";
|
||||
dependsOn = [ "xmpp-ejabberd" ];
|
||||
networks = [ "container:xmpp-ejabberd" ];
|
||||
environmentFiles = [ "${state}/tackygram/env" ];
|
||||
environment = {
|
||||
CREDENTIALS_DIRECTORY = "/run/credentials";
|
||||
TACKYGRAM_CALL_IP = publicIp;
|
||||
TACKYGRAM_CALL_PORTS = range gatewayCalls;
|
||||
};
|
||||
volumes = [
|
||||
"xmpp_tackygram:/var/lib/tackygram"
|
||||
"${state}/tackygram/key:/run/credentials/gateway-key:ro"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services = {
|
||||
docker-xmpp-ejabberd = needsData;
|
||||
docker-xmpp-apushd = needsData;
|
||||
docker-xmpp-tackygram = needsData;
|
||||
};
|
||||
|
||||
# For IPv6; see containers.nix
|
||||
networking.firewall = {
|
||||
allowedTCPPorts = tcpPorts;
|
||||
allowedUDPPorts = [ 3478 ];
|
||||
allowedUDPPortRanges = [
|
||||
turnRelay
|
||||
gatewayCalls
|
||||
];
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [ "d ${state} 0700 root root -" ];
|
||||
|
||||
# ejabberd's Mnesia through its own backup command, its SQLite through
|
||||
# the online backup API
|
||||
asxpio.backup = {
|
||||
paths = [
|
||||
"/var/lib/docker/volumes/xmpp_database"
|
||||
"/var/lib/docker/volumes/xmpp_tackygram"
|
||||
];
|
||||
hookPackages = [
|
||||
config.virtualisation.docker.package
|
||||
pkgs.sqlite
|
||||
];
|
||||
preHooks = [
|
||||
"docker exec xmpp-ejabberd ejabberdctl backup /opt/ejabberd/database/nightly.backup"
|
||||
''sqlite3 /var/lib/docker/volumes/xmpp_database/_data/ejabberd.sqlite ".backup '$STAGE/ejabberd.sqlite'"''
|
||||
];
|
||||
};
|
||||
}
|
||||
32
nixos/hosts/asxpio-services/xray.nix
Normal file
32
nixos/hosts/asxpio-services/xray.nix
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Xray VLESS Reality proxy on :443, host network. Root in the container
|
||||
# only to read the 0400 config and bind :443; every other capability is
|
||||
# dropped.
|
||||
{ config, ... }:
|
||||
let
|
||||
# renovate: datasource=docker depName=ghcr.io/xtls/xray-core
|
||||
xrayVersion = "26.11.9";
|
||||
in
|
||||
{
|
||||
# The whole config.json; a new one restarts the container
|
||||
sops.secrets."xray/config".restartUnits = [ "docker-xray.service" ];
|
||||
|
||||
virtualisation.oci-containers.containers.xray = {
|
||||
image = "ghcr.io/xtls/xray-core:${xrayVersion}";
|
||||
cmd = [
|
||||
"run"
|
||||
"-c"
|
||||
"/etc/xray/config.json"
|
||||
];
|
||||
user = "0";
|
||||
volumes = [ "${config.sops.secrets."xray/config".path}:/etc/xray/config.json:ro" ];
|
||||
extraOptions = [
|
||||
"--network=host"
|
||||
"--cap-drop=ALL"
|
||||
"--cap-add=NET_BIND_SERVICE"
|
||||
"--security-opt=no-new-privileges:true"
|
||||
"--read-only"
|
||||
];
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 443 ];
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
# Overrides that turn asxpio-services into a throwaway test machine:
|
||||
# plain DHCP, SSH from anywhere, no WireGuard, dummy secrets.
|
||||
{ lib, ... }:
|
||||
let
|
||||
manual.autoStart = lib.mkForce false;
|
||||
in
|
||||
{
|
||||
networking.hostName = "rehearsal";
|
||||
sops.defaultSopsFile = ../secrets/rehearsal.yaml;
|
||||
|
|
@ -13,11 +16,26 @@
|
|||
};
|
||||
|
||||
networking.wireguard.interfaces = lib.mkForce { };
|
||||
systemd.services.podman-sillytavern = {
|
||||
systemd.services.docker-sillytavern = {
|
||||
after = lib.mkForce [ ];
|
||||
requires = lib.mkForce [ ];
|
||||
};
|
||||
virtualisation.oci-containers.containers.sillytavern.ports = lib.mkForce [ "127.0.0.1:8000:8000" ];
|
||||
virtualisation.oci-containers.containers = {
|
||||
sillytavern.ports = lib.mkForce [ "127.0.0.1:8000:8000" ];
|
||||
|
||||
# With production data restored these would act as production: a
|
||||
# second asxp.io XMPP server and Telegram session, a runner taking
|
||||
# real CI jobs. Start them by hand, and only with test data.
|
||||
xmpp-ejabberd = manual;
|
||||
xmpp-apushd = manual;
|
||||
xmpp-tackygram = manual;
|
||||
forgejo-runner = manual;
|
||||
xray = manual; # its config is a dummy
|
||||
};
|
||||
|
||||
# No backups of, or from, a test machine
|
||||
systemd.timers.restic-backup.enable = false;
|
||||
systemd.timers.s3-replicate.enable = false;
|
||||
|
||||
services.openssh.openFirewall = lib.mkForce true;
|
||||
services.openssh.settings.AllowUsers = [ "root" ];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue