From 2d981027635cc7c05344db473c96e34beb3e965e Mon Sep 17 00:00:00 2001
From: s3lph <s3lph@kabelsalat.ch>
Date: Wed, 5 Mar 2025 22:12:21 +0100
Subject: [PATCH] feat: initial commit

---
 .ansible-lint                                 |  12 ++
 .forgejo/workflows/aardoc.patch               | 153 ++++++++++++++
 .forgejo/workflows/ansible-galaxy.yml         |  45 ++++
 .forgejo/workflows/ansible-lint.yml           |  35 ++++
 .gitignore                                    |   2 +
 .yamllint                                     |   9 +
 README.md                                     |   3 +
 galaxy.yml                                    |  70 +++++++
 meta/runtime.yml                              |  52 +++++
 roles/pretalx/handlers/main.yml               |   7 +
 roles/pretalx/meta/argument_specs.yml         | 181 ++++++++++++++++
 roles/pretalx/meta/main.yml                   |  42 ++++
 roles/pretalx/tasks/install.yml               | 110 ++++++++++
 roles/pretalx/tasks/main.yml                  |   7 +
 .../templates/etc/pretalx/pretalx.cfg.j2      |  47 +++++
 roles/pretix/handlers/main.yml                |   7 +
 roles/pretix/meta/argument_specs.yml          | 198 ++++++++++++++++++
 roles/pretix/meta/main.yml                    |  42 ++++
 roles/pretix/tasks/install.yml                | 119 +++++++++++
 roles/pretix/tasks/main.yml                   |   7 +
 roles/pretix/templates/etc/cron.d/pretix.j2   |   3 +
 .../pretix/templates/etc/pretix/pretix.cfg.j2 |  47 +++++
 roles/watchtower/meta/argument_specs.yml      |  42 ++++
 roles/watchtower/meta/main.yml                |  42 ++++
 roles/watchtower/tasks/install.yml            |  25 +++
 roles/watchtower/tasks/main.yml               |   7 +
 26 files changed, 1314 insertions(+)
 create mode 100644 .ansible-lint
 create mode 100644 .forgejo/workflows/aardoc.patch
 create mode 100644 .forgejo/workflows/ansible-galaxy.yml
 create mode 100644 .forgejo/workflows/ansible-lint.yml
 create mode 100644 .gitignore
 create mode 100644 .yamllint
 create mode 100644 README.md
 create mode 100644 galaxy.yml
 create mode 100644 meta/runtime.yml
 create mode 100644 roles/pretalx/handlers/main.yml
 create mode 100644 roles/pretalx/meta/argument_specs.yml
 create mode 100644 roles/pretalx/meta/main.yml
 create mode 100644 roles/pretalx/tasks/install.yml
 create mode 100644 roles/pretalx/tasks/main.yml
 create mode 100644 roles/pretalx/templates/etc/pretalx/pretalx.cfg.j2
 create mode 100644 roles/pretix/handlers/main.yml
 create mode 100644 roles/pretix/meta/argument_specs.yml
 create mode 100644 roles/pretix/meta/main.yml
 create mode 100644 roles/pretix/tasks/install.yml
 create mode 100644 roles/pretix/tasks/main.yml
 create mode 100644 roles/pretix/templates/etc/cron.d/pretix.j2
 create mode 100644 roles/pretix/templates/etc/pretix/pretix.cfg.j2
 create mode 100644 roles/watchtower/meta/argument_specs.yml
 create mode 100644 roles/watchtower/meta/main.yml
 create mode 100644 roles/watchtower/tasks/install.yml
 create mode 100644 roles/watchtower/tasks/main.yml

