diff --git a/.ansible-lint b/.ansible-lint
new file mode 100644
index 0000000..7434f60
--- /dev/null
+++ b/.ansible-lint
@@ -0,0 +1,126 @@
+---
+# .ansible-lint
+
+profile: null   # min, basic, moderate,safety, shared, production
+
+# Allows dumping of results in SARIF format
+# sarif_file: result.sarif
+
+# exclude_paths included in this file are parsed relative to this file's location
+# and not relative to the CWD of execution. CLI arguments passed to the --exclude
+# option are parsed relative to the CWD of execution.
+exclude_paths:
+  - .cache/ # implicit unless exclude_paths is defined in config
+  - test/fixtures/formatting-before/
+  - test/fixtures/formatting-prettier/
+  - .ansible-lint
+  - roles/geerlingguy.docker
+# parseable: true
+# quiet: true
+# strict: true
+# verbosity: 1
+
+# Mock modules or roles in order to pass ansible-playbook --syntax-check
+mock_modules:
+  - zuul_return
+  # note the foo.bar is invalid as being neither a module or a collection
+  - fake_namespace.fake_collection.fake_module
+  - fake_namespace.fake_collection.fake_module.fake_submodule
+mock_roles:
+  - mocked_role
+  - author.role_name # old standalone galaxy role
+  - fake_namespace.fake_collection.fake_role # role within a collection
+
+# Enable checking of loop variable prefixes in roles
+loop_var_prefix: "^(__|{role}_)"
+
+# Enforce variable names to follow pattern below, in addition to Ansible own
+# requirements, like avoiding python identifiers. To disable add `var-naming`
+# to skip_list.
+var_naming_pattern: "^[a-z_][a-z0-9_]*$"
+
+use_default_rules: true
+# Load custom rules from this specific folder
+# rulesdir:
+#   - ./rule/directory/
+
+# Ansible-lint is able to recognize and load skip rules stored inside
+# `.ansible-lint-ignore` (or `.config/ansible-lint-ignore.txt`) files.
+# To skip a rule just enter filename and tag, like "playbook.yml package-latest"
+# on a new line.
+# Optionally you can add comments after the tag, prefixed by "#". We discourage
+# the use of skip_list below because that will hide violations from the output.
+# When putting ignores inside the ignore file, they are marked as ignored, but
+# still visible, making it easier to address later.
+skip_list:
+  - skip_this_tag
+
+# Ansible-lint does not automatically load rules that have the 'opt-in' tag.
+# You must enable opt-in rules by listing each rule 'id' below.
+enable_list:
+  - args
+  - empty-string-compare # opt-in
+  - no-log-password # opt-in
+  - no-same-owner # opt-in
+  - name[prefix] # opt-in
+  # add yaml here if you want to avoid ignoring yaml checks when yamllint
+  # library is missing. Normally its absence just skips using that rule.
+  - yaml
+# Report only a subset of tags and fully ignore any others
+# tags:
+#   - jinja[spacing]
+
+# Ansible-lint does not fail on warnings from the rules or tags listed below
+warn_list:
+  - skip_this_tag
+  - experimental # experimental is included in the implicit list
+  # - role-name
+  # - yaml[document-start]  # you can also use sub-rule matches
+
+# Some rules can transform files to fix (or make it easier to fix) identified
+# errors. `ansible-lint --fix` will reformat YAML files and run these transforms.
+# By default it will run all transforms (effectively `write_list: ["all"]`).
+# You can disable running transforms by setting `write_list: ["none"]`.
+# Or only enable a subset of rule transforms by listing rules/tags here.
+# write_list:
+#   - all
+
+# Offline mode disables installation of requirements.yml and schema refreshing
+offline: true
+
+# Define required Ansible's variables to satisfy syntax check
+extra_vars:
+  foo: bar
+  multiline_string_variable: |
+    line1
+    line2
+  complex_variable: ":{;\t$()"
+
+# Uncomment to enforce action validation with tasks, usually is not
+# needed as Ansible syntax check also covers it.
+# skip_action_validation: false
+
+# List of additional kind:pattern to be added at the top of the default
+# match list, first match determines the file kind.
+kinds:
+  # - playbook: "**/examples/*.{yml,yaml}"
+  # - galaxy: "**/folder/galaxy.yml"
+  # - tasks: "**/tasks/*.yml"
+  # - vars: "**/vars/*.yml"
+  # - meta: "**/meta/main.yml"
+  - yaml: "**/*.yaml-too"
+
+# List of additional collections to allow in only-builtins rule.
+# only_builtins_allow_collections:
+#   - example_ns.example_collection
+
+# List of additions modules to allow in only-builtins rule.
+# only_builtins_allow_modules:
+#   - example_module
+
+# Allow setting custom prefix for name[prefix] rule
+task_name_prefix: "{stem} | "
+# Complexity related settings
+
+# Limit the depth of the nested blocks:
+# max_block_depth: 20
\ No newline at end of file
diff --git a/.gitea/workflows/on_pr.yml b/.gitea/workflows/on_pr.yml
new file mode 100644
index 0000000..9424d0c
--- /dev/null
+++ b/.gitea/workflows/on_pr.yml
@@ -0,0 +1,28 @@
+---
+name: "Pull Request: build & lint"
+# yamllint disable-line rule:truthy
+on:
+  pull_request:
+
+jobs:
+  build:
+    runs-on: ubuntu-22.04
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - uses: actions/setup-python@v5
+        with:
+          python-version: '3.10.13'
+      - name: "Creates virtual environment"
+        run: make venv
+      - name: Activate virtualenv
+        run: |
+          source venv/bin/activate
+          echo PATH=$PATH >> $GITHUB_ENV
+      - name: Install galaxy requirements
+        run: ansible-galaxy install -r requirements.yml
+      - name: "Run YAML linting"
+        run: yamllint -c .yamllint.yml .
+      - name: "Run Ansible linting"
+        run: ansible-lint
diff --git a/.gitea/workflows/on_push_master.yml b/.gitea/workflows/on_push_master.yml
new file mode 100644
index 0000000..76fb2f4
--- /dev/null
+++ b/.gitea/workflows/on_push_master.yml
@@ -0,0 +1,30 @@
+---
+name: "Master branch: build & lint"
+# yamllint disable-line rule:truthy
+on:
+  push:
+    branches:
+      - master
+
+jobs:
+  build:
+    runs-on: ubuntu-22.04
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - uses: actions/setup-python@v5
+        with:
+          python-version: '3.10.13'
+      - name: "Creates virtual environment"
+        run: make venv
+      - name: Activate virtualenv
+        run: |
+          source venv/bin/activate
+          echo PATH=$PATH >> $GITHUB_ENV
+      - name: Install galaxy requirements
+        run: ansible-galaxy install -r requirements.yml
+      - name: "Run YAML linting"
+        run: yamllint -c .yamllint.yml .
+      - name: "Run Ansible linting"
+        run: ansible-lint
diff --git a/.gitignore b/.gitignore
index 1bc1426..d875a55 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,6 @@ bin
 *.crt
 *.pem
 synapse-data
