Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bd218241d | |||
| 85510acd33 | |||
| 09a5891364 | |||
| a2deef0f5a |
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 external-minecraft contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
103
Makefile
103
Makefile
@@ -1,103 +0,0 @@
|
||||
# =============================================================================
|
||||
# Minecraft Podman Deployment - Makefile
|
||||
# =============================================================================
|
||||
# Usage:
|
||||
# make install # Install dependencies
|
||||
# make deploy # Deploy Minecraft server
|
||||
# make check # Dry-run (check mode)
|
||||
# make debug # Verbose output
|
||||
# make status # Check server status
|
||||
# make backup # Run manual backup
|
||||
# make restore # Restore from latest backup
|
||||
# make logs # View server logs
|
||||
# make stop # Stop Minecraft server
|
||||
# make start # Start Minecraft server
|
||||
# make restart # Restart Minecraft server
|
||||
# make update # Update Minecraft to latest version
|
||||
# make uninstall # Remove Minecraft deployment (dangerous!)
|
||||
# =============================================================================
|
||||
|
||||
PLAYBOOK := site.yml
|
||||
LIMIT ?= external-apps
|
||||
INVENTORY := inventory/hosts
|
||||
|
||||
.PHONY: help install deploy check debug status backup restore logs stop start restart update uninstall
|
||||
|
||||
help:
|
||||
@echo "Minecraft Podman Deployment Makefile"
|
||||
@echo ""
|
||||
@echo "Usage: make [target]"
|
||||
@echo ""
|
||||
@echo "Targets:"
|
||||
@echo " install Install Ansible collections and dependencies"
|
||||
@echo " deploy Deploy Minecraft server (default: $(LIMIT))"
|
||||
@echo " check Dry-run deployment (--check mode)"
|
||||
@echo " debug Deploy with verbose output"
|
||||
@echo " status Check server status on remote"
|
||||
@echo " backup Run manual backup on remote"
|
||||
@echo " restore Restore from latest backup on remote"
|
||||
@echo " logs View Minecraft server logs"
|
||||
@echo " stop Stop Minecraft server"
|
||||
@echo " start Start Minecraft server"
|
||||
@echo " restart Restart Minecraft server"
|
||||
@echo " update Update Minecraft to latest version"
|
||||
@echo " uninstall Remove Minecraft deployment (DANGEROUS)"
|
||||
@echo ""
|
||||
@echo "Override target host: make deploy LIMIT=my-server"
|
||||
|
||||
install:
|
||||
@echo "Installing Ansible collections..."
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
|
||||
deploy:
|
||||
@echo "Deploying Minecraft to $(LIMIT)..."
|
||||
ansible-playbook $(PLAYBOOK) --limit $(LIMIT)
|
||||
|
||||
check:
|
||||
@echo "Dry-run deployment for $(LIMIT)..."
|
||||
ansible-playbook $(PLAYBOOK) --limit $(LIMIT) --check
|
||||
|
||||
debug:
|
||||
@echo "Deploying with debug output for $(LIMIT)..."
|
||||
ansible-playbook $(PLAYBOOK) --limit $(LIMIT) -vvv
|
||||
|
||||
status:
|
||||
@echo "Checking server status on $(LIMIT)..."
|
||||
ansible $(LIMIT) -m shell -a 'systemctl status minecraft --no-pager'
|
||||
|
||||
backup:
|
||||
@echo "Running backup on $(LIMIT)..."
|
||||
ansible $(LIMIT) -m shell -a '/opt/minecraft/backup.sh'
|
||||
|
||||
restore:
|
||||
@echo "Running restore on $(LIMIT)..."
|
||||
ansible $(LIMIT) -m shell -a '/opt/minecraft/restore.sh'
|
||||
|
||||
logs:
|
||||
@echo "Viewing logs for $(LIMIT)..."
|
||||
ansible $(LIMIT) -m shell -a 'journalctl -u minecraft -n 100 --no-pager'
|
||||
|
||||
stop:
|
||||
@echo "Stopping Minecraft server on $(LIMIT)..."
|
||||
ansible $(LIMIT) -m systemd -a 'name=minecraft state=stopped'
|
||||
|
||||
start:
|
||||
@echo "Starting Minecraft server on $(LIMIT)..."
|
||||
ansible $(LIMIT) -m systemd -a 'name=minecraft state=started'
|
||||
|
||||
restart:
|
||||
@echo "Restarting Minecraft server on $(LIMIT)..."
|
||||
ansible $(LIMIT) -m systemd -a 'name=minecraft state=restarted'
|
||||
|
||||
update:
|
||||
@echo "Updating Minecraft on $(LIMIT)..."
|
||||
ansible-playbook $(PLAYBOOK) --limit $(LIMIT) --tags update
|
||||
|
||||
uninstall:
|
||||
@echo "WARNING: This will remove the Minecraft deployment from $(LIMIT)"
|
||||
@read -p "Are you sure? Type 'yes' to continue: " CONFIRM; \
|
||||
if [ "$$CONFIRM" = "yes" ]; then \
|
||||
ansible-playbook $(PLAYBOOK) --limit $(LIMIT) --tags uninstall; \
|
||||
else \
|
||||
echo "Aborted."; \
|
||||
fi
|
||||
280
README.md
280
README.md
@@ -1,190 +1,146 @@
|
||||
# Minecraft Podman Deployment (Ansible)
|
||||
git pu# Hardened Fedora Minecraft Server (Ansible)
|
||||
|
||||
Automated deployment of a Minecraft server running in a **Podman** container on a remote **Fedora** server using **Ansible**.
|
||||
Ansible playbook that turns a fresh **Fedora 42** server into a hardened,
|
||||
containerized **Minecraft** game server — Java *and* Bedrock clients —
|
||||
running with **rootless Podman** via systemd Quadlet units.
|
||||
|
||||
Everything is managed declaratively from `harden_fedora_minecraft.yml`:
|
||||
OS hardening, firewall, SELinux, SSH access control, the Podman runtime,
|
||||
and the Minecraft/Geyser containers are all deployed by one command.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔒 **[Hardening]** SELinux enforcing, firewalld default-DROP policy, password
|
||||
policy (pwquality + aging), root account locked, automatic security updates
|
||||
(`dnf-automatic`).
|
||||
- 🔑 **[SSH]** Key-only authentication for a dedicated admin user, root login
|
||||
disabled, `AllowUsers` restriction, and a `fail2ban` SSH jail.
|
||||
- 🐳 **[Runtime]** Rootless Podman with user lingering, subuid/subgid ranges,
|
||||
IPv4 forwarding, and the overlay storage driver.
|
||||
- ⛏️ **[Minecraft]** Portable Java server built from
|
||||
[`itzg/docker-minecraft-server`](https://github.com/itzg/docker-minecraft-server)
|
||||
(PAPER by default, version/memory/difficulty configurable).
|
||||
- 🪨 **[Geyser]** A second container, [`geysermc/geyser`](https://github.com/GeyserMC/geyser-docker),
|
||||
in the same pod so **Bedrock** (phone/console) players can join the Java world.
|
||||
- 🗄️ **[Data]** World data and backups live on the host in `/srv/minecraft`
|
||||
with correct SELinux labels.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Ansible Control Node │
|
||||
│ (your machine) │
|
||||
│ ansible.cfg + site.yml + roles/ │
|
||||
└──────────────────────┬──────────────────────────┘
|
||||
│ SSH
|
||||
┌──────────────────────▼──────────────────────────┐
|
||||
│ Fedora Server │
|
||||
│ "external-apps" │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────┐ │
|
||||
│ │ Podman Container │ │
|
||||
│ │ itzg/minecraft-server:latest (Paper) │ │
|
||||
│ │ ┌────────────────────────────────────┐ │ │
|
||||
│ │ │ Minecraft Server (Java) │ │ │
|
||||
│ │ └────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ systemd: minecraft.service │
|
||||
│ systemd: minecraft-backup.timer │
|
||||
│ data: /opt/minecraft/data │
|
||||
│ backups: /opt/minecraft/backups │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
Both game containers run in a single Podman **pod** (shared network
|
||||
namespace) as the non-root user `mcadmin`:
|
||||
|
||||
```
|
||||
external-minecraft/
|
||||
├── ansible.cfg # Ansible configuration
|
||||
├── site.yml # Main deployment playbook
|
||||
├── uninstall.yml # Cleanup playbook
|
||||
├── requirements.yml # Galaxy collections
|
||||
├── Makefile # Convenient wrapper
|
||||
├── inventory/
|
||||
│ └── hosts # Server inventory
|
||||
├── group_vars/
|
||||
│ └── all.yml # All configuration variables
|
||||
├── host_vars/ # (optional) Per-host overrides
|
||||
Internet
|
||||
│
|
||||
┌───────┴────────┐
|
||||
│ firewall (DROP)│ only the ports below are allowed
|
||||
└───────┬────────┘
|
||||
│
|
||||
pod: minecraft (rootless, user mcadmin)
|
||||
├── minecraft-server JVM on 0.0.0.0:25565 (Java clients)
|
||||
│ └── world data → /srv/minecraft/data
|
||||
└── geyser Bedrock listener :19132/udp
|
||||
└── config → /srv/minecraft/geyser
|
||||
└── forwards Bedrock players to 127.0.0.1:25565
|
||||
```
|
||||
|
||||
| Port | Protocol | Purpose |
|
||||
|------|----------|---------|
|
||||
| `22` | TCP | SSH (configurable via `ssh_port`) |
|
||||
| `25565` | TCP | Minecraft Java server |
|
||||
| `25565` | UDP | Optional Java server UDP (game query) |
|
||||
| `19132` | UDP | Bedrock (Geyser), when `enable_geyser: true` |
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
.
|
||||
├── harden_fedora_minecraft.yml # entry point: hosts: external-apps → 7 roles
|
||||
├── inventory.yml # target host definition
|
||||
├── ansible.cfg # inventory + role path + pipelining
|
||||
├── requirements.yml # Ansible collections (ansible.posix)
|
||||
├── group_vars/all.yml # shared variables
|
||||
├── host_vars/external-apps.yml # per-server settings (IP, SSH key, MC tuning)
|
||||
└── roles/
|
||||
└── minecraft-podman/
|
||||
├── tasks/main.yml # All deployment tasks
|
||||
├── handlers/main.yml # Service handlers
|
||||
├── templates/ # Jinja2 templates
|
||||
│ ├── minecraft.service.j2
|
||||
│ ├── minecraft.env.j2
|
||||
│ ├── server.properties.j2
|
||||
│ ├── Containerfile.j2
|
||||
│ ├── backup.sh.j2
|
||||
│ ├── restore.sh.j2
|
||||
│ ├── health-check.sh.j2
|
||||
│ └── jvm.opts.j2
|
||||
└── defaults/main.yml # Role defaults
|
||||
├── packages/ # base + container tooling, full dnf update, dnf-automatic
|
||||
├── firewall/ # firewalld: DROP target, open SSH + Minecraft + Geyser
|
||||
├── selinux/ # enforcing mode + container_file_t context for /srv/minecraft
|
||||
├── auth/ # admin user, SSH key, sudoers, password policy, lock root
|
||||
├── ssh_harden/ # sshd drop-in + fail2ban jail
|
||||
├── podman/ # socket, linger, subuid/subgid, ip_forward, storage
|
||||
└── minecraft/ # data dirs, Quadlet pod/container units, image pulls
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### On the control node (your machine)
|
||||
- Ansible control node (this repo) with Ansible Core ≥ 2.14 and `ansible.posix`
|
||||
- A Fedora 42 target server reachable over SSH with an account that has sudo
|
||||
- The `ansible.user` in the inventory must be able to `become` root on the target
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Fedora/RHEL/CentOS
|
||||
sudo dnf install python3-pip
|
||||
pip3 install --user ansible
|
||||
|
||||
# or via pipx
|
||||
pipx install ansible
|
||||
|
||||
# Debian/Ubuntu
|
||||
sudo apt install ansible
|
||||
```
|
||||
|
||||
### Install Ansible collections
|
||||
|
||||
```bash
|
||||
make install
|
||||
# or
|
||||
# 1. Install the required collection
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
|
||||
# 2. Configure the target server (REQUIRED before first run)
|
||||
# - host_vars/external-apps.yml
|
||||
# * ansible_host: real IP or DNS name
|
||||
# * admin_user_ssh_key: your real SSH public key
|
||||
# * game settings to taste (version, memory, whitelist…)
|
||||
|
||||
# 3. Run the playbook
|
||||
ansible-playbook harden_fedora_minecraft.yml
|
||||
```
|
||||
|
||||
> ⚠️ `admin_user_ssh_key` **must** be a real key before the first run —
|
||||
> the playbook disables SSH password authentication and root login once
|
||||
> the `ssh_harden` role executes.
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `group_vars/all.yml` to configure your server:
|
||||
All tunables live in `host_vars/external-apps.yml` (per server) and
|
||||
`group_vars/all.yml` (cluster-wide). Highlights:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `minecraft.server_name` | Service/container name | `minecraft` |
|
||||
| `minecraft.version` | Server version | `latest` |
|
||||
| `minecraft.server_type` | Vanilla / Paper / etc. | `paper` |
|
||||
| `minecraft.java_memory` | Java heap size | `4G` |
|
||||
| `minecraft.max_players` | Max players | `20` |
|
||||
| `minecraft.port` | Minecraft port | `25565` |
|
||||
| `minecraft.rcon_password` | RCON password | `CHANGE_ME_NOW` |
|
||||
| `minecraft.motd` | Server message of day | Welcome message |
|
||||
| `paths.base_dir` | Install directory | `/opt/minecraft` |
|
||||
| `backup.enabled` | Enable automatic backups | `true` |
|
||||
| Variable | Default | Meaning |
|
||||
|----------|---------|---------|
|
||||
| `admin_user` | `mcadmin` | Non-root user running the containers |
|
||||
| `minecraft_version` | `LATEST` | Server version (`1.21.1`, …) |
|
||||
| `minecraft_server_type` | `PAPER` | PAPER / VANILLA / FABRIC / FORGE |
|
||||
| `minecraft_memory` | `2G` | JVM memory limit |
|
||||
| `minecraft_whitelist` | `""` | Comma-separated allowed usernames (empty = off) |
|
||||
| `enable_geyser` | `true` | Deploy the Bedrock bridge |
|
||||
| `geyser_port` | `19132` | Bedrock UDP port |
|
||||
| `geyser_auth_type` | `online` | `offline` only with a paired Floodgate key |
|
||||
| `ssh_port` | `22` | SSH port used by firewall + sshd |
|
||||
|
||||
**IMPORTANT**: Change `minecraft.rcon_password` before deploying.
|
||||
## Operations
|
||||
|
||||
## Deployment
|
||||
After the playbook has run, manage the servers as `mcadmin`:
|
||||
|
||||
```bash
|
||||
# Basic deployment
|
||||
make deploy
|
||||
|
||||
# With explicit host
|
||||
make deploy LIMIT=external-apps
|
||||
|
||||
# Or directly
|
||||
ansible-playbook site.yml --limit external-apps
|
||||
|
||||
# Dry-run
|
||||
make check
|
||||
|
||||
# Debug mode
|
||||
make debug
|
||||
sudo machinectl shell mcadmin@ -- systemctl --user status minecraft-server geyser
|
||||
sudo machinectl shell mcadmin@ -- systemctl --user restart minecraft-server
|
||||
sudo machinectl shell mcadmin@ -- podman ps # container list
|
||||
sudo machinectl shell mcadmin@ -- journalctl --user-unit minecraft-server -f
|
||||
```
|
||||
|
||||
## Management
|
||||
## References
|
||||
|
||||
```bash
|
||||
make status # Check server status
|
||||
make logs # View recent logs
|
||||
make restart # Restart the server
|
||||
make stop # Stop the server
|
||||
make start # Start the server
|
||||
make backup # Run backup immediately
|
||||
```
|
||||
- [Ansible Documentation](https://docs.ansible.com/)
|
||||
- [Fedora Project](https://fedoraproject.org/)
|
||||
- [Podman](https://podman.io/)
|
||||
- [Podman Quadlet (systemd.unit) man page](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)
|
||||
- [Rootless Podman tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md)
|
||||
- [itzg/docker-minecraft-server](https://github.com/itzg/docker-minecraft-server)
|
||||
- [GeyserMC — Bedrock ↔ Java bridge](https://geysermc.dev/)
|
||||
- [GeyserMC/Geyser](https://github.com/GeyserMC/Geyser)
|
||||
- [Geyser docker image](https://github.com/GeyserMC/geyser-docker)
|
||||
- [Firewalld](https://firewalld.org/)
|
||||
- [fail2ban](https://github.com/fail2ban/fail2ban)
|
||||
- [SELinux quick docs (Fedora)](https://docs.fedoraproject.org/en-US/quick-docs/selinux-getting-started/)
|
||||
|
||||
## Connecting to the Server
|
||||
## License
|
||||
|
||||
In Minecraft, use:
|
||||
```
|
||||
<server-ip>:<port>
|
||||
```
|
||||
|
||||
For the default setup: `external-apps:25565`
|
||||
|
||||
## Backups
|
||||
|
||||
Backups run automatically every 6 hours (configurable in `group_vars/all.yml` under `backup.schedule`).
|
||||
|
||||
- **Location**: `/opt/minecraft/backups/`
|
||||
- **Format**: `minecraft_YYYYMMDD_HHMMSS.tar.gz`
|
||||
- **Retention**: 10 backups (configurable)
|
||||
|
||||
Restore with:
|
||||
```bash
|
||||
make restore
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
```bash
|
||||
make uninstall # Interactive confirmation
|
||||
```
|
||||
|
||||
Or manually:
|
||||
```bash
|
||||
ansible-playbook uninstall.yml --limit external-apps
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Server Type
|
||||
|
||||
Set `minecraft.server_type` to:
|
||||
- `vanilla` - Official Mojang server
|
||||
- `paper` - High-performance fork (recommended)
|
||||
- `fabric` - Modding platform
|
||||
- `forge` - Classic modding
|
||||
- `purpur` - Paper fork with more features
|
||||
|
||||
### Custom Container Image
|
||||
|
||||
To use your own `Containerfile` instead of pulling a public image:
|
||||
1. Set `minecraft.use_custom_container: true` in `group_vars`
|
||||
2. The role will build and deploy a custom image
|
||||
|
||||
### Extras
|
||||
|
||||
- **RCON**: Enabled by default on port 25575. Allows remote console admin.
|
||||
- **Whitelist**: Set `minecraft.whitelist_enabled: true` to require whitelist
|
||||
- **Command Blocks**: Set `minecraft.enable_command_block: true` to enable
|
||||
[MIT](./LICENSE)
|
||||
16
ansible.cfg
16
ansible.cfg
@@ -1,18 +1,10 @@
|
||||
[defaults]
|
||||
inventory = inventory/hosts
|
||||
inventory = inventory.yml
|
||||
roles_path = roles
|
||||
remote_user = boxswapper
|
||||
host_key_checking = False
|
||||
retry_files_enabled = False
|
||||
interpreter_python = auto_silent
|
||||
stdout_callback = yaml
|
||||
timeout = 30
|
||||
|
||||
[privilege_escalation]
|
||||
become = True
|
||||
become_method = sudo
|
||||
become_user = root
|
||||
become_ask_pass = False
|
||||
gathering = smart
|
||||
|
||||
[ssh_connection]
|
||||
pipelining = True
|
||||
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ServerAliveInterval=30
|
||||
pipelining = True
|
||||
@@ -1,162 +1,31 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# Minecraft Podman Deployment - Global Variables
|
||||
# =============================================================================
|
||||
# All variables defined here can be overridden in host_vars/<hostname>.yml
|
||||
# or passed at runtime with -e "variable=value"
|
||||
# ---------------------------------------------------------------------------
|
||||
# Variables shared by every host in the inventory.
|
||||
# Role defaults live in roles/<role>/defaults/main.yml and are overridden here.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Server Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
minecraft:
|
||||
# Server name - used for systemd service naming and container name
|
||||
server_name: "minecraft"
|
||||
# Administrative non-root user created by the auth role.
|
||||
# This is the account the Minecraft containers run under (rootless Podman).
|
||||
admin_user: mcadmin
|
||||
|
||||
# Minecraft version (latest, 1.21.4, 1.20.6, etc.)
|
||||
version: "latest"
|
||||
# SSH public key installed for {{ admin_user }}. MUST be set before first run
|
||||
# or you will lock yourself out once ssh_harden disables password auth.
|
||||
# Put your real key in host_vars/external-apps.yml.
|
||||
admin_user_ssh_key: ""
|
||||
|
||||
# Server type: "vanilla", "paper", "fabric", "forge", "purpur"
|
||||
server_type: "paper"
|
||||
# SSH daemon port (firewall + ssh_harden will both use this).
|
||||
ssh_port: 22
|
||||
|
||||
# Memory allocation for Java (e.g., "4G", "8G", "2G")
|
||||
java_memory: "4G"
|
||||
# Minecraft Java server port exposed to the internet.
|
||||
minecraft_port: 25565
|
||||
minecraft_udp: false
|
||||
|
||||
# Java JVM arguments (additional flags beyond memory)
|
||||
java_args: "-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200"
|
||||
# Wher the Minecraft world/config data lives on the host.
|
||||
minecraft_data_dir: /srv/minecraft
|
||||
|
||||
# Maximum players allowed
|
||||
max_players: 20
|
||||
|
||||
# Server port (default Minecraft port: 25565)
|
||||
port: 25565
|
||||
|
||||
# RCON port (for remote console access)
|
||||
rcon_port: 25575
|
||||
|
||||
# Enable RCON (useful for management)
|
||||
rcon_enabled: true
|
||||
|
||||
# RCON password (CHANGE THIS!)
|
||||
rcon_password: "CHANGE_ME_NOW"
|
||||
|
||||
# Server MOTD (Message of the Day)
|
||||
motd: "Welcome to the Minecraft Server!"
|
||||
|
||||
# Game mode: survival, creative, adventure, spectator
|
||||
game_mode: "survival"
|
||||
|
||||
# Difficulty: peaceful, easy, normal, hard
|
||||
difficulty: "normal"
|
||||
|
||||
# Enable whitelist
|
||||
whitelist_enabled: false
|
||||
|
||||
# Allow command blocks
|
||||
enable_command_block: false
|
||||
|
||||
# View distance (chunks, max 32)
|
||||
view_distance: 10
|
||||
|
||||
# Simulation distance (chunks)
|
||||
simulation_distance: 6
|
||||
|
||||
# Online mode (true = requires Mojang account verification)
|
||||
online_mode: true
|
||||
|
||||
# Max world size (blocks, default 29999984)
|
||||
max_world_size: 29999984
|
||||
|
||||
# Spawn protection (blocks around spawn that can't be modified)
|
||||
spawn_protection: 16
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Podman Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
podman:
|
||||
# Container image (itzg/minecraft-server is highly configurable)
|
||||
image: "itzg/minecraft-server"
|
||||
|
||||
# Image tag (usually the server type)
|
||||
image_tag: "latest"
|
||||
|
||||
# Container restart policy: always, unless-stopped, on-failure, no
|
||||
restart_policy: "unless-stopped"
|
||||
|
||||
# Container memory limit (leave empty for unlimited)
|
||||
memory_limit: ""
|
||||
|
||||
# CPU shares (relative weight)
|
||||
cpu_shares: 1024
|
||||
|
||||
# Container log driver
|
||||
log_driver: "journald"
|
||||
|
||||
# Container log max size
|
||||
log_max_size: "50m"
|
||||
|
||||
# Container log max file count
|
||||
log_max_file: "3"
|
||||
|
||||
# Extra volumes to mount (format: "host_path:container_path:options")
|
||||
extra_volumes: []
|
||||
|
||||
# Container labels
|
||||
labels:
|
||||
managed-by: "ansible"
|
||||
app: "minecraft"
|
||||
|
||||
# Network mode (host, bridge, or slirp4netns)
|
||||
network_mode: "host"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Path Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
paths:
|
||||
# Base installation directory
|
||||
base_dir: "/opt/minecraft"
|
||||
|
||||
# Data directory (where world, configs, etc. are stored)
|
||||
data_dir: "/opt/minecraft/data"
|
||||
|
||||
# Backup directory
|
||||
backup_dir: "/opt/minecraft/backups"
|
||||
|
||||
# Systemd service directory
|
||||
systemd_dir: "/etc/systemd/system"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Backup Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
backup:
|
||||
# Enable automatic backups
|
||||
enabled: true
|
||||
|
||||
# Backup schedule (cron format)
|
||||
schedule: "0 */6 * * *" # Every 6 hours
|
||||
|
||||
# Number of backups to retain
|
||||
retention_count: 10
|
||||
|
||||
# Compress backups with gzip
|
||||
compress: true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Firewall Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
firewall:
|
||||
# Open Minecraft ports in firewalld
|
||||
enabled: true
|
||||
|
||||
# Additional ports to open (for mods, voice chat, etc.)
|
||||
additional_ports:
|
||||
- "19132/udp" # Bedrock (if needed)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Monitoring (Optional)
|
||||
# -----------------------------------------------------------------------------
|
||||
monitoring:
|
||||
# Enable basic health checks
|
||||
enabled: false
|
||||
|
||||
# Health check interval in seconds
|
||||
interval: 300
|
||||
# Geyser - Bedrock protocol bridge. Opens the Bedrock UDP port in the
|
||||
# firewall (roles/firewall) and deploys the geyser.container Quadlet unit
|
||||
# (roles/minecraft) next to the Java server.
|
||||
enable_geyser: true
|
||||
# Bedrock UDP port Geyser publishes (host + container, must match).
|
||||
geyser_port: 19132
|
||||
12
harden_fedora_minecraft.yml
Normal file
12
harden_fedora_minecraft.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
- name: Harden Fedora and deploy Minecraft server
|
||||
hosts: external-apps
|
||||
become: true
|
||||
roles:
|
||||
- packages
|
||||
- firewall
|
||||
- selinux
|
||||
- auth
|
||||
- ssh_harden
|
||||
- podman
|
||||
- minecraft
|
||||
32
host_vars/external-apps.yml
Normal file
32
host_vars/external-apps.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
# Host-specific variables for the external-apps server.
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Set ansible_host to the real public IP or DNS name.
|
||||
# 2. Put your real SSH public key in admin_user_ssh_key.
|
||||
# 3. Adjust Minecraft / Geyser settings as needed.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# The server's real address (overrides the placeholder in inventory.yml).
|
||||
ansible_host: 203.0.113.10
|
||||
|
||||
# Your SSH public key. This is what lets you log in after the playbook
|
||||
# disables password authentication. Replace the placeholder below.
|
||||
admin_user_ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... your-public-key-here"
|
||||
|
||||
# Minecraft server tuning (see roles/minecraft/defaults/main.yml for all options).
|
||||
minecraft_memory: 2G # RAM allocated to the JVM
|
||||
minecraft_version: LATEST # or a specific version, e.g. "1.21.1"
|
||||
minecraft_server_type: PAPER # PAPER | VANILLA | FABRIC | FORGE | ...
|
||||
minecraft_difficulty: normal
|
||||
minecraft_max_players: "20"
|
||||
minecraft_view_distance: "10"
|
||||
minecraft_motd: "Hardened Fedora 42 Minecraft Server"
|
||||
|
||||
# Comma-separated list of Minecraft usernames allowed to join.
|
||||
# Empty string means the whitelist is disabled.
|
||||
minecraft_whitelist: ""
|
||||
|
||||
# Geyser - Bedrock protocol bridge (bedrock clients join the Java world).
|
||||
# enable_geyser: true # set to false to skip the Geyser container
|
||||
# geyser_port: 19132 # Bedrock UDP port (firewall + container)
|
||||
# geyser_auth_type: online # online | offline (offline requires Floodgate)
|
||||
12
inventory.yml
Normal file
12
inventory.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
all:
|
||||
children:
|
||||
minecraft_servers:
|
||||
hosts:
|
||||
external-apps:
|
||||
# Replace with the server's real IP address or DNS name.
|
||||
ansible_host: 203.0.113.10
|
||||
# Initial connection user (must have sudo on first run).
|
||||
# After the playbook creates the admin user, switch to that user.
|
||||
ansible_user: root
|
||||
ansible_become: true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Ansible Inventory - External Minecraft Server
|
||||
# Managed by Sisyphus for Minecraft Podman Deployment
|
||||
|
||||
[minecraft]
|
||||
external-apps ansible_host=external-apps ansible_user=boxswapper
|
||||
|
||||
[minecraft:vars]
|
||||
ansible_python_interpreter=/usr/bin/python3
|
||||
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
|
||||
@@ -1,9 +1,3 @@
|
||||
---
|
||||
# Ansible Galaxy collections required by this deployment
|
||||
# Install with: ansible-galaxy collection install -r requirements.yml
|
||||
|
||||
collections:
|
||||
- name: ansible.posix
|
||||
version: ">=1.5.0"
|
||||
- name: containers.podman
|
||||
version: ">=1.10.0"
|
||||
- ansible.posix
|
||||
14
roles/auth/defaults/main.yml
Normal file
14
roles/auth/defaults/main.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
admin_user: mcadmin
|
||||
admin_user_ssh_key: ""
|
||||
admin_user_password_hash: "" # empty / unset keeps the account password-locked
|
||||
|
||||
auth_minlen: 14
|
||||
auth_dcredit: -1
|
||||
auth_ucredit: -1
|
||||
auth_lcredit: -1
|
||||
auth_ocredit: -1
|
||||
auth_password_max_days: 90
|
||||
auth_password_min_days: 7
|
||||
auth_password_warn_age: 7
|
||||
auth_lock_root_password: true
|
||||
70
roles/auth/tasks/main.yml
Normal file
70
roles/auth/tasks/main.yml
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
# Create the administrative user, install its SSH key, enforce password
|
||||
# policy, and lock the root account.
|
||||
|
||||
- name: Create the administrative user
|
||||
ansible.builtin.user:
|
||||
name: "{{ admin_user }}"
|
||||
groups: wheel
|
||||
append: true
|
||||
create_home: true
|
||||
home: "/home/{{ admin_user }}"
|
||||
shell: /bin/bash
|
||||
comment: "Minecraft server administrator"
|
||||
password: "{{ admin_user_password_hash | default('!') }}"
|
||||
password_expire_max: "{{ auth_password_max_days }}"
|
||||
password_expire_min: "{{ auth_password_min_days }}"
|
||||
state: present
|
||||
|
||||
- name: Install the admin user's SSH public key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ admin_user }}"
|
||||
state: present
|
||||
key: "{{ admin_user_ssh_key }}"
|
||||
when: admin_user_ssh_key | length > 0
|
||||
|
||||
- name: Allow the admin user passwordless sudo
|
||||
ansible.builtin.copy:
|
||||
content: "{{ admin_user }} ALL=(ALL) NOPASSWD: ALL\n"
|
||||
dest: "/etc/sudoers.d/{{ admin_user }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0440"
|
||||
validate: /usr/sbin/visudo -cf %s
|
||||
|
||||
- name: Enforce password complexity settings
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/security/pwquality.conf
|
||||
regexp: '^#?\s*{{ item.key }}\s*='
|
||||
line: "{{ item.key }} = {{ item.value }}"
|
||||
loop:
|
||||
- { key: minlen, value: "{{ auth_minlen }}" }
|
||||
- { key: dcredit, value: "{{ auth_dcredit }}" }
|
||||
- { key: ucredit, value: "{{ auth_ucredit }}" }
|
||||
- { key: lcredit, value: "{{ auth_lcredit }}" }
|
||||
- { key: ocredit, value: "{{ auth_ocredit }}" }
|
||||
|
||||
- name: Enforce global password aging defaults
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/login.defs
|
||||
regexp: '^PASS_MAX_DAYS\s+'
|
||||
line: "PASS_MAX_DAYS {{ auth_password_max_days }}"
|
||||
|
||||
- name: Enforce minimum days between password changes
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/login.defs
|
||||
regexp: '^PASS_MIN_DAYS\s+'
|
||||
line: "PASS_MIN_DAYS {{ auth_password_min_days }}"
|
||||
|
||||
- name: Enforce password change warning
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/login.defs
|
||||
regexp: '^PASS_WARN_AGE\s+'
|
||||
line: "PASS_WARN_AGE {{ auth_password_warn_age }}"
|
||||
|
||||
- name: Lock the root account's password
|
||||
ansible.builtin.user:
|
||||
name: root
|
||||
password: '!'
|
||||
state: present
|
||||
when: auth_lock_root_password | bool
|
||||
6
roles/firewall/defaults/main.yml
Normal file
6
roles/firewall/defaults/main.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
firewall_zone: public
|
||||
firewall_zone_target: DROP
|
||||
firewall_allow_ping: true
|
||||
enable_geyser: true
|
||||
geyser_port: 19132
|
||||
68
roles/firewall/tasks/main.yml
Normal file
68
roles/firewall/tasks/main.yml
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
# Firewall policy: default DROP, only SSH and the Minecraft port are exposed.
|
||||
|
||||
- name: Ensure firewalld is running and enabled
|
||||
ansible.builtin.systemd:
|
||||
name: firewalld
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Set the default zone target to DROP
|
||||
ansible.posix.firewalld:
|
||||
zone: "{{ firewall_zone }}"
|
||||
target: "{{ firewall_zone_target }}"
|
||||
permanent: true
|
||||
immediate: true
|
||||
state: present
|
||||
|
||||
- name: Allow SSH service
|
||||
ansible.posix.firewalld:
|
||||
zone: "{{ firewall_zone }}"
|
||||
service: ssh
|
||||
permanent: true
|
||||
immediate: true
|
||||
state: enabled
|
||||
|
||||
- name: Allow custom SSH port
|
||||
ansible.posix.firewalld:
|
||||
zone: "{{ firewall_zone }}"
|
||||
port: "{{ ssh_port }}/tcp"
|
||||
permanent: true
|
||||
immediate: true
|
||||
state: enabled
|
||||
when: ssh_port != 22
|
||||
|
||||
- name: Allow Minecraft server port (TCP)
|
||||
ansible.posix.firewalld:
|
||||
zone: "{{ firewall_zone }}"
|
||||
port: "{{ minecraft_port }}/tcp"
|
||||
permanent: true
|
||||
immediate: true
|
||||
state: enabled
|
||||
|
||||
- name: Allow Minecraft server port (UDP)
|
||||
ansible.posix.firewalld:
|
||||
zone: "{{ firewall_zone }}"
|
||||
port: "{{ minecraft_port }}/udp"
|
||||
permanent: true
|
||||
immediate: true
|
||||
state: enabled
|
||||
when: minecraft_udp | bool
|
||||
|
||||
- name: Allow Geyser Bedrock port (UDP)
|
||||
ansible.posix.firewalld:
|
||||
zone: "{{ firewall_zone }}"
|
||||
port: "{{ geyser_port }}/udp"
|
||||
permanent: true
|
||||
immediate: true
|
||||
state: enabled
|
||||
when: enable_geyser | bool
|
||||
|
||||
- name: Allow ICMP echo (ping) for basic connectivity checks
|
||||
ansible.posix.firewalld:
|
||||
zone: "{{ firewall_zone }}"
|
||||
icmp_block_inversion: true
|
||||
permanent: true
|
||||
immediate: true
|
||||
state: present
|
||||
when: firewall_allow_ping | bool
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# Minecraft Podman Deployment - Role Defaults
|
||||
# =============================================================================
|
||||
# These are the default values for this role. They can be overridden by
|
||||
# group_vars/all.yml, host_vars/<host>.yml, or -e at runtime.
|
||||
# =============================================================================
|
||||
|
||||
# User to run the Minecraft container as
|
||||
minecraft_user: "minecraft"
|
||||
|
||||
# Podman container name
|
||||
podman_container_name: "minecraft"
|
||||
|
||||
# Whether to use a custom Containerfile (true) or pull pre-built image (false)
|
||||
minecraft_use_custom_container: "{{ minecraft.use_custom_container | default(false) }}"
|
||||
|
||||
# Custom image name (only used if use_custom_container is true)
|
||||
minecraft_custom_image_name: "minecraft-custom"
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# Minecraft Podman Deployment - Handlers
|
||||
# =============================================================================
|
||||
|
||||
- name: restart minecraft
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ minecraft.server_name }}.service"
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
|
||||
- name: reload systemd
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: restart firewalld
|
||||
ansible.builtin.service:
|
||||
name: firewalld
|
||||
state: restarted
|
||||
|
||||
- name: reload backup timer
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ minecraft.server_name }}-backup.timer"
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
@@ -1,423 +0,0 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# Minecraft Podman Deployment - Main Tasks
|
||||
# =============================================================================
|
||||
# This role deploys a Minecraft server using Podman on Fedora
|
||||
# with systemd integration for service management
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# System Preparation
|
||||
# -----------------------------------------------------------------------------
|
||||
- name: System Preparation
|
||||
block:
|
||||
- name: Update DNF cache
|
||||
ansible.builtin.dnf:
|
||||
update_cache: true
|
||||
state: present
|
||||
tags: [install, packages]
|
||||
|
||||
- name: Install required packages
|
||||
ansible.builtin.dnf:
|
||||
name:
|
||||
- podman
|
||||
- podman-plugins # Podman plugins (Netavark, etc.)
|
||||
- fuse-overlayfs # Required for rootless storage
|
||||
- slirp4netns # Required for rootless networking
|
||||
- systemd-container # Systemd integration
|
||||
- firewalld
|
||||
- python3-firewall # For ansible firewalld module
|
||||
- python3-podman # For ansible podman modules
|
||||
- tar # For backups
|
||||
- gzip # For compressed backups
|
||||
- curl # For health checks
|
||||
state: present
|
||||
tags: [install, packages]
|
||||
|
||||
- name: Ensure Podman is installed
|
||||
ansible.builtin.command: podman --version
|
||||
register: podman_version
|
||||
changed_when: false
|
||||
tags: [install, verify]
|
||||
|
||||
- name: Display Podman version
|
||||
ansible.builtin.debug:
|
||||
msg: "Podman {{ podman_version.stdout }} installed"
|
||||
tags: [install, verify]
|
||||
|
||||
tags: [install, packages]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# User and Permissions
|
||||
# -----------------------------------------------------------------------------
|
||||
- name: User Configuration
|
||||
block:
|
||||
- name: Create minecraft system user
|
||||
ansible.builtin.user:
|
||||
name: "{{ minecraft_user }}"
|
||||
system: true
|
||||
shell: /bin/bash
|
||||
home: "{{ paths.base_dir }}"
|
||||
create_home: false
|
||||
comment: "Minecraft Server User"
|
||||
when: minecraft_user != "root"
|
||||
tags: [install, user]
|
||||
|
||||
- name: Configure subuid for rootless podman
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/subuid
|
||||
line: "{{ minecraft_user }}:100000:65536"
|
||||
regexp: "^{{ minecraft_user }}:"
|
||||
state: present
|
||||
create: true
|
||||
mode: "0644"
|
||||
when: minecraft_user != "root"
|
||||
tags: [install, user]
|
||||
|
||||
- name: Configure subgid for rootless podman
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/subgid
|
||||
line: "{{ minecraft_user }}:100000:65536"
|
||||
regexp: "^{{ minecraft_user }}:"
|
||||
state: present
|
||||
create: true
|
||||
mode: "0644"
|
||||
when: minecraft_user != "root"
|
||||
tags: [install, user]
|
||||
|
||||
- name: Create user config directory for podman
|
||||
ansible.builtin.file:
|
||||
path: "{{ paths.base_dir }}/.config/containers"
|
||||
state: directory
|
||||
owner: "{{ minecraft_user }}"
|
||||
group: "{{ minecraft_user }}"
|
||||
mode: "0755"
|
||||
when: minecraft_user != "root"
|
||||
tags: [install, user]
|
||||
|
||||
- name: Create user local share for podman storage
|
||||
ansible.builtin.file:
|
||||
path: "{{ paths.base_dir }}/.local/share/containers"
|
||||
state: directory
|
||||
owner: "{{ minecraft_user }}"
|
||||
group: "{{ minecraft_user }}"
|
||||
mode: "0755"
|
||||
when: minecraft_user != "root"
|
||||
tags: [install, user]
|
||||
|
||||
- name: Ensure systemd lingering is enabled for user
|
||||
ansible.builtin.command: "loginctl enable-linger {{ minecraft_user }}"
|
||||
changed_when: false
|
||||
when: minecraft_user != "root"
|
||||
tags: [install, user]
|
||||
|
||||
tags: [install]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Directory Structure
|
||||
# -----------------------------------------------------------------------------
|
||||
- name: Directory Setup
|
||||
block:
|
||||
- name: Create base directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ minecraft_user }}"
|
||||
group: "{{ minecraft_user }}"
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ paths.base_dir }}"
|
||||
- "{{ paths.data_dir }}"
|
||||
- "{{ paths.backup_dir }}"
|
||||
- "{{ paths.data_dir }}/logs"
|
||||
- "{{ paths.data_dir }}/world"
|
||||
tags: [install, directories]
|
||||
|
||||
- name: Create Containerfile directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ paths.base_dir }}/container"
|
||||
state: directory
|
||||
owner: "{{ minecraft_user }}"
|
||||
group: "{{ minecraft_user }}"
|
||||
mode: "0755"
|
||||
tags: [install, container]
|
||||
|
||||
tags: [install]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Container Build (if using custom Containerfile)
|
||||
# -----------------------------------------------------------------------------
|
||||
- name: Container Build
|
||||
block:
|
||||
- name: Copy Containerfile
|
||||
ansible.builtin.template:
|
||||
src: Containerfile.j2
|
||||
dest: "{{ paths.base_dir }}/container/Containerfile"
|
||||
owner: "{{ minecraft_user }}"
|
||||
group: "{{ minecraft_user }}"
|
||||
mode: "0644"
|
||||
when: minecraft.use_custom_container | default(false)
|
||||
tags: [install, container]
|
||||
|
||||
- name: Build custom Minecraft container image
|
||||
containers.podman.podman_image:
|
||||
name: "{{ minecraft.custom_image_name | default('minecraft-custom') }}"
|
||||
build:
|
||||
path: "{{ paths.base_dir }}/container/"
|
||||
file: Containerfile
|
||||
tag: "{{ minecraft.version }}"
|
||||
state: present
|
||||
when: minecraft.use_custom_container | default(false)
|
||||
tags: [install, container]
|
||||
|
||||
tags: [install]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Server Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
- name: Server Configuration
|
||||
block:
|
||||
- name: Deploy server.properties
|
||||
ansible.builtin.template:
|
||||
src: server.properties.j2
|
||||
dest: "{{ paths.data_dir }}/server.properties"
|
||||
owner: "{{ minecraft_user }}"
|
||||
group: "{{ minecraft_user }}"
|
||||
mode: "0644"
|
||||
notify: restart minecraft
|
||||
tags: [config]
|
||||
|
||||
- name: Deploy eula.txt
|
||||
ansible.builtin.copy:
|
||||
content: "eula=true\n"
|
||||
dest: "{{ paths.data_dir }}/eula.txt"
|
||||
owner: "{{ minecraft_user }}"
|
||||
group: "{{ minecraft_user }}"
|
||||
mode: "0644"
|
||||
tags: [config]
|
||||
|
||||
- name: Deploy environment variables
|
||||
ansible.builtin.template:
|
||||
src: minecraft.env.j2
|
||||
dest: "{{ paths.base_dir }}/minecraft.env"
|
||||
owner: "{{ minecraft_user }}"
|
||||
group: "{{ minecraft_user }}"
|
||||
mode: "0600"
|
||||
notify: restart minecraft
|
||||
tags: [config]
|
||||
|
||||
- name: Deploy JVM options file
|
||||
ansible.builtin.template:
|
||||
src: jvm.opts.j2
|
||||
dest: "{{ paths.data_dir }}/jvm.opts"
|
||||
owner: "{{ minecraft_user }}"
|
||||
group: "{{ minecraft_user }}"
|
||||
mode: "0644"
|
||||
when: minecraft.jvm_opts_file | default(false)
|
||||
notify: restart minecraft
|
||||
tags: [config]
|
||||
|
||||
tags: [config]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Firewall Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
- name: Firewall Configuration
|
||||
block:
|
||||
- name: Ensure firewalld is running
|
||||
ansible.builtin.systemd:
|
||||
name: firewalld
|
||||
state: started
|
||||
enabled: true
|
||||
tags: [firewall]
|
||||
|
||||
- name: Open Minecraft port
|
||||
ansible.posix.firewalld:
|
||||
port: "{{ minecraft.port }}/tcp"
|
||||
permanent: true
|
||||
immediate: true
|
||||
state: enabled
|
||||
tags: [firewall]
|
||||
|
||||
- name: Open RCON port
|
||||
ansible.posix.firewalld:
|
||||
port: "{{ minecraft.rcon_port }}/tcp"
|
||||
permanent: true
|
||||
immediate: true
|
||||
state: enabled
|
||||
when: minecraft.rcon_enabled
|
||||
tags: [firewall]
|
||||
|
||||
- name: Open additional firewall ports
|
||||
ansible.posix.firewalld:
|
||||
port: "{{ item }}"
|
||||
permanent: true
|
||||
immediate: true
|
||||
state: enabled
|
||||
loop: "{{ firewall.additional_ports | default([]) }}"
|
||||
tags: [firewall]
|
||||
|
||||
tags: [firewall]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Systemd Service
|
||||
# -----------------------------------------------------------------------------
|
||||
- name: Systemd Service Configuration
|
||||
block:
|
||||
- name: Deploy systemd service unit
|
||||
ansible.builtin.template:
|
||||
src: minecraft.service.j2
|
||||
dest: "{{ paths.systemd_dir }}/{{ minecraft.server_name }}.service"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify:
|
||||
- reload systemd
|
||||
- restart minecraft
|
||||
tags: [service]
|
||||
|
||||
- name: Reload systemd daemon
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
tags: [service]
|
||||
|
||||
- name: Enable and start Minecraft service
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ minecraft.server_name }}.service"
|
||||
state: started
|
||||
enabled: true
|
||||
tags: [service]
|
||||
|
||||
tags: [service]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Backup Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
- name: Backup Configuration
|
||||
block:
|
||||
- name: Deploy backup script
|
||||
ansible.builtin.template:
|
||||
src: backup.sh.j2
|
||||
dest: "{{ paths.base_dir }}/backup.sh"
|
||||
owner: "{{ minecraft_user }}"
|
||||
group: "{{ minecraft_user }}"
|
||||
mode: "0755"
|
||||
when: backup.enabled
|
||||
tags: [backup]
|
||||
|
||||
- name: Deploy restore script
|
||||
ansible.builtin.template:
|
||||
src: restore.sh.j2
|
||||
dest: "{{ paths.base_dir }}/restore.sh"
|
||||
owner: "{{ minecraft_user }}"
|
||||
group: "{{ minecraft_user }}"
|
||||
mode: "0755"
|
||||
when: backup.enabled
|
||||
tags: [backup]
|
||||
|
||||
- name: Create systemd timer for backups
|
||||
ansible.builtin.template:
|
||||
src: minecraft-backup.timer.j2
|
||||
dest: "{{ paths.systemd_dir }}/{{ minecraft.server_name }}-backup.timer"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
when: backup.enabled
|
||||
tags: [backup]
|
||||
|
||||
- name: Create systemd service for backups
|
||||
ansible.builtin.template:
|
||||
src: minecraft-backup.service.j2
|
||||
dest: "{{ paths.systemd_dir }}/{{ minecraft.server_name }}-backup.service"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
when: backup.enabled
|
||||
tags: [backup]
|
||||
|
||||
- name: Enable backup timer
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ minecraft.server_name }}-backup.timer"
|
||||
state: started
|
||||
enabled: true
|
||||
when: backup.enabled
|
||||
tags: [backup]
|
||||
|
||||
tags: [backup]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Health Check
|
||||
# -----------------------------------------------------------------------------
|
||||
- name: Health Check Configuration
|
||||
block:
|
||||
- name: Deploy health check script
|
||||
ansible.builtin.template:
|
||||
src: health-check.sh.j2
|
||||
dest: "{{ paths.base_dir }}/health-check.sh"
|
||||
owner: "{{ minecraft_user }}"
|
||||
group: "{{ minecraft_user }}"
|
||||
mode: "0755"
|
||||
when: monitoring.enabled
|
||||
tags: [monitoring]
|
||||
|
||||
- name: Create systemd timer for health checks
|
||||
ansible.builtin.template:
|
||||
src: minecraft-health.timer.j2
|
||||
dest: "{{ paths.systemd_dir }}/{{ minecraft.server_name }}-health.timer"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
when: monitoring.enabled
|
||||
tags: [monitoring]
|
||||
|
||||
- name: Create systemd service for health checks
|
||||
ansible.builtin.template:
|
||||
src: minecraft-health.service.j2
|
||||
dest: "{{ paths.systemd_dir }}/{{ minecraft.server_name }}-health.service"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
when: monitoring.enabled
|
||||
tags: [monitoring]
|
||||
|
||||
- name: Enable health check timer
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ minecraft.server_name }}-health.timer"
|
||||
state: started
|
||||
enabled: true
|
||||
when: monitoring.enabled
|
||||
tags: [monitoring]
|
||||
|
||||
tags: [monitoring]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Final Verification
|
||||
# -----------------------------------------------------------------------------
|
||||
- name: Final Verification
|
||||
block:
|
||||
- name: Wait for Minecraft server to start
|
||||
ansible.builtin.uri:
|
||||
url: "http://localhost:{{ minecraft.port }}"
|
||||
return_content: false
|
||||
status_code: [401, 200] # Minecraft responds to HTTP with 401
|
||||
register: server_check
|
||||
retries: 30
|
||||
delay: 5
|
||||
until: server_check.status in [401, 200]
|
||||
ignore_errors: true
|
||||
tags: [verify]
|
||||
|
||||
- name: Check service status
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ minecraft.server_name }}.service"
|
||||
register: service_status
|
||||
tags: [verify]
|
||||
|
||||
- name: Display verification results
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
Server Status: {{ 'Running' if service_status.status.ActiveState == 'active' else 'Stopped' }}
|
||||
Service: {{ service_status.status.SubState }}
|
||||
Port {{ minecraft.port }}: {{ 'Open' if server_check is succeeded else 'Closed' }}
|
||||
tags: [verify]
|
||||
|
||||
tags: [verify]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Custom Minecraft Server Containerfile
|
||||
# Deployed by Ansible - only used if minecraft.use_custom_container is true
|
||||
|
||||
# Base image
|
||||
FROM itzg/minecraft-server:java{{ minecraft.java_version | default('21') }}
|
||||
|
||||
# Custom server type
|
||||
ENV TYPE={{ minecraft.server_type | upper }}
|
||||
ENV VERSION={{ minecraft.version }}
|
||||
|
||||
# Custom plugins/mods directory (uncomment if using mods)
|
||||
# VOLUME /data/mods
|
||||
# VOLUME /data/plugins
|
||||
# VOLUME /data/config
|
||||
|
||||
# Custom entrypoint overrides
|
||||
COPY server.properties /tmp/server.properties
|
||||
RUN mkdir -p /data && cp /tmp/server.properties /data/server.properties
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD curl -f http://localhost:{{ minecraft.port }} || exit 1
|
||||
|
||||
# Expose ports
|
||||
EXPOSE {{ minecraft.port }}/tcp
|
||||
{% if minecraft.rcon_enabled %}
|
||||
EXPOSE {{ minecraft.rcon_port }}/tcp
|
||||
{% endif %}
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Minecraft Server Backup Script
|
||||
# Deployed by Ansible - {{ ansible_date_time.iso8601 if ansible_date_time is defined else 'managed' }}
|
||||
#
|
||||
# Creates rolling backups of the Minecraft world and data directories.
|
||||
# Prunes old backups based on retention count.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
DATA_DIR="{{ paths.data_dir }}"
|
||||
BACKUP_DIR="{{ paths.backup_dir }}"
|
||||
RETENTION_COUNT="{{ backup.retention_count | default(10) }}"
|
||||
COMPRESS="{{ backup.compress | default('true') }}"
|
||||
SERVER_NAME="{{ minecraft.server_name }}"
|
||||
|
||||
# Timestamp for this backup
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
# Log function
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
# Safety checks
|
||||
if [ ! -d "$DATA_DIR" ]; then
|
||||
log "ERROR: Data directory $DATA_DIR does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$BACKUP_DIR" ]; then
|
||||
log "Creating backup directory: $BACKUP_DIR"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
fi
|
||||
|
||||
# Create backup using tar
|
||||
BACKUP_FILE="${BACKUP_DIR}/${SERVER_NAME}_${TIMESTAMP}"
|
||||
|
||||
log "Starting backup of $DATA_DIR..."
|
||||
|
||||
# Use atomic save if RCON is enabled
|
||||
USE_RCON="{{ minecraft.rcon_enabled | default('false') }}"
|
||||
if [ "$USE_RCON" = "true" ]; then
|
||||
log "Using RCON for world save..."
|
||||
# Send save-all command via rcon-cli or telnet
|
||||
# This is a best-effort operation - fall back to tar if it fails
|
||||
if command -v rcon-cli >/dev/null 2>&1; then
|
||||
rcon-cli -a localhost -p {{ minecraft.rcon_port }} -P {{ minecraft.rcon_password }} save-all || true
|
||||
sleep 5
|
||||
elif command -v mcrcon >/dev/null 2>&1; then
|
||||
mcrcon -H localhost -P {{ minecraft.rcon_port }} -p {{ minecraft.rcon_password }} "save-all" || true
|
||||
sleep 5
|
||||
else
|
||||
log "RCON client not found, skipping save-all command (backup may be inconsistent)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Deterministic backup using tar
|
||||
if [ "$COMPRESS" = "true" ]; then
|
||||
BACKUP_FILE="${BACKUP_FILE}.tar.gz"
|
||||
log "Creating compressed backup: $BACKUP_FILE"
|
||||
tar --warning=no-file-changed -czf "$BACKUP_FILE" \
|
||||
--exclude="$DATA_DIR/logs/*.gz" \
|
||||
-C "$(dirname $DATA_DIR)" "$(basename $DATA_DIR)"
|
||||
else
|
||||
BACKUP_FILE="${BACKUP_FILE}.tar"
|
||||
log "Creating uncompressed backup: $BACKUP_FILE"
|
||||
tar --warning=no-file-changed -cf "$BACKUP_FILE" \
|
||||
--exclude="$DATA_DIR/logs/*.gz" \
|
||||
-C "$(dirname $DATA_DIR)" "$(basename $DATA_DIR)"
|
||||
fi
|
||||
|
||||
log "Backup created successfully: $BACKUP_FILE"
|
||||
|
||||
# Prune old backups, retaining the most recent N
|
||||
log "Pruning old backups (keeping $RETENTION_COUNT)..."
|
||||
|
||||
# Count backups (whether .tar or .tar.gz)
|
||||
BACKUP_COUNT=$(ls "${BACKUP_DIR}/"${SERVER_NAME}_*.tar* 2>/dev/null | wc -l)
|
||||
OVERFLOW=$((BACKUP_COUNT - RETENTION_COUNT))
|
||||
|
||||
if [ "$OVERFLOW" -gt 0 ]; then
|
||||
# Remove oldest backups
|
||||
ls -t "${BACKUP_DIR}/"${SERVER_NAME}_*.tar* 2>/dev/null | tail -n "$OVERFLOW" | while read -r old_backup; do
|
||||
log "Removing old backup: $old_backup"
|
||||
rm -f "$old_backup"
|
||||
done
|
||||
else
|
||||
log "Backup count ($BACKUP_COUNT) is within retention limit ($RETENTION_COUNT)"
|
||||
fi
|
||||
|
||||
log "Backup completed at $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
log "Current backups:"
|
||||
ls -lh "${BACKUP_DIR}/" 2>/dev/null || true
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Minecraft Server Health Check Script
|
||||
# Deployed by Ansible - managed file
|
||||
#
|
||||
# Checks that the Minecraft server is running and responding.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SERVER_NAME="{{ minecraft.server_name }}"
|
||||
PORT="{{ minecraft.port }}"
|
||||
CONTAINER_NAME="{{ podman_container_name }}"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "$(date '+%Y-%m-%d %H:%M:%S') $*"
|
||||
}
|
||||
|
||||
# Check 1: Is the systemd service active?
|
||||
SERVICE_STATUS=$(systemctl is-active "${SERVER_NAME}" 2>/dev/null || echo "inactive")
|
||||
if [ "$SERVICE_STATUS" != "active" ]; then
|
||||
log "${RED}[FAIL]${NC} Systemd service ${SERVER_NAME} is not active (status: ${SERVICE_STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
log "${GREEN}[OK]${NC} Systemd service ${SERVER_NAME} is active"
|
||||
|
||||
# Check 2: Is the container running?
|
||||
if ! podman ps --format '{% raw %}{{.Names}} {{.Status}}{% endraw %}' | grep -q "${CONTAINER_NAME}"; then
|
||||
log "${RED}[FAIL]${NC} Container ${CONTAINER_NAME} is not running"
|
||||
exit 1
|
||||
fi
|
||||
log "${GREEN}[OK]${NC} Container ${CONTAINER_NAME} is running"
|
||||
|
||||
# Check 3: Is the Minecraft port responding?
|
||||
# Minecraft responds to HTTP with 401 "It's a teapot" or similar
|
||||
if curl -s -o /dev/null -m 5 http://localhost:${PORT} 2>/dev/null; then
|
||||
log "${GREEN}[OK]${NC} Port ${PORT} is responding"
|
||||
elif curl -s -o /dev/null -m 5 -w "%{http_code}" http://localhost:${PORT} 2>/dev/null | grep -q "401"; then
|
||||
log "${GREEN}[OK]${NC} Port ${PORT} is responding (401 is expected for Minecraft)"
|
||||
else
|
||||
# Check if port is at least listening
|
||||
if ss -tln | grep -q ":${PORT}"; then
|
||||
log "${YELLOW}[WARN]${NC} Port ${PORT} is listening but not responding to HTTP (may be normal)"
|
||||
else
|
||||
log "${RED}[FAIL]${NC} Port ${PORT} is not listening"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check 4: Check recent logs for errors
|
||||
LOG_TAIL=$(journalctl -u "${SERVER_NAME}" --no-pager -n 50 2>/dev/null | grep -i "error\|fatal\|crash" | tail -n 5)
|
||||
if [ -n "$LOG_TAIL" ]; then
|
||||
log "${YELLOW}[WARN]${NC} Recent errors found in logs:"
|
||||
echo "$LOG_TAIL"
|
||||
else
|
||||
log "${GREEN}[OK]${NC} No recent errors in logs"
|
||||
fi
|
||||
|
||||
log "${GREEN}Health check completed successfully${NC}"
|
||||
exit 0
|
||||
@@ -1,4 +0,0 @@
|
||||
# JVM Options file for Minecraft server
|
||||
# Deployed by Ansible - manage via java_args in group_vars/all.yml
|
||||
|
||||
{{ minecraft.java_args | default('-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200') }}
|
||||
@@ -1,16 +0,0 @@
|
||||
# Systemd service for Minecraft backup
|
||||
# Deployed by Ansible
|
||||
|
||||
[Unit]
|
||||
Description=Minecraft Server Backup
|
||||
Requires={{ minecraft.server_name }}.service
|
||||
After={{ minecraft.server_name }}.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User={{ minecraft_user }}
|
||||
Group={{ minecraft_user }}
|
||||
ExecStart={{ paths.base_dir }}/backup.sh
|
||||
|
||||
# Timeout for large worlds
|
||||
TimeoutStartSec=3600
|
||||
@@ -1,15 +0,0 @@
|
||||
# Systemd timer for Minecraft backup
|
||||
# Deployed by Ansible
|
||||
|
||||
[Unit]
|
||||
Description=Minecraft Server Backup Timer
|
||||
|
||||
[Timer]
|
||||
# Backup schedule (default: every 6 hours)
|
||||
OnCalendar={{ backup.schedule | default('*-*-* */6:00:00') }}
|
||||
|
||||
# Run the backup immediately if the machine was powered off during the scheduled time
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -1,11 +0,0 @@
|
||||
# Systemd service for Minecraft health checks
|
||||
# Deployed by Ansible
|
||||
|
||||
[Unit]
|
||||
Description=Minecraft Server Health Check
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User={{ minecraft_user }}
|
||||
Group={{ minecraft_user }}
|
||||
ExecStart={{ paths.base_dir }}/health-check.sh
|
||||
@@ -1,12 +0,0 @@
|
||||
# Systemd timer for Minecraft health checks
|
||||
# Deployed by Ansible
|
||||
|
||||
[Unit]
|
||||
Description=Minecraft Server Health Check Timer
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec={{ monitoring.interval | default(300) }}s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -1,61 +0,0 @@
|
||||
# Minecraft Server Environment Variables
|
||||
# Deployed by Ansible - Managed file, do not edit manually
|
||||
|
||||
# Version and Server Type
|
||||
TYPE={{ minecraft.server_type | upper }}
|
||||
VERSION={{ minecraft.version }}
|
||||
|
||||
# Memory
|
||||
MEMORY={{ minecraft.java_memory }}
|
||||
{% if minecraft.java_args is defined %}
|
||||
JVM_OPTS="{{ minecraft.java_args }}"
|
||||
{% endif %}
|
||||
|
||||
# Network
|
||||
PORT={{ minecraft.port }}
|
||||
{% if minecraft.rcon_enabled %}
|
||||
ENABLE_RCON=true
|
||||
RCON_PORT={{ minecraft.rcon_port }}
|
||||
RCON_PASSWORD={{ minecraft.rcon_password }}
|
||||
{% else %}
|
||||
ENABLE_RCON=false
|
||||
{% endif %}
|
||||
|
||||
# Server Settings
|
||||
MOTD={{ minecraft.motd }}
|
||||
MAX_PLAYERS={{ minecraft.max_players }}
|
||||
GAMEMODE={{ minecraft.game_mode }}
|
||||
DIFFICULTY={{ minecraft.difficulty }}
|
||||
{% if minecraft.online_mode %}
|
||||
ONLINE_MODE=true
|
||||
{% else %}
|
||||
ONLINE_MODE=false
|
||||
{% endif %}
|
||||
VIEW_DISTANCE={{ minecraft.view_distance | default(10) }}
|
||||
SIMULATION_DISTANCE={{ minecraft.simulation_distance | default(6) }}
|
||||
|
||||
{% if minecraft.whitelist_enabled %}
|
||||
ENABLE_WHITELIST=true
|
||||
{% else %}
|
||||
ENABLE_WHITELIST=false
|
||||
{% endif %}
|
||||
|
||||
{% if minecraft.enable_command_block %}
|
||||
ENABLE_COMMAND_BLOCK=true
|
||||
{% else %}
|
||||
ENABLE_COMMAND_BLOCK=false
|
||||
{% endif %}
|
||||
|
||||
{% if minecraft.max_world_size %}
|
||||
MAX_WORLD_SIZE={{ minecraft.max_world_size }}
|
||||
{% endif %}
|
||||
|
||||
{% if minecraft.spawn_protection %}
|
||||
SPAWN_PROTECTION={{ minecraft.spawn_protection }}
|
||||
{% endif %}
|
||||
|
||||
# Server name broadcast
|
||||
SERVER_NAME={{ minecraft.server_name }}
|
||||
|
||||
# Make the container non-interactive
|
||||
EULA=TRUE
|
||||
@@ -1,42 +0,0 @@
|
||||
# Systemd unit for {{ minecraft.server_name }} Minecraft server running on Podman
|
||||
# Deployed by Ansible - managed file
|
||||
[Unit]
|
||||
Description=Minecraft Server ({{ minecraft.server_name }}) - Podman Container
|
||||
Documentation=https://hub.docker.com/r/itzg/minecraft-server
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User={{ minecraft_user }}
|
||||
Group={{ minecraft_user }}
|
||||
WorkingDirectory={{ paths.data_dir }}
|
||||
|
||||
# Pull the image if not present (idempotent)
|
||||
ExecStartPre=/usr/bin/podman pull {{ podman.image }}:{{ podman.image_tag }}
|
||||
|
||||
# Stop existing container before starting
|
||||
ExecStartPre=-/usr/bin/podman rm -f {{ podman_container_name }}
|
||||
|
||||
# Run the Minecraft server in a Podman container
|
||||
ExecStart=/usr/bin/podman run --name {{ podman_container_name }} --replace --env-file {{ paths.base_dir }}/minecraft.env --volume {{ paths.data_dir }}:/data:Z {% if podman.extra_volumes is defined and podman.extra_volumes %}{% for vol in podman.extra_volumes %}--volume {{ vol }} {% endfor %}{% endif %}{% if podman.memory_limit is defined and podman.memory_limit %}--memory {{ podman.memory_limit }} {% endif %}{% if podman.cpu_shares is defined and podman.cpu_shares %}--cpu-shares {{ podman.cpu_shares }} {% endif %}{% if podman.network_mode == 'host' %}--network host {% else %}--publish {{ minecraft.port }}:{{ minecraft.port }}/tcp {% if minecraft.rcon_enabled %}--publish {{ minecraft.rcon_port }}:{{ minecraft.rcon_port }}/tcp {% endif %}{% endif %}{% for key, value in podman.labels.items() %}--label "{{ key }}={{ value }}" {% endfor %}--log-driver {{ podman.log_driver | default('journald') }} {{ podman.image }}:{{ podman.image_tag }}
|
||||
|
||||
# Restart if it crashes (systemd handles restart policy)
|
||||
Restart=on-failure
|
||||
RestartSec=15
|
||||
|
||||
# Basic process limits
|
||||
LimitNOFILE=65536
|
||||
LimitNPROC=4096
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier={{ minecraft.server_name }}
|
||||
|
||||
# Allow container to manage its own namespaces/limits
|
||||
NoNewPrivileges=false
|
||||
ProtectSystem=false
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,106 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Minecraft Server Restore Script
|
||||
# Deployed by Ansible - managed file
|
||||
#
|
||||
# Usage: ./restore.sh [path_to_backup_file]
|
||||
# If no argument given, restores the most recent backup.
|
||||
#
|
||||
# WARNING: This will OVERWRITE the current world data!
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
DATA_DIR="{{ paths.data_dir }}"
|
||||
BACKUP_DIR="{{ paths.backup_dir }}"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log() {
|
||||
echo -e "$(date '+%Y-%m-%d %H:%M:%S') $*"
|
||||
}
|
||||
|
||||
error() {
|
||||
log "${RED}[ERROR]${NC} $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
warn() {
|
||||
log "${YELLOW}[WARN]${NC} $*"
|
||||
}
|
||||
|
||||
ok() {
|
||||
log "${GREEN}[OK]${NC} $*"
|
||||
}
|
||||
|
||||
# Determine which backup to restore
|
||||
if [ $# -eq 1 ]; then
|
||||
# Explicit file provided
|
||||
RESTORE_FILE="$1"
|
||||
if [ ! -f "$RESTORE_FILE" ]; then
|
||||
# Try to find it in the backup dir
|
||||
RESTORE_FILE="${BACKUP_DIR}/$1"
|
||||
if [ ! -f "$RESTORE_FILE" ]; then
|
||||
error "Backup file not found: $1 (looked in $BACKUP_DIR)"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Find most recent backup
|
||||
RESTORE_FILE=$(ls -t "${BACKUP_DIR}/"{{ minecraft.server_name }}_*.tar* 2>/dev/null | head -n1)
|
||||
if [ -z "$RESTORE_FILE" ]; then
|
||||
error "No backups found in $BACKUP_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Selected backup: $RESTORE_FILE"
|
||||
|
||||
# Double-check safety
|
||||
warn "This will overwrite the current Minecraft world data in $DATA_DIR"
|
||||
read -p "Are you sure you want to continue? (yes/no): " CONFIRM
|
||||
if [ "$CONFIRM" != "yes" ]; then
|
||||
log "Aborting restore."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop the Minecraft server if running
|
||||
if podman ps --format '{% raw %}{{.Names}}{% endraw %}' | grep -q '{{ podman_container_name }}'; then
|
||||
warn "Stopping Minecraft server container..."
|
||||
systemctl stop {{ minecraft.server_name }}
|
||||
sleep 3
|
||||
warn "Server stopped."
|
||||
fi
|
||||
|
||||
# Create a temporary backup of current state (safety net)
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
SAFETY_BACKUP="${BACKUP_DIR}/pre_restore_${TIMESTAMP}.tar.gz"
|
||||
log "Creating safety backup of current state: $SAFETY_BACKUP"
|
||||
tar -czf "$SAFETY_BACKUP" -C "$(dirname $DATA_DIR)" "$(basename $DATA_DIR)"
|
||||
|
||||
# Extract the restore file
|
||||
log "Restoring from: $RESTORE_FILE"
|
||||
case "$RESTORE_FILE" in
|
||||
*.tar.gz|*.tgz)
|
||||
tar -xzf "$RESTORE_FILE" -C "$(dirname $DATA_DIR)"
|
||||
;;
|
||||
*.tar)
|
||||
tar -xf "$RESTORE_FILE" -C "$(dirname $DATA_DIR)"
|
||||
;;
|
||||
*)
|
||||
error "Unsupported archive format: $RESTORE_FILE"
|
||||
;;
|
||||
esac
|
||||
|
||||
ok "Restore completed successfully!"
|
||||
|
||||
# Restart the server
|
||||
log "Restarting Minecraft server..."
|
||||
systemctl start {{ minecraft.server_name }}
|
||||
sleep 5
|
||||
|
||||
ok "Server restarted. Verify it's running: systemctl status {{ minecraft.server_name }}"
|
||||
log "A safety backup of the pre-restore state was saved to: $SAFETY_BACKUP"
|
||||
@@ -1,59 +0,0 @@
|
||||
#Minecraft server properties
|
||||
#Deployed by Ansible - Manage via group_vars/all.yml
|
||||
|
||||
#Minecraft properties
|
||||
enable-jmx-monitoring=false
|
||||
rcon.port={{ minecraft.rcon_port | default(25575) }}
|
||||
level-seed=
|
||||
gamemode={{ minecraft.game_mode }}
|
||||
enable-command-block={{ minecraft.enable_command_block | default('false') }}
|
||||
enable-query=false
|
||||
generator-settings={}
|
||||
enforce-secure-profile={{ minecraft.enforce_secure_profile | default('true') }}
|
||||
level-name=world
|
||||
motd={{ minecraft.motd | default('A Minecraft Server') }}
|
||||
query.port={{ minecraft.port | default(25565) }}
|
||||
pvp=true
|
||||
generate-structures=true
|
||||
max-chained-neighbor-updates=1000000
|
||||
difficulty={{ minecraft.difficulty }}
|
||||
network-compression-threshold=256
|
||||
max-tick-time=60000
|
||||
require-resource-pack=false
|
||||
use-native-transport=true
|
||||
max-players={{ minecraft.max_players | default(20) }}
|
||||
online-mode={{ minecraft.online_mode | default('true') }}
|
||||
enable-status=true
|
||||
allow-flight=false
|
||||
initial-disabled-packs=
|
||||
broadcast-rcon-to-ops=true
|
||||
view-distance={{ minecraft.view_distance | default(10) }}
|
||||
server-ip=
|
||||
resource-pack-prompt=
|
||||
allow-nether=true
|
||||
server-port={{ minecraft.port | default(25565) }}
|
||||
enable-rcon={{ minecraft.rcon_enabled | default('false') }}
|
||||
sync-chunk-writes=true
|
||||
op-permission-level=4
|
||||
prevent-proxy-connections=false
|
||||
hide-online-players=false
|
||||
resource-pack=
|
||||
entity-broadcast-range-percentage=100
|
||||
simulation-distance={{ minecraft.simulation_distance | default(6) }}
|
||||
rcon.password={{ minecraft.rcon_password | default('') }}
|
||||
player-idle-timeout=0
|
||||
force-gamemode=false
|
||||
rate-limit=0
|
||||
hardcore=false
|
||||
white-list={{ minecraft.whitelist_enabled | default('false') }}
|
||||
broadcast-console-to-ops=true
|
||||
spawn-npcs=true
|
||||
spawn-animals=true
|
||||
log-ips=true
|
||||
function-permission-level=2
|
||||
level-type=minecraft\:normal
|
||||
text-filtering-config=
|
||||
spawn-monsters=true
|
||||
enforce-whitelist=false
|
||||
spawn-protection={{ minecraft.spawn_protection | default(16) }}
|
||||
max-world-size={{ minecraft.max_world_size | default(29999984) }}
|
||||
31
roles/minecraft/defaults/main.yml
Normal file
31
roles/minecraft/defaults/main.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
minecraft_data_dir: /srv/minecraft
|
||||
minecraft_image: docker.io/itzg/minecraft-server:latest
|
||||
minecraft_server_type: PAPER
|
||||
minecraft_version: LATEST
|
||||
minecraft_memory: 2G
|
||||
minecraft_difficulty: normal
|
||||
minecraft_motd: "Hardened Fedora 42 Minecraft Server"
|
||||
minecraft_max_players: "20"
|
||||
minecraft_view_distance: "10"
|
||||
minecraft_online_mode: "TRUE"
|
||||
minecraft_whitelist: ""
|
||||
# Port the Java server listens on INSIDE the container (host port
|
||||
# publish is controlled by minecraft_port / minecraft_udp).
|
||||
minecraft_container_port: 25565
|
||||
minecraft_prepull: true
|
||||
|
||||
# Geyser - Bedrock protocol bridge (geysermc/geyser). Runs in the same pod
|
||||
# as the Java server and publishes the Bedrock UDP port so Bedrock clients
|
||||
# can join the Java world.
|
||||
enable_geyser: true
|
||||
geyser_image: docker.io/geysermc/geyser:latest
|
||||
# Bedrock UDP port published on the host AND listened on by Geyser inside
|
||||
# the container (pod-level publish, must match the firewall rule).
|
||||
geyser_port: 19132
|
||||
# Inside the pod all containers share localhost, so Geyser talks to the Java
|
||||
# server over 127.0.0.1:{{ minecraft_container_port }}.
|
||||
geyser_remote_address: 127.0.0.1
|
||||
# online matches the Java server's ONLINE_MODE. Set to "offline" only if you
|
||||
# also run Floodgate and pair the servers with a shared key.
|
||||
geyser_auth_type: online
|
||||
101
roles/minecraft/tasks/main.yml
Normal file
101
roles/minecraft/tasks/main.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
# Minecraft server deployment: a Quadlet pod running the Java server
|
||||
# (itzg/minecraft-server) and, when enabled, the Geyser bridge container
|
||||
# (geysermc/geyser) so Bedrock clients can join. Everything runs rootless
|
||||
# under the admin user. Containers share the pod's network namespace, so
|
||||
# Geyser reaches the Java server at 127.0.0.1.
|
||||
|
||||
- name: Build the list of data directories
|
||||
ansible.builtin.set_fact:
|
||||
minecraft_data_dirs: >-
|
||||
{{ [minecraft_data_dir, minecraft_data_dir + '/data', minecraft_data_dir + '/backups']
|
||||
+ ([minecraft_data_dir + '/geyser'] if enable_geyser | bool else []) }}
|
||||
|
||||
- name: Create the Minecraft data directory tree
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0750"
|
||||
loop: "{{ minecraft_data_dirs }}"
|
||||
|
||||
- name: Restore SELinux context on the data directory
|
||||
ansible.builtin.command: restorecon -Rv {{ minecraft_data_dir }}
|
||||
changed_when: false
|
||||
|
||||
- name: Create the Quadlet systemd unit directory
|
||||
ansible.builtin.file:
|
||||
path: "/home/{{ admin_user }}/.config/containers/systemd"
|
||||
state: directory
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0700"
|
||||
|
||||
- name: Deploy the Minecraft pod unit
|
||||
ansible.builtin.template:
|
||||
src: minecraft.pod.j2
|
||||
dest: "/home/{{ admin_user }}/.config/containers/systemd/minecraft.pod"
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Deploy the Minecraft Java server unit
|
||||
ansible.builtin.template:
|
||||
src: minecraft-server.container.j2
|
||||
dest: "/home/{{ admin_user }}/.config/containers/systemd/minecraft-server.container"
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Deploy the Geyser config file
|
||||
ansible.builtin.template:
|
||||
src: geyser-config.yml.j2
|
||||
dest: "{{ minecraft_data_dir }}/geyser/config.yml"
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0640"
|
||||
when: enable_geyser | bool
|
||||
|
||||
- name: Deploy the Geyser bridge unit
|
||||
ansible.builtin.template:
|
||||
src: geyser.container.j2
|
||||
dest: "/home/{{ admin_user }}/.config/containers/systemd/geyser.container"
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0644"
|
||||
when: enable_geyser | bool
|
||||
|
||||
- name: Build the list of images to pull
|
||||
ansible.builtin.set_fact:
|
||||
minecraft_images: "{{ [minecraft_image] + ([geyser_image] if enable_geyser | bool else []) }}"
|
||||
when: minecraft_prepull | bool
|
||||
|
||||
- name: Check if container images are already present
|
||||
ansible.builtin.command: machinectl shell {{ admin_user }}@ -- podman image exists {{ item }}
|
||||
loop: "{{ minecraft_images }}"
|
||||
register: images_exist
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when: minecraft_prepull | bool
|
||||
|
||||
- name: Pull missing container images
|
||||
ansible.builtin.command: machinectl shell {{ admin_user }}@ -- podman pull {{ item.item }}
|
||||
loop: "{{ images_exist.results }}"
|
||||
when:
|
||||
- minecraft_prepull | bool
|
||||
- item.rc != 0
|
||||
|
||||
- name: Build the list of user services to enable
|
||||
ansible.builtin.set_fact:
|
||||
minecraft_units: "{{ ['minecraft-pod.service', 'minecraft-server.service'] + (['geyser.service'] if enable_geyser | bool else []) }}"
|
||||
|
||||
- name: Reload the user systemd daemon
|
||||
ansible.builtin.command: machinectl shell {{ admin_user }}@ -- systemctl --user daemon-reload
|
||||
changed_when: false
|
||||
|
||||
- name: Enable and start the Minecraft services
|
||||
ansible.builtin.command: machinectl shell {{ admin_user }}@ -- systemctl --user enable --now {{ item }}
|
||||
loop: "{{ minecraft_units }}"
|
||||
register: mc_services
|
||||
changed_when: false
|
||||
17
roles/minecraft/templates/geyser-config.yml.j2
Normal file
17
roles/minecraft/templates/geyser-config.yml.j2
Normal file
@@ -0,0 +1,17 @@
|
||||
# Managed by Ansible - minecraft role (Geyser bridge).
|
||||
# Minimal config: Geyser merges these keys over its built-in defaults.
|
||||
# The Java server shares the pod's network namespace, so it is reachable
|
||||
# at 127.0.0.1:{{ minecraft_container_port }}.
|
||||
|
||||
bedrock:
|
||||
address: 0.0.0.0
|
||||
port: {{ geyser_port }}
|
||||
clone-remote-port: false
|
||||
enable-proxy-protocol: false
|
||||
|
||||
remote:
|
||||
address: {{ geyser_remote_address }}
|
||||
port: {{ minecraft_container_port }}
|
||||
auth-type: {{ geyser_auth_type }}
|
||||
allow-passthrough-motd: true
|
||||
allow-passthrough-player-counts: true
|
||||
18
roles/minecraft/templates/geyser.container.j2
Normal file
18
roles/minecraft/templates/geyser.container.j2
Normal file
@@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=Geyser Bedrock bridge container ({{ geyser_image }})
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Container]
|
||||
Label=app=geyser
|
||||
ContainerName=geyser
|
||||
Pod=minecraft.pod
|
||||
Image={{ geyser_image }}
|
||||
Volume={{ minecraft_data_dir }}/geyser:/geyser:Z
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
33
roles/minecraft/templates/minecraft-server.container.j2
Normal file
33
roles/minecraft/templates/minecraft-server.container.j2
Normal file
@@ -0,0 +1,33 @@
|
||||
[Unit]
|
||||
Description=Minecraft Java server container ({{ minecraft_image }})
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Container]
|
||||
Label=app=minecraft-server
|
||||
ContainerName=minecraft-server
|
||||
Pod=minecraft.pod
|
||||
Image={{ minecraft_image }}
|
||||
Environment=EULA=TRUE
|
||||
Environment=TYPE={{ minecraft_server_type }}
|
||||
Environment=VERSION={{ minecraft_version }}
|
||||
Environment=MEMORY={{ minecraft_memory }}
|
||||
Environment=DIFFICULTY={{ minecraft_difficulty }}
|
||||
Environment=MOTD={{ minecraft_motd }}
|
||||
Environment=MAX_PLAYERS={{ minecraft_max_players }}
|
||||
Environment=VIEW_DISTANCE={{ minecraft_view_distance }}
|
||||
Environment=ONLINE_MODE={{ minecraft_online_mode }}
|
||||
Environment=OVERRIDE_SERVER_PROPERTIES=TRUE
|
||||
{% if minecraft_whitelist | length > 0 %}
|
||||
Environment=ENABLE_WHITELIST=TRUE
|
||||
Environment=WHITELIST={{ minecraft_whitelist }}
|
||||
{% endif %}
|
||||
Volume={{ minecraft_data_dir }}/data:/data:Z
|
||||
Volume={{ minecraft_data_dir }}/backups:/backups:Z
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
14
roles/minecraft/templates/minecraft.pod.j2
Normal file
14
roles/minecraft/templates/minecraft.pod.j2
Normal file
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Minecraft pod (Java server + Geyser bridge)
|
||||
|
||||
[Pod]
|
||||
PublishPort={{ minecraft_port }}:25565/tcp
|
||||
{% if minecraft_udp | bool %}
|
||||
PublishPort={{ minecraft_port }}:25565/udp
|
||||
{% endif %}
|
||||
{% if enable_geyser | bool %}
|
||||
PublishPort={{ geyser_port }}:{{ geyser_port }}/udp
|
||||
{% endif %}
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
30
roles/packages/defaults/main.yml
Normal file
30
roles/packages/defaults/main.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
base_packages:
|
||||
- wget
|
||||
- curl
|
||||
- tar
|
||||
- unzip
|
||||
- git
|
||||
- vim-enhanced
|
||||
- htop
|
||||
- tmux
|
||||
- firewalld
|
||||
- fail2ban
|
||||
- dnf-automatic
|
||||
- policycoreutils-python-utils
|
||||
- python3-pip
|
||||
|
||||
podman_packages:
|
||||
- container-tools
|
||||
- podman
|
||||
- podman-docker
|
||||
- podman-compose
|
||||
- fuse-overlayfs
|
||||
- slirp4netns
|
||||
- netavark
|
||||
|
||||
qemu_packages:
|
||||
- qemu-kvm-core
|
||||
|
||||
install_qemu: false
|
||||
enable_dnf_automatic: true
|
||||
45
roles/packages/tasks/main.yml
Normal file
45
roles/packages/tasks/main.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
# Install base system packages, Podman tooling, and keep everything patched.
|
||||
|
||||
- name: Install common system packages
|
||||
ansible.builtin.dnf:
|
||||
name: "{{ base_packages }}"
|
||||
state: present
|
||||
|
||||
- name: Install Podman container tooling
|
||||
ansible.builtin.dnf:
|
||||
name: "{{ podman_packages }}"
|
||||
state: present
|
||||
|
||||
- name: Install QEMU packages (optional - required for building images that need emulation)
|
||||
ansible.builtin.dnf:
|
||||
name: "{{ qemu_packages }}"
|
||||
state: present
|
||||
when: install_qemu | bool
|
||||
|
||||
- name: Apply all available package updates
|
||||
ansible.builtin.dnf:
|
||||
name: "*"
|
||||
state: latest
|
||||
update_only: true
|
||||
|
||||
- name: Enable automatic security updates
|
||||
ansible.builtin.systemd:
|
||||
name: dnf-automatic.timer
|
||||
enabled: true
|
||||
state: started
|
||||
when: enable_dnf_automatic | bool
|
||||
|
||||
- name: Configure dnf-automatic to apply updates
|
||||
ansible.builtin.ini_file:
|
||||
path: /etc/dnf/automatic.conf
|
||||
section: commands
|
||||
option: apply_updates
|
||||
value: "yes"
|
||||
when: enable_dnf_automatic | bool
|
||||
|
||||
- name: Install container SELinux policy support
|
||||
ansible.builtin.dnf:
|
||||
name: container-selinux
|
||||
state: present
|
||||
when: ansible_selinux is defined and ansible_selinux.status == "enabled"
|
||||
8
roles/podman/defaults/main.yml
Normal file
8
roles/podman/defaults/main.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
enable_podman_socket: true
|
||||
podman_enable_linger: true
|
||||
podman_manage_subuid: true
|
||||
podman_subuid_start: 200000
|
||||
podman_manage_subgid: true
|
||||
podman_subgid_start: 200000
|
||||
podman_storage_driver: overlay
|
||||
62
roles/podman/tasks/main.yml
Normal file
62
roles/podman/tasks/main.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
# Podman runtime preparation: rootless container support for the admin user.
|
||||
|
||||
- name: Enable Podman socket activation
|
||||
ansible.builtin.systemd:
|
||||
name: podman.socket
|
||||
enabled: true
|
||||
state: started
|
||||
when: enable_podman_socket | bool
|
||||
|
||||
- name: Enable user lingering for rootless containers
|
||||
ansible.builtin.command: loginctl enable-linger {{ admin_user }}
|
||||
changed_when: false
|
||||
when: podman_enable_linger | bool
|
||||
|
||||
- name: Ensure subuid range for the admin user
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/subuid
|
||||
regexp: '^{{ admin_user }}:'
|
||||
line: "{{ admin_user }}:{{ podman_subuid_start }}:65536"
|
||||
when: podman_manage_subuid | bool
|
||||
|
||||
- name: Ensure subgid range for the admin user
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/subgid
|
||||
regexp: '^{{ admin_user }}:'
|
||||
line: "{{ admin_user }}:{{ podman_subgid_start }}:65536"
|
||||
when: podman_manage_subgid | bool
|
||||
|
||||
- name: Persist kernel modules for container networking
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/modules-load.d/containers.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: "overlay\nbr_netfilter\n"
|
||||
|
||||
- name: Load overlay kernel module
|
||||
ansible.builtin.command: modprobe overlay
|
||||
changed_when: false
|
||||
|
||||
- name: Load br_netfilter kernel module
|
||||
ansible.builtin.command: modprobe br_netfilter
|
||||
changed_when: false
|
||||
|
||||
- name: Enable IPv4 forwarding persistently
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/sysctl.d/99-podman.conf
|
||||
create: true
|
||||
regexp: '^net\.ipv4\.ip_forward'
|
||||
line: "net.ipv4.ip_forward = 1"
|
||||
mode: "0644"
|
||||
|
||||
- name: Apply IPv4 forwarding
|
||||
ansible.builtin.command: sysctl -w net.ipv4.ip_forward=1
|
||||
changed_when: false
|
||||
|
||||
- name: Configure Podman storage driver
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/containers/storage.conf
|
||||
regexp: '^driver\s*='
|
||||
line: 'driver = "{{ podman_storage_driver }}"'
|
||||
2
roles/selinux/defaults/main.yml
Normal file
2
roles/selinux/defaults/main.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
---
|
||||
selinux_set_minecraft_fcontext: true
|
||||
22
roles/selinux/tasks/main.yml
Normal file
22
roles/selinux/tasks/main.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# SELinux hardening: enforcing mode plus correct labels for container data.
|
||||
|
||||
- name: Enforce SELinux (targeted policy)
|
||||
ansible.builtin.selinux:
|
||||
policy: targeted
|
||||
state: enforcing
|
||||
|
||||
- name: Ensure semanage is available
|
||||
ansible.builtin.dnf:
|
||||
name: policycoreutils-python-utils
|
||||
state: present
|
||||
|
||||
- name: Add SELinux file context for the Minecraft data directory
|
||||
ansible.builtin.command:
|
||||
cmd: semanage fcontext -a -t container_file_t "{{ minecraft_data_dir }}(/.*)?"
|
||||
register: selinux_fcontext
|
||||
changed_when: selinux_fcontext.rc == 0
|
||||
failed_when:
|
||||
- selinux_fcontext.rc != 0
|
||||
- "'already defined' not in (selinux_fcontext.stdout + selinux_fcontext.stderr)"
|
||||
when: selinux_set_minecraft_fcontext | bool
|
||||
7
roles/ssh_harden/defaults/main.yml
Normal file
7
roles/ssh_harden/defaults/main.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
ssh_port: 22
|
||||
ssh_restrict_users: true
|
||||
ssh_client_alive_interval: 300
|
||||
ssh_client_alive_count_max: 2
|
||||
ssh_max_auth_tries: 3
|
||||
enable_fail2ban: true
|
||||
10
roles/ssh_harden/handlers/main.yml
Normal file
10
roles/ssh_harden/handlers/main.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: restart sshd
|
||||
ansible.builtin.systemd:
|
||||
name: sshd
|
||||
state: restarted
|
||||
|
||||
- name: restart fail2ban
|
||||
ansible.builtin.systemd:
|
||||
name: fail2ban
|
||||
state: restarted
|
||||
34
roles/ssh_harden/tasks/main.yml
Normal file
34
roles/ssh_harden/tasks/main.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
# SSH hardening: key-only auth for the admin user via a drop-in config,
|
||||
# plus fail2ban to rate-limit brute force attempts.
|
||||
|
||||
- name: Deploy SSH server hardening drop-in
|
||||
ansible.builtin.template:
|
||||
src: sshd-hardening.conf.j2
|
||||
dest: /etc/ssh/sshd_config.d/99-ansible-hardening.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: restart sshd
|
||||
|
||||
- name: Validate sshd configuration before restart
|
||||
ansible.builtin.command: /usr/sbin/sshd -t
|
||||
register: sshd_test
|
||||
changed_when: false
|
||||
failed_when: sshd_test.rc != 0
|
||||
|
||||
- name: Deploy fail2ban SSH jail
|
||||
ansible.builtin.template:
|
||||
src: fail2ban-local.j2
|
||||
dest: /etc/fail2ban/jail.local
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: restart fail2ban
|
||||
|
||||
- name: Ensure fail2ban is running and enabled
|
||||
ansible.builtin.systemd:
|
||||
name: fail2ban
|
||||
enabled: true
|
||||
state: started
|
||||
when: enable_fail2ban | bool
|
||||
11
roles/ssh_harden/templates/fail2ban-local.j2
Normal file
11
roles/ssh_harden/templates/fail2ban-local.j2
Normal file
@@ -0,0 +1,11 @@
|
||||
# Managed by Ansible - ssh_harden role.
|
||||
|
||||
[DEFAULT]
|
||||
bantime = 1h
|
||||
findtime = 10m
|
||||
maxretry = 5
|
||||
ignoreip = 127.0.0.1/8 ::1
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = {{ ssh_port }}
|
||||
18
roles/ssh_harden/templates/sshd-hardening.conf.j2
Normal file
18
roles/ssh_harden/templates/sshd-hardening.conf.j2
Normal file
@@ -0,0 +1,18 @@
|
||||
# Managed by Ansible - ssh_harden role.
|
||||
# Sorted last (99-) so this wins over other drop-ins and the main config.
|
||||
|
||||
PermitRootLogin no
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
PermitEmptyPasswords no
|
||||
UseDNS no
|
||||
X11Forwarding no
|
||||
PrintMotd no
|
||||
MaxAuthTries {{ ssh_max_auth_tries }}
|
||||
MaxSessions 4
|
||||
LoginGraceTime 30
|
||||
ClientAliveInterval {{ ssh_client_alive_interval }}
|
||||
ClientAliveCountMax {{ ssh_client_alive_count_max }}
|
||||
{% if ssh_port != 22 %}Port {{ ssh_port }}{% endif %}
|
||||
{% if ssh_restrict_users %}AllowUsers {{ admin_user }}{% endif %}
|
||||
62
site.yml
62
site.yml
@@ -1,62 +0,0 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# Minecraft Podman Deployment - Main Playbook
|
||||
# =============================================================================
|
||||
# Usage:
|
||||
# ansible-playbook site.yml # Deploy to all minecraft servers
|
||||
# ansible-playbook site.yml --limit external-apps # Deploy to specific host
|
||||
# ansible-playbook site.yml --tags install # Only run install tasks
|
||||
# ansible-playbook site.yml --tags config # Only run config tasks
|
||||
# ansible-playbook site.yml --tags backup # Only run backup tasks
|
||||
# ansible-playbook site.yml --tags update # Only run update tasks
|
||||
|
||||
- name: Deploy Minecraft Server on Podman
|
||||
hosts: minecraft
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
pre_tasks:
|
||||
- name: Display deployment info
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
========================================
|
||||
Minecraft Podman Deployment
|
||||
========================================
|
||||
Target: {{ inventory_hostname }}
|
||||
Server: {{ minecraft.server_name }}
|
||||
Type: {{ minecraft.server_type }}
|
||||
Image: {{ podman.image }}
|
||||
Port: {{ minecraft.port }}
|
||||
Memory: {{ minecraft.java_memory }}
|
||||
========================================
|
||||
|
||||
- name: Verify target is Fedora
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_distribution == "Fedora"
|
||||
fail_msg: "This playbook is designed for Fedora. Detected: {{ ansible_distribution }}"
|
||||
success_msg: "Fedora {{ ansible_distribution_version }} confirmed"
|
||||
|
||||
roles:
|
||||
- role: minecraft-podman
|
||||
tags:
|
||||
- install
|
||||
- config
|
||||
- backup
|
||||
- update
|
||||
- always
|
||||
|
||||
post_tasks:
|
||||
- name: Display completion message
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
========================================
|
||||
Deployment Complete!
|
||||
========================================
|
||||
Server IP: {{ ansible_default_ipv4.address | default(ansible_host) }}
|
||||
Port: {{ minecraft.port }}
|
||||
RCON: {{ 'Enabled' if minecraft.rcon_enabled else 'Disabled' }}
|
||||
Status: {{ minecraft.server_name }}.service
|
||||
========================================
|
||||
Connect: {{ ansible_default_ipv4.address | default(ansible_host) }}:{{ minecraft.port }}
|
||||
========================================
|
||||
112
uninstall.yml
112
uninstall.yml
@@ -1,112 +0,0 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# Minecraft Podman Deployment - Uninstall Playbook
|
||||
# =============================================================================
|
||||
# Usage:
|
||||
# ansible-playbook uninstall.yml --limit external-apps
|
||||
#
|
||||
# WARNING: This will remove ALL Minecraft data, containers, and configuration
|
||||
# from the target server. Run only if you're sure you want to clean up.
|
||||
# =============================================================================
|
||||
|
||||
- name: Uninstall Minecraft Server from Podman
|
||||
hosts: minecraft
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
tasks:
|
||||
- name: Confirm uninstall
|
||||
ansible.builtin.pause:
|
||||
prompt: "WARNING: This will remove all Minecraft data from {{ inventory_hostname }}. Type 'yes' to continue"
|
||||
register: confirm
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Abort if not confirmed
|
||||
ansible.builtin.fail:
|
||||
msg: "Uninstall aborted"
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- confirm.user_input | lower != "yes"
|
||||
|
||||
- name: Stop Minecraft service
|
||||
ansible.builtin.systemd:
|
||||
name: "minecraft.service"
|
||||
state: stopped
|
||||
enabled: false
|
||||
ignore_errors: true
|
||||
|
||||
- name: Stop backup timer
|
||||
ansible.builtin.systemd:
|
||||
name: "minecraft-backup.timer"
|
||||
state: stopped
|
||||
enabled: false
|
||||
ignore_errors: true
|
||||
|
||||
- name: Stop health timer
|
||||
ansible.builtin.systemd:
|
||||
name: "minecraft-health.timer"
|
||||
state: stopped
|
||||
enabled: false
|
||||
ignore_errors: true
|
||||
|
||||
- name: Remove systemd unit files
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- "/etc/systemd/system/minecraft.service"
|
||||
- "/etc/systemd/system/minecraft-backup.timer"
|
||||
- "/etc/systemd/system/minecraft-backup.service"
|
||||
- "/etc/systemd/system/minecraft-health.timer"
|
||||
- "/etc/systemd/system/minecraft-health.service"
|
||||
ignore_errors: true
|
||||
|
||||
- name: Reload systemd daemon
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Stop and remove container
|
||||
containers.podman.podman_container:
|
||||
name: "minecraft"
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
register: container_remove
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Remove container (fallback with command)
|
||||
ansible.builtin.command: podman rm -f minecraft
|
||||
ignore_errors: true
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- container_remove is failed
|
||||
|
||||
- name: Remove base directory
|
||||
ansible.builtin.file:
|
||||
path: "/opt/minecraft"
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Remove Minecraft system user
|
||||
ansible.builtin.user:
|
||||
name: "minecraft"
|
||||
state: absent
|
||||
remove: true
|
||||
ignore_errors: true
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Close firewall ports
|
||||
ansible.posix.firewalld:
|
||||
port: "{{ item }}"
|
||||
permanent: true
|
||||
immediate: true
|
||||
state: disabled
|
||||
loop:
|
||||
- "25565/tcp"
|
||||
- "25575/tcp"
|
||||
ignore_errors: true
|
||||
tags: [firewall]
|
||||
|
||||
- name: Uninstall complete message
|
||||
ansible.builtin.debug:
|
||||
msg: "Minecraft server removed from {{ inventory_hostname }}"
|
||||
Reference in New Issue
Block a user