commit 3c80aef53b751254d71d7c9eb6eeb9616d9c5f6d Author: boxswapper Date: Wed Sep 2 14:47:08 2026 -0600 initial commital diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..19ffa6c --- /dev/null +++ b/Makefile @@ -0,0 +1,103 @@ +# ============================================================================= +# 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8ca947 --- /dev/null +++ b/README.md @@ -0,0 +1,190 @@ +# Minecraft Podman Deployment (Ansible) + +Automated deployment of a Minecraft server running in a **Podman** container on a remote **Fedora** server using **Ansible**. + +## 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 + +``` +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 +└── 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 +``` + +## Prerequisites + +### On the control node (your machine) + +```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 +ansible-galaxy collection install -r requirements.yml +``` + +## Configuration + +Edit `group_vars/all.yml` to configure your server: + +| 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` | + +**IMPORTANT**: Change `minecraft.rcon_password` before deploying. + +## Deployment + +```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 +``` + +## Management + +```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 +``` + +## Connecting to the Server + +In Minecraft, use: +``` +: +``` + +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 diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..1d42bdb --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,18 @@ +[defaults] +inventory = inventory/hosts +roles_path = roles +remote_user = boxswapper +host_key_checking = False +retry_files_enabled = False +stdout_callback = yaml +timeout = 30 + +[privilege_escalation] +become = True +become_method = sudo +become_user = root +become_ask_pass = False + +[ssh_connection] +pipelining = True +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ServerAliveInterval=30 diff --git a/group_vars/all.yml b/group_vars/all.yml new file mode 100644 index 0000000..f3a638d --- /dev/null +++ b/group_vars/all.yml @@ -0,0 +1,162 @@ +--- +# ============================================================================= +# Minecraft Podman Deployment - Global Variables +# ============================================================================= +# All variables defined here can be overridden in host_vars/.yml +# or passed at runtime with -e "variable=value" + +# ----------------------------------------------------------------------------- +# Server Configuration +# ----------------------------------------------------------------------------- +minecraft: + # Server name - used for systemd service naming and container name + server_name: "minecraft" + + # Minecraft version (latest, 1.21.4, 1.20.6, etc.) + version: "latest" + + # Server type: "vanilla", "paper", "fabric", "forge", "purpur" + server_type: "paper" + + # Memory allocation for Java (e.g., "4G", "8G", "2G") + java_memory: "4G" + + # Java JVM arguments (additional flags beyond memory) + java_args: "-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200" + + # 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 diff --git a/inventory/hosts b/inventory/hosts new file mode 100644 index 0000000..3b85261 --- /dev/null +++ b/inventory/hosts @@ -0,0 +1,9 @@ +# 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' diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..c0afd2f --- /dev/null +++ b/requirements.yml @@ -0,0 +1,9 @@ +--- +# 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" diff --git a/roles/minecraft-podman/defaults/main.yml b/roles/minecraft-podman/defaults/main.yml new file mode 100644 index 0000000..3eb2c0d --- /dev/null +++ b/roles/minecraft-podman/defaults/main.yml @@ -0,0 +1,19 @@ +--- +# ============================================================================= +# Minecraft Podman Deployment - Role Defaults +# ============================================================================= +# These are the default values for this role. They can be overridden by +# group_vars/all.yml, host_vars/.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" diff --git a/roles/minecraft-podman/handlers/main.yml b/roles/minecraft-podman/handlers/main.yml new file mode 100644 index 0000000..62505b2 --- /dev/null +++ b/roles/minecraft-podman/handlers/main.yml @@ -0,0 +1,25 @@ +--- +# ============================================================================= +# 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 diff --git a/roles/minecraft-podman/tasks/main.yml b/roles/minecraft-podman/tasks/main.yml new file mode 100644 index 0000000..204c2e1 --- /dev/null +++ b/roles/minecraft-podman/tasks/main.yml @@ -0,0 +1,423 @@ +--- +# ============================================================================= +# 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] diff --git a/roles/minecraft-podman/templates/Containerfile.j2 b/roles/minecraft-podman/templates/Containerfile.j2 new file mode 100644 index 0000000..8625516 --- /dev/null +++ b/roles/minecraft-podman/templates/Containerfile.j2 @@ -0,0 +1,28 @@ +# 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 %} diff --git a/roles/minecraft-podman/templates/backup.sh.j2 b/roles/minecraft-podman/templates/backup.sh.j2 new file mode 100644 index 0000000..6ce3949 --- /dev/null +++ b/roles/minecraft-podman/templates/backup.sh.j2 @@ -0,0 +1,96 @@ +#!/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 diff --git a/roles/minecraft-podman/templates/health-check.sh.j2 b/roles/minecraft-podman/templates/health-check.sh.j2 new file mode 100644 index 0000000..9e2e082 --- /dev/null +++ b/roles/minecraft-podman/templates/health-check.sh.j2 @@ -0,0 +1,66 @@ +#!/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 diff --git a/roles/minecraft-podman/templates/jvm.opts.j2 b/roles/minecraft-podman/templates/jvm.opts.j2 new file mode 100644 index 0000000..26b2950 --- /dev/null +++ b/roles/minecraft-podman/templates/jvm.opts.j2 @@ -0,0 +1,4 @@ +# 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') }} diff --git a/roles/minecraft-podman/templates/minecraft-backup.service.j2 b/roles/minecraft-podman/templates/minecraft-backup.service.j2 new file mode 100644 index 0000000..7bb7c86 --- /dev/null +++ b/roles/minecraft-podman/templates/minecraft-backup.service.j2 @@ -0,0 +1,16 @@ +# 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 diff --git a/roles/minecraft-podman/templates/minecraft-backup.timer.j2 b/roles/minecraft-podman/templates/minecraft-backup.timer.j2 new file mode 100644 index 0000000..beeef85 --- /dev/null +++ b/roles/minecraft-podman/templates/minecraft-backup.timer.j2 @@ -0,0 +1,15 @@ +# 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 diff --git a/roles/minecraft-podman/templates/minecraft-health.service.j2 b/roles/minecraft-podman/templates/minecraft-health.service.j2 new file mode 100644 index 0000000..5334dc7 --- /dev/null +++ b/roles/minecraft-podman/templates/minecraft-health.service.j2 @@ -0,0 +1,11 @@ +# 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 diff --git a/roles/minecraft-podman/templates/minecraft-health.timer.j2 b/roles/minecraft-podman/templates/minecraft-health.timer.j2 new file mode 100644 index 0000000..2f91ea4 --- /dev/null +++ b/roles/minecraft-podman/templates/minecraft-health.timer.j2 @@ -0,0 +1,12 @@ +# 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 diff --git a/roles/minecraft-podman/templates/minecraft.env.j2 b/roles/minecraft-podman/templates/minecraft.env.j2 new file mode 100644 index 0000000..62f6b73 --- /dev/null +++ b/roles/minecraft-podman/templates/minecraft.env.j2 @@ -0,0 +1,61 @@ +# 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 diff --git a/roles/minecraft-podman/templates/minecraft.service.j2 b/roles/minecraft-podman/templates/minecraft.service.j2 new file mode 100644 index 0000000..036e650 --- /dev/null +++ b/roles/minecraft-podman/templates/minecraft.service.j2 @@ -0,0 +1,42 @@ +# 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 diff --git a/roles/minecraft-podman/templates/restore.sh.j2 b/roles/minecraft-podman/templates/restore.sh.j2 new file mode 100644 index 0000000..c302343 --- /dev/null +++ b/roles/minecraft-podman/templates/restore.sh.j2 @@ -0,0 +1,106 @@ +#!/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" diff --git a/roles/minecraft-podman/templates/server.properties.j2 b/roles/minecraft-podman/templates/server.properties.j2 new file mode 100644 index 0000000..7ac8b9e --- /dev/null +++ b/roles/minecraft-podman/templates/server.properties.j2 @@ -0,0 +1,59 @@ +#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) }} diff --git a/site.yml b/site.yml new file mode 100644 index 0000000..e717a02 --- /dev/null +++ b/site.yml @@ -0,0 +1,62 @@ +--- +# ============================================================================= +# 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 }} + ======================================== diff --git a/uninstall.yml b/uninstall.yml new file mode 100644 index 0000000..e1fadc4 --- /dev/null +++ b/uninstall.yml @@ -0,0 +1,112 @@ +--- +# ============================================================================= +# 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 }}"