+geerlingguy.docker
+geerlingguy.certbot
+venv
diff --git a/.yamllint.yml b/.yamllint.yml
new file mode 100644
index 0000000..ae15d5f
--- /dev/null
+++ b/.yamllint.yml
@@ -0,0 +1,41 @@
+---
+yaml-files:
+  - '*.yaml'
+  - '*.yml'
+  - '.yamllint'
+
+ignore:
+  - venv
+  - geerlingguy.docker
+  - geerlingguy.certbot
+
+rules:
+  anchors: enable
+  braces: enable
+  brackets: enable
+  colons: enable
+  commas: enable
+  comments:
+    level: warning
+  comments-indentation:
+    level: warning
+  document-end: disable
+  document-start:
+    level: warning
+  empty-lines: enable
+  empty-values: disable
+  float-values: disable
+  hyphens: enable
+  indentation: enable
+  key-duplicates: enable
+  key-ordering: disable
+  line-length:
+    max: 160
+    level: warning
+  new-line-at-end-of-file: enable
+  new-lines: enable
+  octal-values: disable
+  quoted-strings: disable
+  trailing-spaces: enable
+  truthy:
+    level: warning
diff --git a/Makefile b/Makefile
index 92b9595..9227d03 100644
--- a/Makefile
+++ b/Makefile
@@ -1,2 +1,9 @@
+venv:
+	if ! [ -d venv ]; then python3 -m venv venv; fi;
+	./venv/bin/pip install -r requirements.txt
+
 install:
 	ansible-galaxy install -r requirements.yml
+
+action:
+	act -W .gitea/workflows/on_pr.yml
\ No newline at end of file
diff --git a/ansible.cfg b/ansible.cfg
index 865284a..e031fc6 100644
--- a/ansible.cfg
+++ b/ansible.cfg
@@ -2,11 +2,7 @@
 remote_user = root
 remote_port = 22
 host_key_checking = False
-#hash_behaviour = merge
 pipelining=True
-fact_path = facts.d
-gathering = implicit
-gather_subset = all
 inject_facts_as_vars = True
 roles_path = roles
 inventory = inventory.ini