diff --git a/.ansible-lint b/.ansible-lint
new file mode 100644
index 0000000..4bdfa07
--- /dev/null
+++ b/.ansible-lint
@@ -0,0 +1,12 @@
+---
+
+skip_list:
+  - meta-runtime[unsupported-version]
+  - galaxy[no-changelog]
+  - galaxy[version-incorrect]
+  - name[casing]
+  - var-naming[no-role-prefix]
+
+# Exclude defaults files generated by aar-doc
+exclude_paths:
+  - "**/defaults/"
diff --git a/.forgejo/workflows/aardoc.patch b/.forgejo/workflows/aardoc.patch
new file mode 100644
index 0000000..25e5893
--- /dev/null
+++ b/.forgejo/workflows/aardoc.patch
@@ -0,0 +1,153 @@
+--- aar_doc/defaults.py
++++ aar_doc/defaults.py
+@@ -15,7 +15,7 @@
+ from ruamel.yaml.scalarstring import LiteralScalarString, SingleQuotedScalarString
+ 
+ yaml = YAML()
+-yaml.indent(mapping=2, sequence=2, offset=2)
++yaml.indent(mapping=2, sequence=4, offset=2)
+ yaml.encoding = "utf-8"
+ yaml.allow_unicode = True
+ 
+--- aar_doc/defaults.py
++++ aar_doc/defaults.py
+@@ -73,18 +73,27 @@ def add_default(
+         else:
+             self._defaults.setdefault(name, RoleDefault(name, value, description))
+ 
++    def safe_quote_recursive(self, value):
++        if isinstance(value, list):
++            return [self.safe_quote_recursive(v) for v in value]
++        elif isinstance(value, dict):
++            return {k: self.safe_quote_recursive(v) for k, v in value.items()}
++        elif isinstance(value, str):
++            if value in ("yes", "no"):
++                return SingleQuotedScalarString(value)
++            elif "\n" in value:
++                return LiteralScalarString(value)
++            elif ":" in value:
++                return SingleQuotedScalarString(value)
++        return value
++
+     def to_commented_map(self) -> CommentedMap:
+         """
+         Returns all tracked defaults as a CommentedMap.
+         """
+         commented_defaults = CommentedMap()
+         for role_default in self.defaults:
+-            value = role_default.value
+-            if isinstance(value, str):
+-                if value in ("yes", "no"):
+-                    value = SingleQuotedScalarString(value)
+-                if "\n" in value:
+-                    value = LiteralScalarString(value)
++            value = self.safe_quote_recursive(role_default.value)
+             commented_defaults[role_default.name] = value
+             description_items = (
+                 role_default.description
+--- aar_doc/core.py
++++ aar_doc/core.py
+@@ -7,6 +7,7 @@ and rendering jinja2 templates from processing data.
+
+ import json
+ import pathlib
++import re
+ from enum import Enum
+
+ import jinja2
+@@ -21,6 +22,24 @@ yaml.encoding = "utf-8"
+ yaml.allow_unicode = True
+
+
++def ansible_doc_markup(text):
++    # Regular expressions copied from ansible-doc:
++    # https://github.com/ansible/ansible/blob/devel/lib/ansible/cli/doc.py#L436
++    out = re.sub(r'\bI\(([^)]+)\)', r'*\1*', text)                # I(text)     -> *text*
++    out = re.sub(r'\bB\(([^)]+)\)', r'**\1**', out)               # B(text)     -> **text**
++    out = re.sub(r'\bC\(([^)]+)\)', r'`\1`', out)                 # C(text)     -> `text`
++    out = re.sub(r'\bM\(([^)]+)\)', r'`\1`', out)                 # M(module)   -> `module`
++    out = re.sub(r'\bO\(((?:[^\\)]+|\\.)+)\)', r'`\1`', out)      # O(option)   -> `option`
++    out = re.sub(r'\bV\(((?:[^\\)]+|\\.)+)\)', r'`\1`', out)      # V(value)    -> `value`
++    out = re.sub(r'\bV\(((?:[^\\)]+|\\.)+)\)', r'`\1`', out)      # E(env)      -> `env`
++    out = re.sub(r'\bV\(((?:[^\\)]+|\\.)+)\)', r'`\1`', out)      # RV(retval)  -> `retval`
++    out = re.sub(r'\bU\(([^)]+)\)', r'[\1]', out)                 # U(url)      -> [url]
++    out = re.sub(r'\bL\(([^)]+), *([^)]+)\)', r'[\1](\2)', out)   # L(text,url) -> [text](url)
++    out = re.sub(r'\bR\(([^)]+), *([^)]+)\)', r'[\1](#\2)', out)  # R(text,frag) -> [text](#frag)
++    out = re.sub(r'\bHORIZONTALLINE\b', r'\n\n---\n', out)        # HORIZONTALLINE -> ---
++    return out
++
++
+ class OutputMode(Enum):
+     """
+     Defines the options for the output mode.
+@@ -240,6 +259,7 @@ def render_content(ctx: typer.Context, content_template: str) -> str:
+         autoescape=jinja2.select_autoescape(),
+         undefined=jinja2.StrictUndefined,
+     )
++    env.filters['ansible_doc_markup'] = ansible_doc_markup
+
+     role = ctx.obj["config"]["role"]
+     metadata = ctx.obj["data"]["metadata"]
+@@ -270,12 +290,14 @@ def render_content(ctx: typer.Context, content_template: str) -> str:
+             keep_trailing_newline=True,
+             loader=jinja2.FileSystemLoader([role_path, output_template_file.parent]),
+         )
++        env.filters['ansible_doc_markup'] = ansible_doc_markup
+         template = env.get_template(output_template_file.name)
+     except (FileNotFoundError, OSError):
+         env = jinja2.Environment(
+             keep_trailing_newline=True,
+             loader=jinja2.FileSystemLoader(role_path),
+         )
++        env.filters['ansible_doc_markup'] = ansible_doc_markup
+         template = env.from_string(source=output_template)
+
+     return template.render(
+--- aar_doc/templates/markdown.j2
++++ aar_doc/templates/markdown.j2
+@@ -3,7 +3,7 @@
+ {%- if "version" in galaxy_collection %}
+ Version: {{ galaxy_collection.version }}
+ {% endif %}
+-{{ metadata.galaxy_info.description }}
++{{ metadata.galaxy_info.description | ansible_doc_markup }}
+ {% if ("galaxy_tags" in metadata.galaxy_info) and (metadata.galaxy_info.galaxy_tags | length > 0) %}
+ Tags: {{ metadata.galaxy_info.galaxy_tags | join(', ') }}
+ {%- endif %}
+@@ -22,14 +22,15 @@ Tags: {{ metadata.galaxy_info.galaxy_tags | join(', ') }}
+
+ ### Entrypoint: {{ entrypoint }}
+
+-{{ argument_specs[entrypoint].short_description }}
++{{ argument_specs[entrypoint].short_description | ansible_doc_markup }}
+
+ {% if "description" in argument_specs[entrypoint] %}
+ {%- if argument_specs[entrypoint].description is string -%}
+-{{ argument_specs[entrypoint].description }}
++{{ argument_specs[entrypoint].description | ansible_doc_markup }}
+ {% else %}
+ {%- for line in argument_specs[entrypoint].description -%}
+-{{ line }}
++{{ line | ansible_doc_markup }}
++
+ {% endfor -%}
+ {% endif -%}
+ {% endif -%}
+@@ -39,7 +40,7 @@ Tags: {{ metadata.galaxy_info.galaxy_tags | join(', ') }}
+ |Option|Description|Type|Required|Default|
+ |---|---|---|---|---|
+ {%- for name, details in options.items() %}
+-| {{ name }} | {{ details.display_description }} | {{ details.display_type }} | {{ details.display_required }} | {{ details.display_default }} |
++| {{ name }} | {{ details.display_description | ansible_doc_markup }} | {{ details.display_type }} | {{ details.display_required }} | {{ details.display_default }} |
+ {%- endfor %}
+
+ {% if entrypoint_options[entrypoint] | length > 1 -%}
+@@ -49,7 +50,7 @@ Tags: {{ metadata.galaxy_info.galaxy_tags | join(', ') }}
+ |Option|Description|Type|Required|Default|
+ |---|---|---|---|---|
+ {%- for name, details in options.items() %}
+-| {{ name }} | {{ details.display_description }} | {{ details.display_type }} | {{ details.display_required }} | {{ details.display_default }} |
++| {{ name }} | {{ details.display_description | ansible_doc_markup }} | {{ details.display_type }} | {{ details.display_required }} | {{ details.display_default }} |
+ {%- endfor %}
+
+ {% endfor -%}
diff --git a/.forgejo/workflows/ansible-galaxy.yml b/.forgejo/workflows/ansible-galaxy.yml
new file mode 100644
index 0000000..c853bd6
--- /dev/null
+++ b/.forgejo/workflows/ansible-galaxy.yml
@@ -0,0 +1,45 @@
+---
+
+name: Ansible Galaxy
+
+on:  # noqa yaml[truthy]
+  push:
+    tags:
+      - 'v*'
+
+jobs:
+  deploy:
+    runs-on: docker
+    steps:
+
+      - uses: actions/checkout@v4
+
+      - name: Install dependencies
+        run: |
+          apt update; apt install --yes python3-pip patch
+          pip3 install --break-system-packages ansible aar-doc
+
+      - name: Patch aar-doc
+        run: |
+          cd /usr/local/lib/python3.*/dist-packages/
+          patch -p0 < $OLDPWD/.forgejo/workflows/aardoc.patch
+
+      - name: Set version in galaxy.yml
+        run: |
+          VERSION=${GITHUB_REF#refs/tags/v}
+          sed -re "s/^version:.*$/version: ${VERSION}/" -i galaxy.yml
+
+      - name: Generate metadata, readme and defaults from argument_spec
+        run: |
+          set -euo pipefail
+          for r in roles/*; do
+            aar-doc --output-mode replace $r markdown
+            aar-doc --output-mode replace $r defaults
+          done
+
+      - name: Upload collection to Ansible Galaxy
+        env:
+          GALAXY_API_KEY: ${{ secrets.GALAXY_API_KEY }}
+        run: |
+          ansible-galaxy collection build
+          ansible-galaxy collection publish --api-key=${GALAXY_API_KEY} s3lph-conference*tar.gz
diff --git a/.forgejo/workflows/ansible-lint.yml b/.forgejo/workflows/ansible-lint.yml
new file mode 100644
index 0000000..5079e39
--- /dev/null
+++ b/.forgejo/workflows/ansible-lint.yml
@@ -0,0 +1,35 @@
+---
+
+name: Ansible Lint
+on: [push, pull_request]  # noqa yaml[truthy]
+
+jobs:
+  build:
+    runs-on: docker
+
+    steps:
+
+      - uses: actions/checkout@v4
+
+      - name: Install dependencies
+        run: |
+          apt update; apt install --yes python3-pip patch
+          pip3 install --break-system-packages ansible-lint aar-doc
+
+      - name: Patch aar-doc
+        run: |
+          cd /usr/local/lib/python3.*/dist-packages/
+          patch -p0 < $OLDPWD/.forgejo/workflows/aardoc.patch
+
+
+      - name: Generate metadata, readme and defaults from argument_spec
+        run: |
+          set -euo pipefail
+          for r in roles/*; do
+            aar-doc --output-mode replace $r markdown
+            aar-doc --output-mode replace $r defaults
+          done
+
+      - name: Run ansible-lint
+        run: |
+          ansible-lint
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..02022ef
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.ansible/
+s3lph-conference*.tar.gz
diff --git a/.yamllint b/.yamllint
new file mode 100644
index 0000000..6b658bd
--- /dev/null
+++ b/.yamllint
@@ -0,0 +1,9 @@
+---
+
+extends: default
+
+rules:
+  line-length: disable
+
+ignore:
+  - "**/defaults/"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a069796
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Ansible Collection - s3lph.conference
+
+Documentation for the collection.
diff --git a/galaxy.yml b/galaxy.yml
new file mode 100644
index 0000000..e658afc
--- /dev/null
+++ b/galaxy.yml
@@ -0,0 +1,70 @@
+---
+# SPDX-License-Identifier: MIT-0
+### REQUIRED
+# The namespace of the collection. This can be a company/brand/organization or product namespace under which all
+# content lives. May only contain alphanumeric lowercase characters and underscores. Namespaces cannot start with
+# underscores or numbers and cannot contain consecutive underscores
+namespace: s3lph
+
+# The name of the collection. Has the same character restrictions as 'namespace'
+name: conference
+
+# The version of the collection. Must be compatible with semantic versioning
+version: 0.0.1
+
+# The path to the Markdown (.md) readme file. This path is relative to the root of the collection
+readme: README.md
+
+# A list of the collection's content authors. Can be just the name or in the format 'Full Name <email> (url)
+# @nicks:irc/im.site#channel'
+authors:
+  - s3lph <s3lph@kabelsalat.ch>
+
+### OPTIONAL but strongly recommended
+# A short summary description of the collection
+description: Install Pretix and Pretalx
+
+# Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only
+# accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file'
+license:
+  - MIT
+
+# A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character
+# requirements as 'namespace' and 'name'
+tags:
+  - application
+  - pretix
+  - pretalx
+
+# Collections that this collection requires to be installed for it to be usable. The key of the dict is the
+# collection label 'namespace.name'. The value is a version range
+# L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version
+# range specifiers can be set and are separated by ','
+dependencies:
+  community.podman: ">=1.16.2,<2.0.0"
+  community.postgresql: ">=3.10.2,<4.0.0"
+
+# The URL of the originating SCM repository
+repository: https://git.kabelsalat.ch/s3lph/ansible-collection-conference
+
+# The URL to any online docs
+documentation: https://git.kabelsalat.ch/s3lph/ansible-collection-conference
+
+# The URL to the homepage of the collection/project
+homepage: https://git.kabelsalat.ch/s3lph/ansible-collection-conference
+
+# The URL to the collection issue tracker
+issues: https://git.kabelsalat.ch/s3lph/ansible-collection-conference/issues
+
+# A list of file glob-like patterns used to filter any files or directories that should not be included in the build
+# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This
+# uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry',
+# and '.git' are always filtered. Mutually exclusive with 'manifest'
+build_ignore: []
+
+# A dict controlling use of manifest directives used in building the collection artifact. The key 'directives' is a
+# list of MANIFEST.in style
+# L(directives,https://packaging.python.org/en/latest/guides/using-manifest-in/#manifest-in-commands). The key
+# 'omit_default_directives' is a boolean that controls whether the default directives are used. Mutually exclusive
+# with 'build_ignore'
+# manifest: null
diff --git a/meta/runtime.yml b/meta/runtime.yml
new file mode 100644
index 0000000..ab3941d
--- /dev/null
+++ b/meta/runtime.yml
@@ -0,0 +1,52 @@
+---
+# Collections must specify a minimum required ansible version to upload
+# to galaxy
+requires_ansible: '>=2.15'
+
+# Content that Ansible needs to load from another location or that has
+# been deprecated/removed
+# plugin_routing:
+#   action:
+#     redirected_plugin_name:
+#       redirect: ns.col.new_location
+#     deprecated_plugin_name:
+#       deprecation:
+#         removal_version: "4.0.0"
+#         warning_text: |
+#           See the porting guide on how to update your playbook to
+#           use ns.col.another_plugin instead.
+#     removed_plugin_name:
+#       tombstone:
+#         removal_version: "2.0.0"
+#         warning_text: |
+#           See the porting guide on how to update your playbook to
+#           use ns.col.another_plugin instead.
+#   become:
+#   cache:
+#   callback:
+#   cliconf:
+#   connection:
+#   doc_fragments:
+#   filter:
+#   httpapi:
+#   inventory:
+#   lookup:
+#   module_utils:
+#   modules:
+#   netconf:
+#   shell:
+#   strategy:
+#   terminal:
+#   test:
+#   vars:
+
+# Python import statements that Ansible needs to load from another location
+# import_redirection:
+#   ansible_collections.ns.col.plugins.module_utils.old_location:
+#     redirect: ansible_collections.ns.col.plugins.module_utils.new_location
+
+# Groups of actions/modules that take a common set of options
+# action_groups:
+#   group_name:
+#     - module1
+#     - module2
diff --git a/roles/pretalx/handlers/main.yml b/roles/pretalx/handlers/main.yml
new file mode 100644
index 0000000..f6cca73
--- /dev/null
+++ b/roles/pretalx/handlers/main.yml
@@ -0,0 +1,7 @@
+---
+
+- name: Restart Pretalx
+  community.podman.podman_container:
+    name: pretalx
+    state: started
+    restart: true
diff --git a/roles/pretalx/meta/argument_specs.yml b/roles/pretalx/meta/argument_specs.yml
new file mode 100644
index 0000000..38dccd6
--- /dev/null
+++ b/roles/pretalx/meta/argument_specs.yml
@@ -0,0 +1,181 @@
+---
+
+argument_specs:
+
+  main:
+    version_added: "0.0.1"
+    short_description: Install and configure Pretalx.
+    description:
+      - Install and configure the L(Pretalx,https://pretalx.com/p/about/) conference scheduling software.
+      - "Execution of this role can be limited using the following tags:"
+      - "C(role::pretalx:install): Install Pretalx, Valkey and PostgreSQL"
+      - "C(role::pretalx): Apply all of the above."
+    author: s3lph
+    options:
+
+      # Container image settings
+      pretalx_image:
+        description:
+          - OCI Container image name for Pretalx
+        type: str
+        default: docker.io/pretalx/standalone
+      pretalx_image_tag:
+        description:
+          - OCI Container image tag for Pretalx
+        type: str
+        default: "v2024.3.1"
+      pretalx_cache_image:
+        description:
+          - OCI Container image name for Valkey
+        type: str
+        default: docker.io/valkey/valkey
+      pretalx_cache_image_tag:
+        description:
+          - OCI Container image tag for Valkey
+        type: str
+        default: "8"
+      pretalx_http_hostaddr:
+        description:
+          - Host address to map to Pretalx http port
+        type: str
+        default: "[::1]"
+      pretalx_http_hostport:
+        description:
+          - Host port to map to Pretalx http port
+        type: int
+        default: 8081
+      pretalx_container_uid:
+        description:
+          - UID under which Pretalx runs inside the container.
+          - On the host, this is offset by O(pretalx_subuid_begin).
+          - You should not need to change this.
+        type: int
+        default: 999
+      pretalx_container_gid:
+        description:
+          - GID under which Pretalx runs inside the container.
+          - On the host, this is offset by O(pretalx_subgid_begin).
+          - You should not need to change this.
+        type: int
+        default: 999
+      pretalx_watchtower_enabled:
+        description:
+          - "Whether to enable automatic container updates through L(Watchtower,https://containrrr.dev/watchtower/)."
+          - "If this is true, you should set O(pretalx_image_tag) to something other than V(latest) or V(stable)."
+          - "See also: M(s3lph.conference.watchtower)."
+        type: bool
+        default: false
+
+      # System user settings
+      pretalx_system_group:
+        description:
+          - Name of the Pretalx system group.
+        type: str
+        default: pretalx
+      pretalx_system_user:
+        description:
+          - Name of the Pretalx system user.
+        type: str
+        default: pretalx
+      pretalx_system_home:
+        description:
+          - Home of the Pretalx system user.
+          - The persistent Pretalx data will be stored here.
+        type: str
+        default: /var/lib/pretalx
+      pretalx_subuid_begin:
+        description:
+          - First subordinate uid for the Pretalx system user.
+        type: int
+        default: 200000
+      pretalx_subuid_count:
+        description:
+          - Number of subordinate uids for the Pretalx system user.
+        type: int
+        default: 65536
+      pretalx_subgid_begin:
+        description:
+          - First subordinate gid for the Pretalx system user.
+        type: int
+        default: 200000
+      pretalx_subgid_count:
+        description:
+          - Number of subordinate gids for the Pretalx system user.
+        type: int
+        default: 65536
+
+      # Database settings
+      pretalx_postgresql_username:
+        description:
+          - PostgreSQL username to be used by Pretalx.
+        type: str
+        default: pretalx
+      #pretalx_postgresql_password:
+      #  description:
+      #    - PostgreSQL password to be used by Pretalx.
+      #  type: str
+      #  required: true
+      pretalx_postgresql_database:
+        description:
+          - PostgreSQL database to be used by Pretalx.
+        type: str
+        default: pretalx
+
+      # pretalx.cfg: main config section
+      pretalx_url:
+        description:
+          - Base URL of the Pretalx installation.
+        type: str
+        default: https://pretalx.example.org
+      pretalx_timezone:
+        description:
+          - Time zone of the Pretalx instance
+        type: str
+        default: UTC
+
+      # pretalx.cfg: mail config section
+      pretalx_mail_from:
+        description:
+          - Address from which Pretalx sends e-mail.
+        type: str
+        default: pretalx@example.org
+      pretalx_mail_host:
+        description:
+          - Host over which Pretalx sends e-mail.
+        type: str
+        default: 172.17.0.1
+      pretalx_mail_port:
+        description:
+          - Port over which Pretalx sends e-mail.
+          - "The default depends on the value if O(pretalx_tls_mode):"
+          - "O(pretalx_tls_mode=none): V(25)"
+          - "O(pretalx_tls_mode=tls): V(465)"
+          - "O(pretalx_tls_mode=starttls): V(587)"
+        type: int
+        default: 0
+      pretalx_mail_tls_mode:
+        description:
+          - TLS mode to use when Pretalx sends e-mail.
+        type: str
+        choices: ["none", "tls", "starttls"]
+        default: "none"
+      pretalx_mail_username:
+        description:
+          - If present, the username Pretalx uses to send e-mail.
+          - "Only takes effect if O(pretalx_mail_password) is set as well."
+        type: str
+        default: null
+      pretalx_mail_password:
+        description:
+          - If present, the password Pretalx uses to send e-mail.
+          - "Only takes effect if O(pretalx_mail_username) is set as well."
+        type: str
+        default: null
+
+      # pretalx.cfg: redis config section
+      pretalx_valkey_db_offset:
+        description:
+          - Valkey database index offset to be used by Pretalx.
+          - Pretalx uses this and the following two database indices.
+        type: int
+        default: 0
diff --git a/roles/pretalx/meta/main.yml b/roles/pretalx/meta/main.yml
new file mode 100644
index 0000000..2770508
--- /dev/null
+++ b/roles/pretalx/meta/main.yml
@@ -0,0 +1,42 @@
+---
+# SPDX-License-Identifier: MIT
+galaxy_info:
+  author: s3lph
+  description: Install and configure Pretalx.
+
+  # If the issue tracker for your role is not on github, uncomment the
+  # next line and provide a value
+  issue_tracker_url: https://git.kabelsalat.ch/s3lph/ansible-collection-conference/issues
+
+  # Choose a valid license ID from https://spdx.org - some suggested licenses:
+  # - BSD-3-Clause (default)
+  # - MIT
+  # - GPL-2.0-or-later
+  # - GPL-3.0-only
+  # - Apache-2.0
+  # - CC-BY-4.0
+  license: MIT
+
+  min_ansible_version: "2.15"
+
+  # If this a Container Enabled role, provide the minimum Ansible Container version.
+  # min_ansible_container_version:
+
+  platforms:
+    - name: Debian
+      versions:
+        - bullseye
+        - bookworm
+        - trixie
+
+  galaxy_tags: []
+  # List tags for your role here, one per line. A tag is a keyword that describes
+  # and categorizes the role. Users find roles by searching for tags. Be sure to
+  # remove the '[]' above, if you add tags to this list.
+  #
+  # NOTE: A tag is limited to a single word comprised of alphanumeric characters.
+  #       Maximum 20 tags per role.
+
+dependencies: []
+# List your role dependencies here, one per line. Be sure to remove the '[]' above,
+# if you add dependencies to this list.
diff --git a/roles/pretalx/tasks/install.yml b/roles/pretalx/tasks/install.yml
new file mode 100644
index 0000000..b9799d1
--- /dev/null
+++ b/roles/pretalx/tasks/install.yml
@@ -0,0 +1,110 @@
+---
+
+- name: Install dependencies
+  ansible.builtin.package:
+    name:
+      - podman
+      - postgresql
+      - python3-psycopg2
+
+- name: Create pretalx group
+  ansible.builtin.group:
+    name: "{{ pretalx_system_group }}"
+
+- name: Create pretalx user
+  ansible.builtin.user:
+    name: "{{ pretalx_system_user }}"
+    group: "{{ ptrealx_system_group }}"
+    home: "{{ pretalx_system_home }}"
+    password: '!'
+    shell: /usr/sbin/nologin
+    comment: Pretalx
+
+- name: Create subuid entry
+  ansible.builtin.lineinfile:
+    path: /etc/subuid
+    line: "{{ pretalx_system_user }}:{{ pretalx_subuid_begin }}:{{ pretalx_subuid_count }}"
+    regexp: "^{{ pretalx_system_user }}:"
+    create: true
+    owner: root
+    group: root
+    mode: "0644"
+
+- name: Create subgid entry
+  ansible.builtin.lineinfile:
+    path: /etc/subgid
+    line: "{{ pretalx_system_user }}:{{ pretalx_subgid_begin }}:{{ pretalx_subgid_count }}"
+    regexp: "^{{ pretalx_system_user }}:"
+    create: true
+    owner: root
+    group: root
+    mode: "0644"
+
+- name: Create pretalx directories
+  ansible.builtin.file:
+    path: "{{ pretalx_system_home }}/{{ item }}"
+    state: directory
+    owner: "{{ pretalx_subuid_begin + pretalx_container_uid }}"
+    group: "{{ pretalx_subgid_begin + pretalx_container_gid }}"
+    mode: "0750"
+  loop:
+    - data
+    - public
+
+- name: Create PostgreSQL user
+  community.postgresql.postgresql_user:
+    name: "{{ pretalx_postgresql_username }}"
+    #password: "{{ pretalx_postgresql_password }}"
+    #no_password_changes: true
+
+- name: Create PostgreSQL database
+  community.postgresql.postgresql_db:
+    name: "{{ pretalx_postgresql_database }}"
+    owner: "{{ pretalx_postgresql_username }}"
+
+- name: Create Pretalx configuration directory
+  ansible.builtin.file:
+    path: /etc/pretalx
+    owner: "{{ pretalx_system_user }}"
+    group: "{{ pretalx_system_group }}"
+    mode: "0755"
+    state: directory
+
+- name: Render Pretalx configuration
+  ansible.builtin.template:
+    src: etc/pretalx/pretalx.cfg.j2
+    dest: /etc/pretalx/pretalx.cfg
+    owner: "{{ pretalx_system_user }}"
+    group: "{{ pretalx_system_group }}"
+    mode: "0700"
+  notify: Restart Pretalx
+
+- name: Create pretalx-cache container
+  community.podman.podman_container:
+    name: pretalx-cache
+    image: "{{ pretalx_cache_image }}:{{ pretalx_cache_image_tag }}"
+    restart_policy: unless-stopped
+    labels:
+      com.centurylinklabs.watchtower.enable: "{{ pretalx_watchtower_enabled | ternary('true', 'false') }}"
+    subuidname: "{{ pretix_system_user }}"
+    subgidname: "{{ pretix_system_user }}"
+
+- name: Create pretalx container
+  community.podman.podman_container:
+    name: pretalx
+    image: "{{ pretalx_image }}:{{ pretalx_image_tag }}"
+    restart_policy: unless-stopped
+    ports:
+      - "{{ pretalx_http_hostaddr }}:{{ pretalx_http_hostport }}:80"
+    volumes:
+      - "/etc/pretalx:/etc/pretalx:ro"
+      - "{{ pretalx_system_home }}/data:/data"
+      - "{{ pretalx_system_home }}/public:/public"
+      - "/run/postgresql:/run/postgresql"
+    env:
+      PRETALX_FILESYSTEM_MEDIA: /public/media
+      PRETALX_FILESYSTEM_STATIC: /public/static
+    labels:
+      com.centurylinklabs.watchtower.enable: "{{ pretalx_watchtower_enabled | ternary('true', 'false') }}"
+    subuidname: "{{ pretix_system_user }}"
+    subgidname: "{{ pretix_system_user }}"
diff --git a/roles/pretalx/tasks/main.yml b/roles/pretalx/tasks/main.yml
new file mode 100644
index 0000000..be86796
--- /dev/null
+++ b/roles/pretalx/tasks/main.yml
@@ -0,0 +1,7 @@
+---
+
+- name: Install Pretalx
+  ansible.builtin.import_tasks: install.yml
+  tags:
+    - "role::pretalx"
+    - "role::pretalx:install"
diff --git a/roles/pretalx/templates/etc/pretalx/pretalx.cfg.j2 b/roles/pretalx/templates/etc/pretalx/pretalx.cfg.j2
new file mode 100644
index 0000000..7ccb675
--- /dev/null
+++ b/roles/pretalx/templates/etc/pretalx/pretalx.cfg.j2
@@ -0,0 +1,47 @@
+{{ ansible_managed | comment }}
+
+[site]
+debug=false
+url={{ pretalx_url }}
+
+[locale]
+time_zone={{ pretalx_timezone }}Europe/Zurich
+
+[database]
+backend=postgresql
+name={{ pretalx_postgresql_database }}
+user={{ pretalx_postgresql_user }}
+; password=*********
+host=
+
+[mail]
+from={{ pretalx_mail_from }}
+host={{ pretalx_mail_host }}
+{% if pretalx_mail_tls_mode == 'starttls' %}
+port={{ pretalx_mail_port or 587 }}
+tls=on
+ssl=off
+{% elif pretalx_mail_tls_mode == 'tls' %}
+port={{ pretalx_mail_port or 465 }}
+tls=off
+ssl=on
+{% else %}
+port={{ pretalx_mail_port or 25 }}
+tls=off
+ssl=off
+{% endif %}
+{% if pretalx_mail_username is not none and pretalx_mail_password is not none %}
+user={{ pretalx_mail_username }}
+password={{ pretalx_mail_password }}
+{% endif %}
+
+
+[redis]
+location=redis://pretalx-cache:6379/{{ pretalx_valkey_db_offset }}
+; Remove the following line if you are unsure about your redis' security
+; to reduce impact if redis gets compromised.
+sessions=true
+
+[celery]
+backend=redis://pretalx-cache:6379/{{ pretalx_valkey_db_offset + 1 }}
+broker=redis://pretalx-cache:6379/{{ pretalx_valkey_db_offset + 2 }}
diff --git a/roles/pretix/handlers/main.yml b/roles/pretix/handlers/main.yml
new file mode 100644
index 0000000..4b6a3a3
--- /dev/null
+++ b/roles/pretix/handlers/main.yml
@@ -0,0 +1,7 @@
+---
+
+- name: Restart Pretix
+  community.podman.podman_container:
+    name: pretix
+    state: started
+    restart: true
diff --git a/roles/pretix/meta/argument_specs.yml b/roles/pretix/meta/argument_specs.yml
new file mode 100644
index 0000000..6d6b5d1
--- /dev/null
+++ b/roles/pretix/meta/argument_specs.yml
@@ -0,0 +1,198 @@
+---
+
+argument_specs:
+
+  main:
+    version_added: "0.0.1"
+    short_description: Install and configure Pretix.
+    description:
+      - Install and configure the L(Pretix,https://pretix.eu) conference ticketing software.
+      - "Execution of this role can be limited using the following tags:"
+      - "C(role::pretix:install): Install Pretix, Valkey and PostgreSQL"
+      - "C(role::pretix): Apply all of the above."
+    author: s3lph
+    options:
+
+      # Container image settings
+      pretix_image:
+        description:
+          - OCI Container image name for Pretix
+        type: str
+        default: docker.io/pretix/standalone
+      pretix_image_tag:
+        description:
+          - OCI Container image tag for Pretix
+        type: str
+        default: "2025.2"
+      pretix_cache_image:
+        description:
+          - OCI Container image name for Redis
+        type: str
+        default: docker.io/valkey/valkey
+      pretix_cache_image_tag:
+        description:
+          - OCI Container image tag for Redis
+        type: str
+        default: "8"
+      pretix_http_hostpaddr:
+        description:
+          - Host address to map to Pretix http port
+        type: str
+        default: "[::1]"
+      pretix_http_hostport:
+        description:
+          - Host port to map to Pretix http port
+        type: int
+        default: 8080
+      pretix_container_uid:
+        description:
+          - UID under which Pretix runs inside the container.
+          - On the host, this is offset by O(pretix_subuid_begin).
+          - You should not need to change this.
+        type: int
+        default: 15371
+      pretix_container_gid:
+        description:
+          - GID under which Pretix runs inside the container.
+          - On the host, this is offset by O(pretix_subgid_begin).
+          - You should not need to change this.
+        type: int
+        default: 15371
+      pretix_watchtower_enabled:
+        description:
+          - "Whether to enable automatic container updates through L(Watchtower,https://containrrr.dev/watchtower/)."
+          - "If this is true, you should set O(pretix_image_tag) to something other than V(latest) or V(stable)."
+          - "See also: M(s3lph.conference.watchtower)."
+        type: bool
+        default: false
+
+      # System user settings
+      pretix_system_group:
+        description:
+          - Name of the Pretix system group.
+        type: str
+        default: pretix
+      pretix_system_user:
+        description:
+          - Name of the Pretix system user.
+        type: str
+        default: pretix
+      pretix_system_home:
+        description:
+          - Home of the Pretix system user.
+          - The persistent Pretix data will be stored here.
+        type: str
+        default: /var/lib/pretix
+      pretix_subuid_begin:
+        description:
+          - First subordinate uid for the Pretix system user.
+        type: int
+        default: 100000
+      pretix_subuid_count:
+        description:
+          - Number of subordinate uids for the Pretix system user.
+        type: int
+        default: 65536
+      pretix_subgid_begin:
+        description:
+          - First subordinate gid for the Pretix system user.
+        type: int
+        default: 100000
+      pretix_subgid_count:
+        description:
+          - Number of subordinate gids for the Pretix system user.
+        type: int
+        default: 65536
+
+      # Database settings
+      pretix_postgresql_username:
+        description:
+          - PostgreSQL username to be used by Pretix.
+        type: str
+        default: pretix
+      #pretix_postgresql_password:
+      #  description:
+      #    - PostgreSQL password to be used by Pretix.
+      #  type: str
+      #  required: true
+      pretix_postgresql_database:
+        description:
+          - PostgreSQL database to be used by Pretix.
+        type: str
+        default: pretix
+
+      # pretix.cfg: main config section
+      pretix_instance_name:
+        description:
+          - Name of the Pretix installation.
+        type: str
+        default: My pretix installation
+      pretix_url:
+        description:
+          - Base URL of the Pretix installation.
+        type: str
+        default: https://pretix.example.org
+      pretix_currency:
+        description:
+          - Currency to use in the Pretix installation.
+        type: str
+        default: EUR
+      pretix_registration:
+        description:
+          - Whether user signup in the Pretix installation should be enabled.
+        type: bool
+        default: false
+
+      # pretix.cfg: mail config section
+      pretix_mail_from:
+        description:
+          - Address from which Pretix sends e-mail.
+        type: str
+        default: pretix@example.org
+      pretix_mail_host:
+        description:
+          - Host over which Pretix sends e-mail.
+        type: str
+        default: 172.17.0.1
+      pretix_mail_port:
+        description:
+          - Port over which Pretix sends e-mail.
+          - "The default depends on the value if O(pretix_tls_mode):"
+          - "O(pretix_tls_mode=none): V(25)"
+          - "O(pretix_tls_mode=tls): V(465)"
+          - "O(pretix_tls_mode=starttls): V(587)"
+        type: int
+        default: 0
+      pretix_mail_tls_mode:
+        description:
+          - TLS mode to use when Pretix sends e-mail.
+        type: str
+        choices: ["none", "tls", "starttls"]
+        default: "none"
+      pretix_mail_username:
+        description:
+          - If present, the username Pretix uses to send e-mail.
+          - "Only takes effect if O(pretix_mail_password) is set as well."
+        type: str
+        default: null
+      pretix_mail_password:
+        description:
+          - If present, the password Pretix uses to send e-mail.
+          - "Only takes effect if O(pretix_mail_username) is set as well."
+        type: str
+        default: null
+
+      # pretix.cfg: redis config section
+      pretix_valkey_db_offset:
+        description:
+          - Valkey database index offset to be used by Pretix.
+          - Pretix uses this and the following two database indices.
+        type: int
+        default: 0
+
+      # Cronjob
+      pretix_cron:
+        description:
+          - The cron expression of when to execute Pretix jobs.
+        type: str
+        default: "*/15 * * * *"
diff --git a/roles/pretix/meta/main.yml b/roles/pretix/meta/main.yml
new file mode 100644
index 0000000..13e5d04
--- /dev/null
+++ b/roles/pretix/meta/main.yml
@@ -0,0 +1,42 @@
+---
+# SPDX-License-Identifier: MIT
+galaxy_info:
+  author: s3lph
+  description: Install and configure Pretix.
+
+  # If the issue tracker for your role is not on github, uncomment the
+  # next line and provide a value
+  issue_tracker_url: https://git.kabelsalat.ch/s3lph/ansible-collection-conference/issues
+
+  # Choose a valid license ID from https://spdx.org - some suggested licenses:
+  # - BSD-3-Clause (default)
+  # - MIT
+  # - GPL-2.0-or-later
+  # - GPL-3.0-only
+  # - Apache-2.0
+  # - CC-BY-4.0
+  license: MIT
+
+  min_ansible_version: "2.15"
+
+  # If this a Container Enabled role, provide the minimum Ansible Container version.
+  # min_ansible_container_version:
+
+  platforms:
+    - name: Debian
+      versions:
+        - bullseye
+        - bookworm
+        - trixie
+
+  galaxy_tags: []
+  # List tags for your role here, one per line. A tag is a keyword that describes
+  # and categorizes the role. Users find roles by searching for tags. Be sure to
+  # remove the '[]' above, if you add tags to this list.
+  #
+  # NOTE: A tag is limited to a single word comprised of alphanumeric characters.
+  #       Maximum 20 tags per role.
+
+dependencies: []
+# List your role dependencies here, one per line. Be sure to remove the '[]' above,
+# if you add dependencies to this list.
diff --git a/roles/pretix/tasks/install.yml b/roles/pretix/tasks/install.yml
new file mode 100644
index 0000000..f70b128
--- /dev/null
+++ b/roles/pretix/tasks/install.yml
@@ -0,0 +1,119 @@
+---
+
+- name: Install dependencies
+  ansible.builtin.package:
+    name:
+      - podman
+      - postgresql
+      - python3-psycopg2
+
+- name: Create pretix group
+  ansible.builtin.group:
+    name: "{{ pretix_system_group }}"
+
+- name: Create pretix user
+  ansible.builtin.user:
+    name: "{{ pretix_system_user }}"
+    group: "{{ pretix_system_group }}"
+    home: "{{ pretix_system_home }}"
+    password: '!'
+    shell: /usr/sbin/nologin
+    comment: Pretix
+
+- name: Create subuid entry
+  ansible.builtin.lineinfile:
+    path: /etc/subuid
+    line: "{{ pretix_system_user }}:{{ pretix_subuid_begin }}:{{ pretix_subuid_count }}"
+    regexp: "^{{ pretix_system_user }}:"
+    create: true
+    owner: root
+    group: root
+    mode: "0644"
+
+- name: Create subgid entry
+  ansible.builtin.lineinfile:
+    path: /etc/subgid
+    line: "{{ pretix_system_user }}:{{ pretix_subgid_begin }}:{{ pretix_subgid_count }}"
+    regexp: "^{{ pretix_system_user }}:"
+    create: true
+    owner: root
+    group: root
+    mode: "0644"
+
+- name: Create pretix data directory
+  ansible.builtin.file:
+    path: "{{ pretix_system_home }}/data"
+    state: directory
+    owner: "{{ pretix_subuid_begin + pretix_container_uid }}"
+    group: "{{ pretix_subgid_begin + pretix_container_gid }}"
+    mode: "0700"
+
+- name: Create PostgreSQL user
+  community.postgresql.postgresql_user:
+    name: "{{ pretix_postgresql_username }}"
+    #password: "{{ pretix_postgresql_password }}"
+    #no_password_changes: true
+
+- name: Create PostgreSQL database
+  community.postgresql.postgresql_db:
+    name: "{{ pretix_postgresql_database }}"
+    owner: "{{ pretix_postgresql_username }}"
+
+- name: Create Pretix configuration directory
+  ansible.builtin.file:
+    path: /etc/pretix
+    owner: "{{ pretix_system_user }}"
+    group: "{{ pretix_system_group }}"
+    mode: "0755"
+    state: directory
+
+- name: Render Pretix configuration
+  ansible.builtin.template:
+    src: etc/pretix/pretix.cfg.j2
+    dest: /etc/pretix/pretix.cfg
+    owner: "{{ pretix_system_user }}"
+    group: "{{ pretix_system_group }}"
+    mode: "0700"
+    notify: Restart Pretix
+
+- name: Create pretix-cache container
+  containers.podman.podman_container:
+    name: pretix-cache
+    image: "{{ pretix_cache_image }}:{{ pretix_cache_image_tag }}"
+    restart_policy: unless-stopped
+    labels:
+      com.centurylinklabs.watchtower.enable: "{{ pretix_watchtower_enabled | ternary('true', 'false') }}"
+    subuidname: "{{ pretix_system_user }}"
+    subgidname: "{{ pretix_system_user }}"
+
+- name: Create pretix container
+  containers.podman.podman_container:
+    name: pretix
+    image: "{{ pretix_image }}:{{ pretix_image_tag }}"
+    command: "all"
+    restart_policy: unless-stopped
+    ports:
+      - "{{ pretix_http_hostaddr }}:{{ pretix_http_hostport }}:80"
+    volumes:
+      - "/etc/pretix:/etc/pretix:ro"
+      - "{{ pretix_system_home }}/data:/data"
+      - "/run/postgresql:/run/postgresql"
+    sysctl:
+      net.core.somaxconn: "4096"
+    subuidname: "{{ pretix_system_user }}"
+    subgidname: "{{ pretix_system_user }}"
+    labels:
+      com.centurylinklabs.watchtower.enable: "{{ pretix_watchtower_enabled | ternary('true', 'false') }}"
+
+- name: Run Pretix upgrade
+  containers.podman.podman_container_exec:
+    container: pretix
+    command: pretix upgrade
+
+- name: Render Pretix cronjob
+  ansible.builtin.template:
+    src: etc/cron.d/pretix.j2
+    dest: /etc/cron.d/pretix
+    owner: root
+    group: root
+    mode: "0644"
diff --git a/roles/pretix/tasks/main.yml b/roles/pretix/tasks/main.yml
new file mode 100644
index 0000000..2b5b835
--- /dev/null
+++ b/roles/pretix/tasks/main.yml
@@ -0,0 +1,7 @@
+---
+
+- name: Install Pretix
+  ansible.builtin.import_tasks: install.yml
+  tags:
+    - "role::pretix"
+    - "role::pretix:install"
diff --git a/roles/pretix/templates/etc/cron.d/pretix.j2 b/roles/pretix/templates/etc/cron.d/pretix.j2
new file mode 100644
index 0000000..bc3d8fc
--- /dev/null
+++ b/roles/pretix/templates/etc/cron.d/pretix.j2
@@ -0,0 +1,3 @@
+{{ ansible_managed | comment }}
+
+{{ pretix_cron }}  {{ pretix_system_user }}  podmap exec pretix cron
diff --git a/roles/pretix/templates/etc/pretix/pretix.cfg.j2 b/roles/pretix/templates/etc/pretix/pretix.cfg.j2
new file mode 100644
index 0000000..bd4e281
--- /dev/null
+++ b/roles/pretix/templates/etc/pretix/pretix.cfg.j2
@@ -0,0 +1,47 @@
+{{ ansible_managed | comment }}
+
+instance_name={{ pretix_instance_name }}
+url={{ pretix_url }}
+currency={{ pretix_currency }}
+datadir=/data
+trust_x_forwarded_for=on
+trust_x_forwarded_proto=on
+registration={{ pretix_registration | ternary('on', 'off') }}
+
+[database]
+backend=postgresql
+name={{ pretix_postgresql_database }}
+user={{ pretix_postgresql_user }}
+host=
+
+[mail]
+from={{ pretix_mail_from }}
+host={{ pretix_mail_host }}
+{% if pretix_mail_tls_mode == 'starttls' %}
+port={{ pretix_mail_port or 587 }}
+tls=on
+ssl=off
+{% elif pretix_mail_tls_mode == 'tls' %}
+port={{ pretix_mail_port or 465 }}
+tls=off
+ssl=on
+{% else %}
+port={{ pretix_mail_port or 25 }}
+tls=off
+ssl=off
+{% endif %}
+{% if pretix_mail_username is not none and pretix_mail_password is not none %}
+user={{ pretix_mail_username }}
+password={{ pretix_mail_password }}
+{% endif %}
+
+
+[redis]
+location=redis://pretix-cache:6379/{{ pretix_valkey_db_offset }}
+; Remove the following line if you are unsure about your redis' security
+; to reduce impact if redis gets compromised.
+sessions=true
+
+[celery]
+backend=redis://pretix-cache:6379/{{ pretix_valkey_db_offset + 1 }}
+broker=redis://pretix-cache:6379/{{ pretix_valkey_db_offset + 2 }}
diff --git a/roles/watchtower/meta/argument_specs.yml b/roles/watchtower/meta/argument_specs.yml
new file mode 100644
index 0000000..8a6ad15
--- /dev/null
+++ b/roles/watchtower/meta/argument_specs.yml
@@ -0,0 +1,42 @@
+---
+
+argument_specs:
+
+  main:
+    version_added: "0.0.1"
+    short_description: Install and configure Watchtower.
+    description:
+      - Install and configure L(Watchtower,https://containrrr.dev/watchtower/).
+      - "Execution of this role can be limited using the following tags:"
+      - "C(role::watchtower:install): Install Podman and Watchtower"
+      - "C(role::watchtower): Apply all of the above."
+    author: s3lph
+    options:
+
+      watchtower_image:
+        description:
+          - The name of the OCI image to run.
+          - "See also: O(watchtower_image_tag)."
+        type: str
+        default: docker.io/containrrr/watchtower
+      watchtower_image_tag:
+        description:
+          - The tag of the OCI image to run
+          - "See also: O(watchtower_image)."
+        type: str
+        default: latest
+      watchtower_schedule:
+        description:
+          - Cron expression of when containers should be updated.
+          - If omitted, default to once every 24h without specifying a point in time.
+        type: str
+      watchtower_label_enable:
+        description:
+          - Whether Watchtower support has to be enabled explicitly via container labels.
+        type: bool
+        default: true
+      watchtower_update_self:
+        description:
+          - Whether Watchtower should update itself as well.
+        type: bool
+        default: true
diff --git a/roles/watchtower/meta/main.yml b/roles/watchtower/meta/main.yml
new file mode 100644
index 0000000..0889655
--- /dev/null
+++ b/roles/watchtower/meta/main.yml
@@ -0,0 +1,42 @@
+---
+# SPDX-License-Identifier: MIT
+galaxy_info:
+  author: s3lph
+  description: Install and configure Watchtower.
+
+  # If the issue tracker for your role is not on github, uncomment the
+  # next line and provide a value
+  issue_tracker_url: https://git.kabelsalat.ch/s3lph/ansible-collection-conference/issues
+
+  # Choose a valid license ID from https://spdx.org - some suggested licenses:
+  # - BSD-3-Clause (default)
+  # - MIT
+  # - GPL-2.0-or-later
+  # - GPL-3.0-only
+  # - Apache-2.0
+  # - CC-BY-4.0
+  license: MIT
+
+  min_ansible_version: "2.15"
+
+  # If this a Container Enabled role, provide the minimum Ansible Container version.
+  # min_ansible_container_version:
+
+  platforms:
+    - name: Debian
+      versions:
+        - bullseye
+        - bookworm
+        - trixie
+
+  galaxy_tags: []
+  # List tags for your role here, one per line. A tag is a keyword that describes
+  # and categorizes the role. Users find roles by searching for tags. Be sure to
+  # remove the '[]' above, if you add tags to this list.
+  #
+  # NOTE: A tag is limited to a single word comprised of alphanumeric characters.
+  #       Maximum 20 tags per role.
+
+dependencies: []
+# List your role dependencies here, one per line. Be sure to remove the '[]' above,
+# if you add dependencies to this list.
diff --git a/roles/watchtower/tasks/install.yml b/roles/watchtower/tasks/install.yml
new file mode 100644
index 0000000..b12e367
--- /dev/null
+++ b/roles/watchtower/tasks/install.yml
@@ -0,0 +1,25 @@
+---
+
+- name: Install dependencies
+  ansible.builtin.package:
+    name:
+      - podman
+
+- name: Enable podman socket
+  ansible.builtin.systemd_service:
+    name: podman.socket
+    state: started
+    enabled: true
+
+- name: Create Watchtower container
+  community.podman.podman_container:
+    name: watchtower
+    image: "{{ watchtower_image }}:{{ watchtower_image_tag }}"
+    env:
+      WATCHTOWER_SCHEDULE: "{{ watchtower_schedule | default(omit) }}"
+      WATCHTOWER_LABEL_ENABLE: "{{ watchtower_label_enable | default(omit) }}"
+    restart_policy: unless-stopped
+    volumes:
+      - "/run/podman/podman.sock:/var/run/docker.sock"
+    labels:
+      com.centurylinklabs.watchtower.enable: "{{ watchtower_update_self | ternary('true', 'false') }}"
diff --git a/roles/watchtower/tasks/main.yml b/roles/watchtower/tasks/main.yml
new file mode 100644
index 0000000..a0e6fc5
--- /dev/null
+++ b/roles/watchtower/tasks/main.yml
@@ -0,0 +1,7 @@
+---
+
+- name: Install Watchtower
+  ansible.builtin.import_tasks: install.yml
+  tags:
+    - "role::watchtower"
+    - "role::watchtower:install"