diff --git a/ansible-playbook.yml b/ansible-playbook.yml
new file mode 100755
index 0000000..668326c
--- /dev/null
+++ b/ansible-playbook.yml
@@ -0,0 +1,39 @@
+---
+- hosts: internal-apps
+ become: true
+ vars_files:
+ - ./vars.yml
+ tasks:
+ - name: Pull images
+ containers.podman.podman_image:
+ name: "{{ item }}"
+ loop: "{{ images }}"
+ tags:
+ - images
+
+ roles:
+ - role: packages
+ tags: [packages]
+ - role: directories
+ tags: [directories]
+ - role: sysctl
+ tags: [sysctl]
+ - role: network
+ tags: [network]
+ - role: wireguard
+ tags: [wireguard]
+ - role: npm
+ tags: [npm]
+ - role: gitea
+ tags: [gitea]
+ - role: jellyfin
+ tags: [jellyfin]
+ - role: wazuh
+ tags: [wazuh]
+ # - role: minecraft
+ # tags: [minecraft]
+ # - role: 7dtd
+ # tags: [7dtd]
+ # - role: zomboid
+ # tags: [zomboid]
+
diff --git a/inventory.ini b/inventory.ini
new file mode 100755
index 0000000..11c0d53
--- /dev/null
+++ b/inventory.ini
@@ -0,0 +1,2 @@
+[internal-apps]
+7dtd ansible_user=fedora ansible_become_password="VjocLlmw6faSdM"
diff --git a/requirements.yml b/requirements.yml
new file mode 100755
index 0000000..54b1ffa
--- /dev/null
+++ b/requirements.yml
@@ -0,0 +1,5 @@
+collections:
+ - name: containers.podman
+ - name: ansible.posix
+
+
\ No newline at end of file
diff --git a/roles/7dtd/files/mods/MoreAnimals.zip b/roles/7dtd/files/mods/MoreAnimals.zip
new file mode 100644
index 0000000..538ace6
Binary files /dev/null and b/roles/7dtd/files/mods/MoreAnimals.zip differ
diff --git a/roles/7dtd/files/mods/MoreAnimals/Config/spawning.xml b/roles/7dtd/files/mods/MoreAnimals/Config/spawning.xml
new file mode 100644
index 0000000..796ebea
--- /dev/null
+++ b/roles/7dtd/files/mods/MoreAnimals/Config/spawning.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ 4
+ 0.25,0.438,0.338,0.25,0.163,0.088
+
+
+ 3
+ 0.6,1.05,0.81,0.6,0.39,0.21
+
\ No newline at end of file
diff --git a/roles/7dtd/files/mods/MoreAnimals/ModInfo.xml b/roles/7dtd/files/mods/MoreAnimals/ModInfo.xml
new file mode 100644
index 0000000..fe020df
--- /dev/null
+++ b/roles/7dtd/files/mods/MoreAnimals/ModInfo.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/roles/7dtd/tasks/main.yml b/roles/7dtd/tasks/main.yml
new file mode 100644
index 0000000..6fa45da
--- /dev/null
+++ b/roles/7dtd/tasks/main.yml
@@ -0,0 +1,135 @@
+---
+# 7 Days to Die dedicated server (vinanrra/7dtd-server container, LinuxGSM-based, SteamCMD-installed).
+# PUBLIC server: players connect to 7dtd.boxswapper.io:26900 (TCP+UDP) and enter the join password
+# (sdtd_server_password). Server config is rendered from roles/7dtd/templates/sdtdserver.xml.j2 on
+# every deploy - vars.yml is the source of truth for the server config.
+# External prerequisites (NOT managed by this repo):
+# - Cloudflare A record 7dtd.boxswapper.io -> public IP (grey-cloud, like mc.boxswapper.io)
+# - Router port forwards: TCP+UDP 26900, UDP 26901-26902 + TCP 8081 (telnet) -> 10.10.0.47
+# - CHANGED sdtd_server_password / sdtd_telnet_password in vars.yml before first deploy
+- name: Create 7 Days to Die data directory
+ ansible.builtin.file:
+ path: /opt/apps/7dtd/data
+ state: directory
+ mode: '0755'
+
+# The container (linuxserver-style base) runs as uid/gid 1000 (PUID/PGID) and chowns its volumes on
+# start; pre-seeding with that ownership lets the init script and server write alongside our xml.
+# NOTE: NO /etc/timezone or /etc/localtime bind mounts here (unlike minecraft/zomboid) - the image's
+# user.sh writes BOTH paths itself to apply the TimeZone env var, so ro binds crash-loop the container.
+- name: Create 7 Days to Die saves dir (container user)
+ ansible.builtin.file:
+ path: /opt/apps/7dtd/data/7DaysToDie
+ state: directory
+ owner: '1000'
+ group: '1000'
+ mode: '0755'
+
+- name: Create 7 Days to Die serverfiles directory (container user)
+ ansible.builtin.file:
+ path: /opt/apps/7dtd/serverfiles
+ state: directory
+ owner: '1000'
+ group: '1000'
+ mode: '0755'
+
+# Mods live in the bind-mounted serverfiles; the container loads them on its next start
+# (recreate on every deploy re-reads them). zips come from roles/7dtd/files/mods (sdtd_mods
+# in vars.yml), extracted one level deep (each zip carries its own mod subfolder; prefix
+# folders with z- if they must sort LAST for load order).
+# Deployed now: MoreAnimals — a PURE XML data patch (no DLL/Harmony) raising wild-game/predator
+# spawn density (v3.x schema: maxcount per 80x80m chunk + 6-value respawndelay tuples). Works
+# with EAC ON, needs NO client install (spawning is server-authoritative; the server pushes its
+# XML state to clients on connect). GATE: XML values apply only while the sandbox Density options
+# are Default — once sdtd_sandbox_code sets any density != Default, the sandbox value wins and
+# this patch is inert; use SandboxCode Day/Night Animal Density + Respawn instead.
+# unarchive only ADDS — it never removes, so a mod dropped from sdtd_mods would keep loading:
+# with an empty list we wipe the whole dir and the create-task below rebuilds it empty.
+# Partial mod swaps are not auto-cleaned (the role only knows zip names, not the folders zips
+# unpack into) — remove those retired folders from the box by hand.
+- name: Remove 7 Days to Die Mods directory when no mods are deployed
+ ansible.builtin.file:
+ path: /opt/apps/7dtd/serverfiles/Mods
+ state: absent
+ when: sdtd_mods | length == 0
+
+- name: Create 7 Days to Die Mods directory (container user)
+ ansible.builtin.file:
+ path: /opt/apps/7dtd/serverfiles/Mods
+ state: directory
+ owner: '1000'
+ group: '1000'
+ mode: '0755'
+
+- name: Deploy 7 Days to Die mods from vendored zips
+ ansible.builtin.unarchive:
+ src: "mods/{{ item }}"
+ dest: /opt/apps/7dtd/serverfiles/Mods
+ owner: '1000'
+ group: '1000'
+ mode: '0755'
+ loop: "{{ sdtd_mods }}"
+
+- name: Template 7 Days to Die serverconfig from vars.yml
+ ansible.builtin.template:
+ src: sdtdserver.xml.j2
+ dest: /opt/apps/7dtd/serverfiles/sdtdserver.xml
+ owner: '1000'
+ group: '1000'
+ mode: '0644'
+
+# LinuxGSM reads config-lgsm/sdtdserver/sdtdserver.cfg on EVERY invocation (start/monitor/stop).
+# Bind-mounting it makes the role the owner of that config (see vars.yml sdtd_lgsm_querymode);
+# a previously unmanaged podman volume left defaults in place, causing the monitor crash-loop.
+- name: Create 7 Days to Die LGSM config directory (container user)
+ ansible.builtin.file:
+ path: /opt/apps/7dtd/lgsm
+ state: directory
+ owner: '1000'
+ group: '1000'
+ mode: '0755'
+
+- name: Template 7 Days to Die LGSM config from vars.yml
+ ansible.builtin.template:
+ src: sdtdserver.cfg.j2
+ dest: /opt/apps/7dtd/lgsm/sdtdserver.cfg
+ owner: '1000'
+ group: '1000'
+ mode: '0644'
+
+- name: Deploy 7 Days to Die Server
+ containers.podman.podman_container:
+ image: "{{ sdtd_image }}"
+ name: 7dtd
+ env: "{{ sdtd_env_base }}"
+ restart_policy: always
+ recreate: true
+ state: started
+ volumes:
+ - /opt/apps/7dtd/data:/home/sdtdserver/.local/share/7DaysToDie:z
+ - /opt/apps/7dtd/serverfiles:/home/sdtdserver/serverfiles:z
+ - /opt/apps/7dtd/lgsm:/home/sdtdserver/lgsm/config-lgsm/sdtdserver:z
+ ports:
+ # Required: game port (TCP+UDP), Steam query/beacon ports (UDP)
+ - "26900:26900/tcp"
+ - "26900:26900/udp"
+ - "26901:26901/udp"
+ - "26902:26902/udp"
+ # Optional: telnet admin console (gated by sdtd_telnet_password)
+ - "8081:8081/tcp"
+ network:
+ - npm_internal
+ ip: '{{ sdtd_ip }}'
+ vars:
+ # sdtd_env_base: START_MODE=1 starts the server; PUID/PGID match the pre-seeded dir ownership;
+ # MONITOR=YES restarts the game process if it crashes (LinuxGSM cron).
+ # IMPORTANT: restart_policy: always is only valid with START_MODE=1 (never 0/2/4 per image docs).
+ sdtd_env_base:
+ START_MODE: "{{ sdtd_start_mode }}"
+ VERSION: "{{ sdtd_version }}"
+ PUID: "{{ sdtd_puid }}"
+ PGID: "{{ sdtd_pgid }}"
+ TimeZone: "{{ sdtd_timezone }}"
+ BACKUP: "{{ sdtd_backup }}"
+ MONITOR: "{{ sdtd_monitor }}"
+ TEST_ALERT: "NO"
\ No newline at end of file
diff --git a/roles/7dtd/templates/sdtdserver.cfg.j2 b/roles/7dtd/templates/sdtdserver.cfg.j2
new file mode 100644
index 0000000..c990db7
--- /dev/null
+++ b/roles/7dtd/templates/sdtdserver.cfg.j2
@@ -0,0 +1,14 @@
+##################################
+####### Instance Settings ########
+##################################
+# Managed by Ansible (roles/7dtd/templates/sdtdserver.cfg.j2) - local edits are overwritten on re-run.
+# Source of truth: sdtd_* variables in vars.yml.
+
+## 7dtd answers NO A2S query on the game port (ServerVisibility=0 + SteamNetworking disabled),
+## so the default gamedig-based monitor (querymode=2) always fails and force-restarts the healthy
+## server every ~5 min. querymode=1 checks the tmux session instead.
+querymode="{{ sdtd_lgsm_querymode }}"
+
+## LGSM graceful stop reads ${telnetpassword} from this cfg (the game XML TelnetPassword is parsed
+## into ${telnetpass}, a different var). Without it, stop falls back to a hard tmux kill.
+telnetpassword="{{ sdtd_telnet_password }}"
\ No newline at end of file
diff --git a/roles/7dtd/templates/sdtdserver.xml.j2 b/roles/7dtd/templates/sdtdserver.xml.j2
new file mode 100644
index 0000000..6aa16e1
--- /dev/null
+++ b/roles/7dtd/templates/sdtdserver.xml.j2
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% if sdtd_sandbox_code %}
+
+{% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file