diff --git a/group_vars/all.yml b/group_vars/all.yml
index 9947b69..ac2fc20 100644
--- a/group_vars/all.yml
+++ b/group_vars/all.yml
@@ -1,6 +1,7 @@
 ---
 # Standard variables
-os: "{{ ansible_system|lower }}"
+os: "{{ ansible_system | lower }}"
+# yamllint disable-line rule:line-length
 arch: "{% if ansible_architecture == 'aarch64' %}arm64{% elif ansible_architecture == 'amd64' or ansible_architecture == 'x86_64' %}amd64{% elif ansible_architecture == 'armhf' %}armhf{% else %}{{ ansible_architecture }}{% endif %}"
 
 # Docker stuff
diff --git a/playbooks/base.yml b/playbooks/base.yml
index bf23827..643462a 100644
--- a/playbooks/base.yml
+++ b/playbooks/base.yml
@@ -1,5 +1,6 @@
 ---
-- hosts: all
+- name: "Install the base playbook"
+  hosts: all
   roles:
     - role: root_user
       tags: root_user
diff --git a/playbooks/docker.yml b/playbooks/docker.yml
index debda70..116a9e8 100644
--- a/playbooks/docker.yml
+++ b/playbooks/docker.yml
@@ -1,4 +1,5 @@
 ---
-- hosts: all
+- name: "Install docker"
+  hosts: all
   roles:
     - geerlingguy.docker
diff --git a/playbooks/reboot.yml b/playbooks/reboot.yml
index bd59e96..6e77292 100644
--- a/playbooks/reboot.yml
+++ b/playbooks/reboot.yml
@@ -1,6 +1,7 @@
 ---
-- hosts: all
+- name: "Reboot hosts"
+  hosts: all
   tasks:
-  - name: Reboot
-    reboot:
-      reboot_timeout: 3600
\ No newline at end of file
+    - name: Reboot
+      ansible.builtin.reboot:
+        reboot_timeout: 3600
diff --git a/playbooks/upgrade.yml b/playbooks/upgrade.yml
index f3414ab..3bb01fa 100644
--- a/playbooks/upgrade.yml
+++ b/playbooks/upgrade.yml
@@ -1,9 +1,17 @@
 ---
-- hosts: all
+- name: "Upgrade hosts"
+  hosts: all
   tasks:
-  - name: "update apt cache"
-    apt: update_cache=yes force_apt_get=yes cache_valid_time=3600
-  - name: "upgrade packages"
-    apt: upgrade=safe force_apt_get=yes
-  - name: "upgrade dist packages"
-    apt: upgrade=dist force_apt_get=yes
+    - name: "Run update apt cache"
+      ansible.builtin.apt:
+        update_cache: true
+        force_apt_get: true
+        cache_valid_time: 3600
+    - name: "Run upgrade packages"
+      ansible.builtin.apt:
+        upgrade: safe
+        force_apt_get: true
+    - name: "Run upgrade dist packages"
+      ansible.builtin.apt:
+        upgrade: dist
+        force_apt_get: true
diff --git a/requirements.txt b/requirements.txt
index 0d48dfc..6fe9594 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,10 +1,33 @@
-ansible==7.1.0
-ansible-core==2.14.1
+ansible==9.2.0
+ansible-compat==4.1.11
+ansible-core==2.16.3
+ansible-lint==24.2.0
+attrs==23.2.0
+black==24.2.0
+bracex==2.4
 cffi==1.15.1
+click==8.1.7
 cryptography==39.0.0
+filelock==3.13.1
 Jinja2==3.1.2
+jsonschema==4.21.1
+jsonschema-specifications==2023.12.1
+markdown-it-py==3.0.0
 MarkupSafe==2.1.1
+mdurl==0.1.2
+mypy-extensions==1.0.0
 packaging==23.0
+pathspec==0.12.1
+platformdirs==4.2.0
 pycparser==2.21
+Pygments==2.17.2
 PyYAML==6.0
+referencing==0.33.0
 resolvelib==0.8.1
+rich==13.7.0
+rpds-py==0.18.0
+ruamel.yaml==0.18.6
+ruamel.yaml.clib==0.2.8
+subprocess-tee==0.4.1
+wcmatch==8.5
+yamllint==1.35.1
diff --git a/roles/base/tasks/main.yml b/roles/base/tasks/main.yml
index 0adeb9c..4418e83 100644
--- a/roles/base/tasks/main.yml
+++ b/roles/base/tasks/main.yml
@@ -13,7 +13,7 @@
 # the packages you want to install just as you would pass them
 # to `apt install`.
 - name: "Install wanted packages"
