diff --git a/README.md b/README.md
index 68c92b4..6906a7f 100644
--- a/README.md
+++ b/README.md
@@ -45,3 +45,8 @@ Im Ansible-Repo müssen diese Sachen hinzugefügt werden:
     * Individuelle Config für den Service. Wenn Docker Compose, hier weiterleiten auf den eigentlichen Dienst in Compose.
     * Cert-Dateinamen anpassen
 * `resources/chaosknoten/`*host*`/docker_compose/compose.yaml.j2`: Config für Docker Compose (wenn verwendet)
+
+## License
+
+This CCCHH ansible-ccchh repository is licensed under the [MIT License](./LICENSE).  
+[`custom_pipeline_oidc_group_and_role_mapping.py`](./roles/netbox/files/custom_pipeline_oidc_group_and_role_mapping.py) is licensed under the Creative Commons: CC BY-SA 4.0 license.
diff --git a/inventories/chaosknoten/host_vars/netbox.yaml b/inventories/chaosknoten/host_vars/netbox.yaml
new file mode 100644
index 0000000..2304112
--- /dev/null
+++ b/inventories/chaosknoten/host_vars/netbox.yaml
@@ -0,0 +1,16 @@
+netbox__version: "v4.1.7"
+netbox__db_password: "{{ lookup('community.general.passwordstore', 'noc/vm-secrets/chaosknoten/netbox/DATABASE_PASSWORD', create=false, missing='error') }}"
+netbox__config: "{{ lookup('ansible.builtin.template', 'resources/chaosknoten/netbox/netbox/configuration.py.j2') }}"
+netbox__custom_pipeline_oidc_group_and_role_mapping: true
+
+nginx__version_spec: ""
+nginx__configurations:
+  - name: netbox.hamburg.ccc.de
+    content: "{{ lookup('ansible.builtin.file', 'resources/chaosknoten/netbox/nginx/netbox.hamburg.ccc.de.conf') }}"
+
+certbot__version_spec: ""
+certbot__acme_account_email_address: j+letsencrypt-ccchh@jsts.xyz
+certbot__certificate_domains:
+  - "netbox.hamburg.ccc.de"
+certbot__new_cert_commands:
+  - "systemctl reload nginx.service"
diff --git a/inventories/chaosknoten/hosts.yaml b/inventories/chaosknoten/hosts.yaml
index 911a87d..0f10bea 100644
--- a/inventories/chaosknoten/hosts.yaml
+++ b/inventories/chaosknoten/hosts.yaml
@@ -32,6 +32,10 @@ all:
         mumble:
           ansible_host: mumble.hamburg.ccc.de
           ansible_user: chaos
+        netbox:
+          ansible_host: netbox-intern.hamburg.ccc.de
+          ansible_user: chaos
+          ansible_ssh_common_args: -J ssh://chaos@public-reverse-proxy.hamburg.ccc.de
         onlyoffice:
           ansible_host: onlyoffice-intern.hamburg.ccc.de
           ansible_user: chaos
@@ -64,6 +68,7 @@ all:
         keycloak:
         lists:
         mumble:
+        netbox:
         onlyoffice:
         pad:
         pretalx:
@@ -94,6 +99,7 @@ all:
         keycloak:
         lists:
         mumble:
+        netbox:
         onlyoffice:
         pad:
         pretalx:
@@ -112,6 +118,7 @@ all:
         keycloak:
         lists:
         mumble:
+        netbox:
         onlyoffice:
         pad:
         pretalx:
@@ -123,6 +130,7 @@ all:
         eh22-wiki:
         tickets:
         keycloak:
+        netbox:
         onlyoffice:
         pad:
         pretalx:
@@ -136,6 +144,7 @@ all:
         tickets:
         cloud:
         keycloak:
+        netbox:
         onlyoffice:
         pad:
         pretalx:
@@ -146,3 +155,6 @@ all:
       hosts:
         eh22-wiki:
         wiki:
+    netbox_hosts:
+      hosts:
+        netbox:
diff --git a/playbooks/deploy.yaml b/playbooks/deploy.yaml
index da2937f..66f03de 100644
--- a/playbooks/deploy.yaml
+++ b/playbooks/deploy.yaml
@@ -29,6 +29,11 @@
   roles:
     - dokuwiki
 
+- name: Ensure NetBox deployment on netbox_hosts
+  hosts: netbox_hosts
+  roles:
+    - netbox
+
 - name: Ensure NGINX deployment on nginx_hosts, which are also public_reverse_proxy_hosts, before certbot role runs
   hosts: nginx_hosts:&public_reverse_proxy_hosts
   roles:
diff --git a/resources/chaosknoten/netbox/netbox/configuration.py.j2 b/resources/chaosknoten/netbox/netbox/configuration.py.j2
new file mode 100644
index 0000000..789a539
--- /dev/null
+++ b/resources/chaosknoten/netbox/netbox/configuration.py.j2
@@ -0,0 +1,60 @@
+ALLOWED_HOSTS = [ "netbox.hamburg.ccc.de" ]
+DATABASE = {
+  "HOST": "localhost",
+  "NAME": "netbox",
+  "USER": "netbox",
+  "PASSWORD": "{{ lookup('community.general.passwordstore', 'noc/vm-secrets/chaosknoten/netbox/DATABASE_PASSWORD', create=false, missing='error') }}",
+}
+REDIS = {
+    "tasks": {
+      "HOST": "localhost",
+      "PORT": 6379,
+      "USERNAME": "",
+      "PASSWORD": "",
+      "DATABASE": 0,
+      "SSL": False,
+    },
+    "caching": {
+      "HOST": "localhost",
+      "PORT": 6379,
+      "USERNAME": "",
+      "PASSWORD": "",
+      "DATABASE": 1,
+      "SSL": False,
+    },
+}
+SECRET_KEY = "{{ lookup('community.general.passwordstore', 'noc/vm-secrets/chaosknoten/netbox/SECRET_KEY', create=false, missing='error') }}"
+SESSION_COOKIE_SECURE = True
+
+# CCCHH ID (Keycloak) integration.
+# https://github.com/python-social-auth/social-core/blob/0925304a9e437f8b729862687d3a808c7fb88a95/social_core/backends/keycloak.py#L7
+# https://python-social-auth.readthedocs.io/en/latest/backends/keycloak.html
+REMOTE_AUTH_BACKEND = "social_core.backends.keycloak.KeycloakOAuth2"
+SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL = (
+    "https://id.hamburg.ccc.de/realms/ccchh/protocol/openid-connect/token"
+)
+SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL = (
+    "https://id.hamburg.ccc.de/realms/ccchh/protocol/openid-connect/auth"
+)
+SOCIAL_AUTH_KEYCLOAK_KEY = "netbox"
+SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi/Shi+b2OyYNGVFPsa6qf9SesEpRl5U5rpwgmt8H7NawMvwpPUYVW9o46QW0ulYcDmysT3BzpP3tagO/SFNoOjZdYe0D9nJ7vEp8KHbzR09KCfkyQIi0wLssKnDotVHL5JeUY+iKk+gjiwF9FSFSHPBqsST7hXVAut9LkOvs2aDod9AzbTH/uYbt4wfUm5l/1Ii8D+K7YcsFGUIqxv4XS/ylKqObqN4M2dac69iIwapoh6reaBQEm66vrOzJ+3yi4DZuPrkShJqi2hddtoyZihyCkF+eJJKEI5LrBf1KZB3Ec2YUrqk93ZGUGs/XY6R87QSfR3hJ82B1wnF+c2pw+QIDAQAB"
+SOCIAL_AUTH_KEYCLOAK_SECRET = "{{ lookup('community.general.passwordstore', 'noc/vm-secrets/chaosknoten/netbox/SOCIAL_AUTH_KEYCLOAK_SECRET', create=false, missing='error') }}"
+# Use custom OIDC group and role mapping pipeline functions added in via
+# netbox__custom_pipeline_oidc_group_and_role_mapping.
+# The default pipeline this is based on can be found here:
+# https://github.com/netbox-community/netbox/blob/main/netbox/netbox/settings.py
+SOCIAL_AUTH_PIPELINE = [
+    "social_core.pipeline.social_auth.social_details",
+    "social_core.pipeline.social_auth.social_uid",
+    "social_core.pipeline.social_auth.social_user",
+    "social_core.pipeline.user.get_username",
+    "social_core.pipeline.user.create_user",
+    "social_core.pipeline.social_auth.associate_user",
+    "netbox.authentication.user_default_groups_handler",
+    "social_core.pipeline.social_auth.load_extra_data",
+    "social_core.pipeline.user.user_details",
+    # Custom OIDC group and role mapping functions.
+    "netbox.custom_pipeline_oidc_mapping.add_groups",
+    "netbox.custom_pipeline_oidc_mapping.remove_groups",
+    "netbox.custom_pipeline_oidc_mapping.set_roles",
+]
diff --git a/resources/chaosknoten/netbox/nginx/netbox.hamburg.ccc.de.conf b/resources/chaosknoten/netbox/nginx/netbox.hamburg.ccc.de.conf
new file mode 100644
index 0000000..5550686
--- /dev/null
+++ b/resources/chaosknoten/netbox/nginx/netbox.hamburg.ccc.de.conf
@@ -0,0 +1,48 @@
+# partly generated 2022-01-08, Mozilla Guideline v5.6, nginx 1.17.7, OpenSSL 1.1.1k, intermediate configuration
+# https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=intermediate&openssl=1.1.1k&guideline=5.6
+server {
+    # Listen on a custom port for the proxy protocol.
+    listen 8443 ssl http2 proxy_protocol;
+    # Make use of the ngx_http_realip_module to set the $remote_addr and
+    # $remote_port to the client address and client port, when using proxy
+    # protocol.
+    # First set our proxy protocol proxy as trusted.
+    set_real_ip_from 172.31.17.140;
+    # Then tell the realip_module to get the addreses from the proxy protocol
+    # header.
+    real_ip_header proxy_protocol;
+
+    server_name netbox.hamburg.ccc.de;
+
+    ssl_certificate /etc/letsencrypt/live/netbox.hamburg.ccc.de/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/netbox.hamburg.ccc.de/privkey.pem;
+    # verify chain of trust of OCSP response using Root CA and Intermediate certs
+    ssl_trusted_certificate /etc/letsencrypt/live/netbox.hamburg.ccc.de/chain.pem;
+
+    # HSTS (ngx_http_headers_module is required) (63072000 seconds)
+    add_header Strict-Transport-Security "max-age=63072000" always;
+
+    proxy_set_header Host $host;
+    proxy_set_header X-Forwarded-Host $host;
+    proxy_set_header X-Real-IP $remote_addr;
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_set_header X-Forwarded-Port 443;
+    # This is https in any case.
+    proxy_set_header X-Forwarded-Proto https;
+    # Hide the X-Forwarded header.
+    proxy_hide_header X-Forwarded;
+    # Assume we are the only Reverse Proxy (well using Proxy Protocol, but that
+    # is transparent).
+    # Also provide "_hidden" for by, since it's not relevant.
+    proxy_set_header Forwarded "for=$remote_addr;proto=https;host=$host;by=_hidden";
+
+    client_max_body_size 25m;
+
+    location /static/ {
+        alias /opt/netbox/netbox/static/;
+    }
+
+    location / {
+        proxy_pass http://127.0.0.1:8001;
+    }
+}
diff --git a/resources/chaosknoten/public-reverse-proxy/nginx/acme_challenge.conf b/resources/chaosknoten/public-reverse-proxy/nginx/acme_challenge.conf
index d5ae146..c3f9fed 100644
--- a/resources/chaosknoten/public-reverse-proxy/nginx/acme_challenge.conf
+++ b/resources/chaosknoten/public-reverse-proxy/nginx/acme_challenge.conf
@@ -17,7 +17,7 @@ map $host $upstream_acme_challenge_host {
     invite.hamburg.ccc.de 172.31.17.144:31820;
     keycloak-admin.hamburg.ccc.de 172.31.17.144:31820;
     matrix.hamburg.ccc.de 172.31.17.150:31820;
-    netbox.hamburg.ccc.de 172.31.17.149:31820;
+    netbox.hamburg.ccc.de 172.31.17.167:31820;
     onlyoffice.hamburg.ccc.de 172.31.17.147:31820;
     pad.hamburg.ccc.de 172.31.17.141:31820;
     pretalx.hamburg.ccc.de 172.31.17.157:31820;
diff --git a/resources/chaosknoten/public-reverse-proxy/nginx/nginx.conf b/resources/chaosknoten/public-reverse-proxy/nginx/nginx.conf
index 0529f4c..dfcf8d2 100644
--- a/resources/chaosknoten/public-reverse-proxy/nginx/nginx.conf
+++ b/resources/chaosknoten/public-reverse-proxy/nginx/nginx.conf
@@ -32,7 +32,7 @@ stream {
         onlyoffice.hamburg.ccc.de 172.31.17.147:8443;
         hackertours.hamburg.ccc.de 172.31.17.151:8443;
         staging.hackertours.hamburg.ccc.de 172.31.17.151:8443;
-        netbox.hamburg.ccc.de 172.31.17.149:8443;
+        netbox.hamburg.ccc.de 172.31.17.167:8443;
         matrix.hamburg.ccc.de 172.31.17.150:8443;
         element.hamburg.ccc.de 172.31.17.151:8443;
         branding-resources.hamburg.ccc.de 172.31.17.151:8443;
diff --git a/roles/netbox/README.md b/roles/netbox/README.md
new file mode 100644
index 0000000..594e8f8
--- /dev/null
+++ b/roles/netbox/README.md
@@ -0,0 +1,88 @@
+# `netbox` role
+
+A role for setting up NetBox.  
+It automatically pulls in all required dependencies like Redis and PostgreSQL, deploys the provided systemd services and gunicorn config and sets up a PostgreSQL database named `netbox` with an owner named `netbox` and the specified password.
+However providing the [NetBox configuration](#netbox-configuration), [setting up a web server like nginx to proxy to gunicorn](#web-server-setup) and tasks like creating users, etc. you have to do yourself.
+
+## Supported Distributions
+
+Should work on Debian-based distributions.
+
+## Required Arguments
+
+- `netbox__version`: The NetBox version to deploy.
+- `netbox__db_password`: The password to use for connection to the database.
+  This is required since the upgrade script runs as root and therefore peer authentication doesn't work.
+- `netbox__config`: The NetBox config to deploy.
+  See [NetBox Configuration](#netbox-configuration) for more infos.
+
+## Optional Arguments
+
+- `netbox__custom_pipeline_oidc_group_and_role_mapping`: Whether or not to have custom pipeline code for OIDC group and role mapping present.
+  See [Custom Pipeline Code for OIDC Group and Role Mapping](#custom-pipeline-code-for-oidc-group-and-role-mapping) for more infos.  
+  Defaults to `false`.
+
+## NetBox Configuration
+
+The NetBox configuration should include a connection to Redis as well as a connection to PostgreSQL.  
+Configuration for the Redis connection:
+
+```python
+REDIS = {
+    "tasks": {
+      "HOST": "localhost",
+      "PORT": 6379,
+      "USERNAME": "",
+      "PASSWORD": "",
+      "DATABASE": 0,
+      "SSL": False,
+    },
+    "caching": {
+      "HOST": "localhost",
+      "PORT": 6379,
+      "USERNAME": "",
+      "PASSWORD": "",
+      "DATABASE": 1,
+      "SSL": False,
+    },
+}
+```
+
+Configuration for the PostgreSQL connection:
+
+```python
+DATABASE = {
+  "HOST": "localhost",
+  "NAME": "netbox",
+  "USER": "netbox",
+  "PASSWORD": "<same as netbox__db_password>",
+}
+```
+
+Further configuration should take place. Some relevant resources can be found here:
+
+- Installation guide configuration docs: <https://netboxlabs.com/docs/netbox/en/stable/installation/3-netbox/#configuration>
+- Configuration docs: <https://netboxlabs.com/docs/netbox/en/stable/configuration/>
+- Example configuration: <https://github.com/netbox-community/netbox/blob/main/netbox/netbox/configuration_example.py>
+
+## Web Server Setup
+
+As this role just sets up gunicorn, but doesn't set up a web server, you need to do that yourself.  
+The relevant documentation on how to do that can be found here:
+
+- Web server setup docs: <https://netboxlabs.com/docs/netbox/en/stable/installation/5-http-server/>
+- Example base nginx config: <https://github.com/netbox-community/netbox/blob/main/contrib/nginx.conf>
+
+## Custom Pipeline Code for OIDC Group and Role Mapping
+
+Setting the option `netbox__custom_pipeline_oidc_group_and_role_mapping` to `true` makes this role ensure custom pipeline code for OIDC group and role mapping is present.
+Note that this role uses code for NetBox >= 4.0.0.  
+The code is available in `files/custom_pipeline_oidc_group_and_role_mapping.py`, licensed under the CC BY-SA 4.0 license and taken from [this authentik NetBox documentation](https://docs.goauthentik.io/integrations/services/netbox/).
+The documentation also shows how to use the pipeline code by defining a custom `SOCIAL_AUTH_PIPELINE`, which you also need to do, as the configuration isn't provided by this role.
+However instead of under `netbox.custom_pipeline.` the functions are available under `netbox.custom_pipeline_oidc_mapping.` with this role.
+See also [the default settings.py](https://github.com/netbox-community/netbox/blob/main/netbox/netbox/settings.py) for the default `SOCIAL_AUTH_PIPELINE`.
+
+## Links & Resources
+
+- The NetBox Git Repo: <https://github.com/netbox-community/netbox>
+- The NetBox installation docs: <https://netboxlabs.com/docs/netbox/en/stable/installation/>
diff --git a/roles/netbox/defaults/main.yaml b/roles/netbox/defaults/main.yaml
new file mode 100644
index 0000000..49b518e
--- /dev/null
+++ b/roles/netbox/defaults/main.yaml
@@ -0,0 +1 @@
+netbox__custom_pipeline_oidc_group_and_role_mapping: false
diff --git a/roles/netbox/files/custom_pipeline_oidc_group_and_role_mapping.py b/roles/netbox/files/custom_pipeline_oidc_group_and_role_mapping.py
new file mode 100644
index 0000000..470f388
--- /dev/null
+++ b/roles/netbox/files/custom_pipeline_oidc_group_and_role_mapping.py
@@ -0,0 +1,55 @@
+# Licensed under Creative Commons: CC BY-SA 4.0 license.
+# https://github.com/goauthentik/authentik/blob/main/LICENSE
+# https://github.com/goauthentik/authentik/blob/main/website/integrations/services/netbox/index.md
+# https://docs.goauthentik.io/integrations/services/netbox/
+from netbox.authentication import Group
+
+class AuthFailed(Exception):
+    pass
+
+def add_groups(response, user, backend, *args, **kwargs):
+    try:
+        groups = response['groups']
+    except KeyError:
+        pass
+
+    # Add all groups from oAuth token
+    for group in groups:
+        group, created = Group.objects.get_or_create(name=group)
+        user.groups.add(group)
+
+def remove_groups(response, user, backend, *args, **kwargs):
+    try:
+        groups = response['groups']
+    except KeyError:
+        # Remove all groups if no groups in oAuth token
+        user.groups.clear()
+        pass
+
+    # Get all groups of user
+    user_groups = [item.name for item in user.groups.all()]
+    # Get groups of user which are not part of oAuth token
+    delete_groups = list(set(user_groups) - set(groups))
+
+    # Delete non oAuth token groups
+    for delete_group in delete_groups:
+        group = Group.objects.get(name=delete_group)
+        user.groups.remove(group)
+
+
+def set_roles(response, user, backend, *args, **kwargs):
+    # Remove Roles temporary
+    user.is_superuser = False
+    user.is_staff = False
+    try:
+        groups = response['groups']
+    except KeyError:
+        # When no groups are set
+        # save the user without Roles
+        user.save()
+        pass
+
+    # Set roles is role (superuser or staff) is in groups
+    user.is_superuser = True if 'superusers' in groups else False
+    user.is_staff = True if 'staff' in groups else False
+    user.save()
diff --git a/roles/netbox/handlers/main.yaml b/roles/netbox/handlers/main.yaml
new file mode 100644
index 0000000..fd7eb62
--- /dev/null
+++ b/roles/netbox/handlers/main.yaml
@@ -0,0 +1,24 @@
+- name: Run upgrade script
+  ansible.builtin.command: /opt/netbox/upgrade.sh
+  become: true
+  # When it runs, this should always report changed.
+  changed_when: true
+
+- name: Ensure netbox systemd services are set up and up-to-date
+  ansible.builtin.systemd_service:
+    daemon_reload: true
+    name: "{{ item }}"
+    enabled: true
+    state: restarted
+  become: true
+  loop:
+    - "netbox.service"
+    - "netbox-rq.service"
+
+- name: Ensure netbox housekeeping timer is set up and up-to-date
+  ansible.builtin.systemd_service:
+    daemon_reload: true
+    name: "netbox-housekeeping.timer"
+    enabled: true
+    state: restarted
+  become: true
diff --git a/roles/netbox/meta/argument_specs.yaml b/roles/netbox/meta/argument_specs.yaml
new file mode 100644
index 0000000..0506389
--- /dev/null
+++ b/roles/netbox/meta/argument_specs.yaml
@@ -0,0 +1,16 @@
+argument_specs:
+  main:
+    options:
+      netbox__version:
+        type: str
+        required: true
+      netbox__db_password:
+        type: str
+        required: true
+      netbox__config:
+        type: str
+        required: true
+      netbox__custom_pipeline_oidc_group_and_role_mapping:
+        type: bool
+        required: false
+        default: false
diff --git a/roles/netbox/meta/main.yaml b/roles/netbox/meta/main.yaml
new file mode 100644
index 0000000..79c845d
--- /dev/null
+++ b/roles/netbox/meta/main.yaml
@@ -0,0 +1,11 @@
+---
+dependencies:
+  - role: redis
+  - role: postgresql
+    vars:
+      postgresql__dbs:
+        - name: netbox
+          owner: netbox
+      postgresql__users:
+        - name: netbox
+          password: "{{ netbox__db_password }}"
diff --git a/roles/netbox/tasks/main.yaml b/roles/netbox/tasks/main.yaml
new file mode 100644
index 0000000..dffa746
--- /dev/null
+++ b/roles/netbox/tasks/main.yaml
@@ -0,0 +1,124 @@
+- name: Ensure all dependencies are installed
+  ansible.builtin.apt:
+    name:
+      - python3
+      - python3-pip
+      - python3-venv
+      - python3-dev
+      - build-essential
+      - libxml2-dev
+      - libxslt1-dev
+      - libffi-dev
+      - libpq-dev
+      - libssl-dev
+      - zlib1g-dev
+      - git
+  become: true
+
+- name: Ensure NetBox source is present
+  ansible.builtin.git:
+    repo: https://github.com/netbox-community/netbox.git
+    dest: /opt/netbox/
+    version: "{{ netbox__version }}"
+  become: true
+  notify:
+    - Run upgrade script
+    - Ensure netbox systemd services are set up and up-to-date
+
+- name: Ensures custom pipeline code for OIDC group and role mapping is present
+  ansible.builtin.copy:
+    src: custom_pipeline_oidc_group_and_role_mapping.py
+    dest: /opt/netbox/netbox/netbox/custom_pipeline_oidc_mapping.py
+    mode: "0644"
+    owner: root
+    group: root
+  when: netbox__custom_pipeline_oidc_group_and_role_mapping
+  become: true
+  notify:
+    - Ensure netbox systemd services are set up and up-to-date
+
+- name: Ensures custom pipeline code for OIDC group and role mapping is not present
+  ansible.builtin.file:
+    path: /opt/netbox/netbox/netbox/custom_pipeline_oidc_mapping.py
+    state: absent
+  when: not netbox__custom_pipeline_oidc_group_and_role_mapping
+  become: true
+  notify:
+    - Ensure netbox systemd services are set up and up-to-date
+
+- name: Ensure netbox user
+  block:
+    - name: Ensure netbox group exists
+      ansible.builtin.group:
+        name: netbox
+        system: true
+      become: true
+
+    - name: Ensure netbox user exists
+      ansible.builtin.user:
+        name: netbox
+        group: netbox
+        password: '!'
+        system: true
+      become: true
+
+- name: Ensure relevant directories are owned by netbox user
+  ansible.builtin.file:
+    path: "{{ item }}"
+    state: directory
+    owner: netbox
+    recurse: true
+  become: true
+  loop:
+    - "/opt/netbox/netbox/media/"
+    - "/opt/netbox/netbox/reports/"
+    - "/opt/netbox/netbox/scripts/"
+
+- name: Deploy configuration.py
+  ansible.builtin.copy:
+    content: "{{ netbox__config }}"
+    dest: "/opt/netbox/netbox/netbox/configuration.py"
+    mode: "0644"
+    owner: root
+    group: root
+  become: true
+  notify: Ensure netbox systemd services are set up and up-to-date
+
+- name: Ensure provided gunicorn config is copied
+  ansible.builtin.copy:
+    remote_src: true
+    src: "/opt/netbox/contrib/gunicorn.py"
+    dest: "/opt/netbox/gunicorn.py"
+    mode: "0644"
+    owner: root
+    group: root
+  become: true
+  notify: Ensure netbox systemd services are set up and up-to-date
+
+- name: Ensure provided netbox systemd service files are copied
+  ansible.builtin.copy:
+    remote_src: true
+    src: "/opt/netbox/contrib/{{ item }}"
+    dest: "/etc/systemd/system/{{ item }}"
+    mode: "0644"
+    owner: root
+    group: root
+  become: true
+  loop:
+    - "netbox.service"
+    - "netbox-rq.service"
+  notify: Ensure netbox systemd services are set up and up-to-date
+
+- name: Ensure provided housekeeping systemd service and timer are copied
+  ansible.builtin.copy:
+    remote_src: true
+    src: "/opt/netbox/contrib/{{ item }}"
+    dest: "/etc/systemd/system/{{ item }}"
+    mode: "0644"
+    owner: root
+    group: root
+  become: true
+  loop:
+    - "netbox-housekeeping.service"
+    - "netbox-housekeeping.timer"
+  notify: Ensure netbox housekeeping timer is set up and up-to-date
diff --git a/roles/postgresql/README.md b/roles/postgresql/README.md
new file mode 100644
index 0000000..6457931
--- /dev/null
+++ b/roles/postgresql/README.md
@@ -0,0 +1,37 @@
+# Role `postgresql`
+
+Ensures `postgresql` is installed by installing the distributions package.  
+Also ensures the optionally given databases and users are set up as specified.
+
+## Supported Distributions
+
+Should work on Debian-based distributions.
+
+## Required Arguments
+
+None.
+
+## Optional Arguments
+
+- `postgresql__dbs`: List of databases with their owner to ensure are set up.  
+- `postgresql__dbs.*.name`: Name of the database.
+- `postgresql__dbs.*.owner`: Owner of the database.
+- `postgresql__users`: List of users to ensure are set up.
+- `postgresql__users.*.name`: Name of the user.
+- `postgresql__users.*.password`: Optional password for the user.
+   If left unset, the user will have no password set, but can still connect using [peer authentication](https://www.postgresql.org/docs/current/auth-peer.html) on the local system.
+   (Peer authentication works when a password is set as well.)
+
+## Example Arguments
+
+```yaml
+postgresql__dbs:
+  - name: netbox
+    owner: netbox
+  - name: foo
+    owner: bar
+postgresql__users:
+  - name: netbox
+    password: super_secret
+  - name: bar
+```
diff --git a/roles/postgresql/defaults/main.yaml b/roles/postgresql/defaults/main.yaml
new file mode 100644
index 0000000..21fcd46
--- /dev/null
+++ b/roles/postgresql/defaults/main.yaml
@@ -0,0 +1,2 @@
+postgresql__dbs: [ ]
+postgresql__users: [ ]
diff --git a/roles/postgresql/meta/argument_specs.yaml b/roles/postgresql/meta/argument_specs.yaml
new file mode 100644
index 0000000..28e5813
--- /dev/null
+++ b/roles/postgresql/meta/argument_specs.yaml
@@ -0,0 +1,28 @@
+argument_specs:
+  main:
+    options:
+      postgresql__dbs:
+        type: list
+        elements: dict
+        required: false
+        default: [ ]
+        options:
+          name:
+            type: str
+            required: true
+          owner:
+            type: str
+            required: true
+      postgresql__users:
+        type: list
+        elements: dict
+        required: false
+        default: [ ]
+        options:
+          name:
+            type: str
+            required: true
+          password:
+            type: str
+            required: false
+            default: ""
diff --git a/roles/postgresql/tasks/main.yaml b/roles/postgresql/tasks/main.yaml
new file mode 100644
index 0000000..8f89018
--- /dev/null
+++ b/roles/postgresql/tasks/main.yaml
@@ -0,0 +1,30 @@
+- name: Ensure postgresql is installed
+  ansible.builtin.apt:
+    name:
+      - postgresql
+  become: true
+
+- name: Ensure Python library for community.postgresql is installed if needed
+  ansible.builtin.apt:
+    name:
+      - python3-psycopg
+  become: true
+  when: postgresql__dbs != [ ] or postgresql__users != [ ]
+
+- name: Ensure users
+  community.postgresql.postgresql_user:
+    name: "{{ item.name }}"
+    password: "{{ item.password | default('') }}"
+  become: true
+  become_user: postgres
+  loop: "{{ postgresql__users }}"
+  loop_control:
+    label: "user {{ item.name }} with {{ 'a password' if item.password is defined else 'no password' }}"
+
+- name: Ensure dbs with owners
+  community.postgresql.postgresql_db:
+    name: "{{ item.name }}"
+    owner: "{{ item.owner }}"
+  become: true
+  become_user: postgres
+  loop: "{{ postgresql__dbs }}"
diff --git a/roles/redis/README.md b/roles/redis/README.md
new file mode 100644
index 0000000..dd30500
--- /dev/null
+++ b/roles/redis/README.md
@@ -0,0 +1,15 @@
+# Role `redis`
+
+Ensures `redis` is installed by installing the distributions package.
+
+## Supported Distributions
+
+Should work on Debian-based distributions.
+
+## Required Arguments
+
+None.
+
+## Optional Arguments
+
+None.
diff --git a/roles/redis/tasks/main.yaml b/roles/redis/tasks/main.yaml
new file mode 100644
index 0000000..ad70e44
--- /dev/null
+++ b/roles/redis/tasks/main.yaml
@@ -0,0 +1,5 @@
+- name: Ensure redis is installed
+  ansible.builtin.apt:
+    name:
+      - redis
+  become: true