-  apt:
+  ansible.builtin.apt:
     name:
       - apt-transport-https
       - arptables
@@ -70,7 +70,7 @@
       - zip
     # state: latest will update the package everytime the
     # role is ran against a host
-    state: latest
+    state: present
     # Update the cache before trying to update ?
     # You generally want this because it's not updating itself
     update_cache: true
@@ -82,7 +82,7 @@
   until: apt_res is success
 # Same as above, except you *remove* packages instead of installing them
 - name: "Remove unanted packages"
-  apt:
+  ansible.builtin.apt:
     name:
       - ntpdate
     # note the value of `state` here
@@ -91,7 +91,7 @@
 # that is golbally available. It will map to the hostname you
 # assigned your host in the `inventory` file
 - name: "Hard set hostname"
-  template:
+  ansible.builtin.template:
     src: hostname.j2
     dest: /etc/hostname
     owner: root
@@ -101,14 +101,14 @@
     # more info https://docs.ansible.com/ansible/latest/collections/ansible/builtin/file_module.html#parameter-mode
     mode: "0644"
 - name: "Setup motd"
-  template:
+  ansible.builtin.template:
     src: motd.j2
     dest: /etc/motd
     owner: root
     group: root
     mode: "0644"
 - name: "Setup hosts"
-  template:
+  ansible.builtin.template:
     src: hosts.j2
     dest: /etc/hosts
     owner: root
diff --git a/roles/ntp/handlers/main.yml b/roles/ntp/handlers/main.yml
index 7bddadd..ec5d7f2 100644
--- a/roles/ntp/handlers/main.yml
+++ b/roles/ntp/handlers/main.yml
@@ -3,6 +3,6 @@
 # in this example you want to invoke this restart handler when the
 # configuration of the service changes for example.
 - name: "Restart ntp"
-  service:
+  ansible.builtin.service:
     name: ntp
     state: restarted
diff --git a/roles/ntp/tasks/main.yml b/roles/ntp/tasks/main.yml
index 205077b..ea74084 100644
--- a/roles/ntp/tasks/main.yml
+++ b/roles/ntp/tasks/main.yml
@@ -1,10 +1,10 @@
 ---
 - name: "Install ntp"
-  apt:
+  ansible.builtin.apt:
     name: ntp
     state: present
 - name: "Configure ntp"
-  copy:
+  ansible.builtin.copy:
     src: ntp.conf
     dest: /etc/ntp.conf
     owner: root
@@ -17,8 +17,8 @@
   notify: "Restart ntp"
 # Here you say that you want the NTP service to be restarted as well
 # as enabled on boot.
-- name: "ntp service"
-  service:
+- name: "NTP service"
+  ansible.builtin.service:
     name: ntp
     state: restarted
     enabled: true
diff --git a/roles/root_user/tasks/main.yml b/roles/root_user/tasks/main.yml
index 4d91c30..0a13d24 100644
--- a/roles/root_user/tasks/main.yml
+++ b/roles/root_user/tasks/main.yml
@@ -1,14 +1,14 @@
 ---
 # Ensures the .ssh directory exists
-- name: "creates the .ssh root directory"
-  file:
+- name: "Creates the .ssh root directory"
+  ansible.builtin.file:
     path: "/root/.ssh"
     state: directory
     owner: root
     group: root
     mode: 0700
 - name: "Install root SSH keys"
-  template:
+  ansible.builtin.template:
     src: authorized_keys.j2
     dest: /root/.ssh/authorized_keys
     owner: root
@@ -17,8 +17,8 @@
 # Delete users you don't need
 # respectively you can also *add* users
 # https://docs.ansible.com/ansible/latest/collections/ansible/builtin/user_module.html#ansible-collections-ansible-builtin-user-module
-- name: "Delete usual cloud users user"
-  user:
+- name: "Delete usual cloud users user"  # noqa: loop-var-prefix
+  ansible.builtin.user:
     name: "{{ item }}"
     state: absent
     remove: true
diff --git a/roles/vim/tasks/main.yml b/roles/vim/tasks/main.yml
index 5cdccff..cb18ee1 100644
--- a/roles/vim/tasks/main.yml
+++ b/roles/vim/tasks/main.yml
@@ -1,12 +1,13 @@
 ---
 - name: Install vim
-  apt:
+  ansible.builtin.apt:
     name: vim-nox
-    state: latest
+    state: present
 # use the `copy` module to copy files to the remote host
 - name: Configure vim
-  copy:
+  ansible.builtin.copy:
     src: vimrc
     dest: /etc/vim/vimrc
     owner: root
     group: root
+    mode: "0650"