diff --git a/hedgedoc/defaults/main.yml b/hedgedoc/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dac3316d5635d20f67d402da2fd1a8f0c1825813
--- /dev/null
+++ b/hedgedoc/defaults/main.yml
@@ -0,0 +1,22 @@
+---
+
+hedgedoc_data_root: /var/lib/hedgedoc
+hedgedoc_install_root: /opt/hedgedoc
+hedgedoc_version: "1.8.2"
+
+# https://docs.hedgedoc.org/configuration
+hedgedoc_db:
+  dialect: sqlite
+  storage: "{{ hedgedoc_data_root }}/db.sqlite"
+hedgedoc_domain: hedgedoc.example.org
+hedgedoc_urlPath: null
+hedgedoc_allowGravatar: false
+hedgedoc_protocolUseSSL: true
+# hedgedoc_csp
+# hedgedoc_cookiePolicy
+# hedgedoc_extra_config
+
+# hedgedoc_db:
+#   dialect: postgres
+#   host: /run/postgresql
+#   database: hedgedoc
diff --git a/hedgedoc/handlers/main.yml b/hedgedoc/handlers/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f3b4244580428ba03d8dce22536bf11a84d169cb
--- /dev/null
+++ b/hedgedoc/handlers/main.yml
@@ -0,0 +1,10 @@
+---
+
+- name: Reload systemd
+  systemd:
+    daemon_reload: true
+
+- name: Restart hedgedoc
+  systemd:
+    name: hedgedoc.service
+    state: restarted
diff --git a/hedgedoc/tasks/main.yml b/hedgedoc/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ed57ca30cf1b1170db5a0129e6d6ac7ebcd5c351
--- /dev/null
+++ b/hedgedoc/tasks/main.yml
@@ -0,0 +1,120 @@
+---
+
+- name: Install required packages
+  apt:
+    name:
+      - nodejs
+      - yarnpkg
+      - npm
+
+- name: Create system group
+  group:
+    name: hedgedoc
+    system: true
+    state: present
+
+- name: Create system user
+  user:
+    name: hedgedoc
+    group: hedgedoc
+    system: true
+    home: "{{ hedgedoc_data_root }}"
+    shell: /usr/bin/nologin
+    state: present
+
+- import_tasks: postgres.yml
+  when:
+    - hedgedoc_db.dialect == "postgres"
+    - hedgedoc_db.host[0] == '/'
+  tags:
+    - postgresql
+
+- name: Install systemd service file
+  template:
+    src: hedgedoc.service.j2
+    dest: /etc/systemd/system/hedgedoc.service
+    owner: root
+    group: root
+    mode: "0644"
+  notify:
+    - Reload systemd
+    - Restart hedgedoc
+
+- name: Get installed version package.json
+  slurp:
+    src: "{{ hedgedoc_install_root }}/package.json"
+  register: installed_package_json
+  ignore_errors: true
+
+- when: >-
+    installed_package_json.failed or
+    installed_package_json.content|b64decode|from_json|json_query('version')|trim
+    != hedgedoc_version
+  block:
+    - name: Create temporary directory
+      tempfile:
+        state: directory
+      register: tempdir
+
+    - name: Fetch and extract HedgeDoc
+      unarchive:
+        # yamllint disable-line rule:line-length
+        src: "https://github.com/hedgedoc/hedgedoc/releases/download/{{ hedgedoc_version }}/hedgedoc-{{ hedgedoc_version }}.tar.gz"
+        dest: "{{ tempdir.path }}"
+        remote_src: true
+
+    - name: Move HedgeDoc to target directory
+      copy:
+        src: "{{ tempdir.path }}/hedgedoc/"
+        dest: "{{ hedgedoc_install_root }}-{{ hedgedoc_version }}"
+        remote_src: true
+
+    - name: yarn install
+      command:
+        cmd: yarnpkg install --production=true --pure-lockfile
+        chdir: "{{ hedgedoc_install_root }}-{{ hedgedoc_version }}"
+
+    - name: Get old install target
+      stat:
+        path: "{{ hedgedoc_install_root }}"
+      register: install_root
+
+    - name: Stop service for upgrade
+      systemd:
+        name: hedgedoc.service
+        state: stopped
+
+    - name: Replace install root symlink
+      file:
+        src: "{{ hedgedoc_install_root }}-{{ hedgedoc_version }}"
+        dest: "{{ hedgedoc_install_root }}"
+        state: link
+        follow: false
+        force: true
+
+    - name: Remove old version
+      file:
+        path: "{{ install_root.stat.lnk_source }}"
+        state: absent
+      when: install_root.stat.islnk is defined and install_root.stat.islnk
+
+    - name: Remove temporary directory
+      file:
+        path: "{{ tempdir.path }}"
+        state: absent
+
+- name: Install config
+  template:
+    src: "config.json.j2"
+    dest: "{{ hedgedoc_install_root }}/config.json"
+    owner: root
+    group: hedgedoc
+    mode: "0640"
+  notify:
+    - Restart hedgedoc
+
+- name: Enable and start service
+  systemd:
+    name: hedgedoc.service
+    state: started
+    enabled: true
diff --git a/hedgedoc/tasks/postgres.yml b/hedgedoc/tasks/postgres.yml
new file mode 100644
index 0000000000000000000000000000000000000000..750701c64e88ddbd0ea077ce7ad056d607738fd5
--- /dev/null
+++ b/hedgedoc/tasks/postgres.yml
@@ -0,0 +1,20 @@
+---
+
+- become: true
+  become_user: postgres
+  block:
+    - name: Create the postgres user
+      postgresql_user:
+        name: hedgedoc
+        state: present
+    - name: Create the database
+      postgresql_db:
+        name: "{{ hedgedoc_db.database }}"
+        owner: hedgedoc
+        state: present
+
+- name: Ensure postgres is running
+  service:
+    name: postgresql
+    state: started
+    enabled: true
diff --git a/hedgedoc/templates/config.json.j2 b/hedgedoc/templates/config.json.j2
new file mode 100644
index 0000000000000000000000000000000000000000..f105892f6accf486be0bfe83e5344612402ab609
--- /dev/null
+++ b/hedgedoc/templates/config.json.j2
@@ -0,0 +1,23 @@
+{
+    "production": {
+        "domain": "{{ hedgedoc_domain }}",
+        "urlPath": {{ hedgedoc_urlPath|to_json }},
+        "path": "/run/hedgedoc/hedgedoc.sock",
+        "loglevel": "info",
+        "uploadsPath": "{{ hedgedoc_data_root }}/uploads",
+        "allowGravatar": {{ hedgedoc_allowGravatar|to_json }},
+        "protocolUseSSL": {{ hedgedoc_protocolUseSSL|to_json }},
+{% if hedgedoc_csp is defined %}
+        "csp": {{ hedgedoc_csp|to_json }},
+{% endif %}
+{% if hedgedoc_cookiePolicy is defined %}
+        "cookiePolicy": "{{ hedgedoc_cookiePolicy }}",
+{% endif %}
+        "db": {{ hedgedoc_db|to_nice_json|indent(8, false) }}
+{% if hedgedoc_extra_config is defined -%}
+{% for k, v in hedgedoc_extra_config.items() %}
+        , "{{ k }}": {{ v|to_nice_json|indent(8, false) }}
+{% endfor %}
+{%- endif %}
+    }
+}
diff --git a/hedgedoc/templates/hedgedoc.service.j2 b/hedgedoc/templates/hedgedoc.service.j2
new file mode 100644
index 0000000000000000000000000000000000000000..135de7480caded622765372ae921d74d57543492
--- /dev/null
+++ b/hedgedoc/templates/hedgedoc.service.j2
@@ -0,0 +1,52 @@
+# Adapted from upstream example:
+# https://github.com/hedgedoc/hedgedoc/blob/2fb83e5a345a2d7ad829784e60fba3e8e5cab6a9/docs/content/setup/manual-setup.md#systemd-unit-example
+
+[Unit]
+Description=HedgeDoc – The best platform to write and share markdown.
+Documentation=https://docs.hedgedoc.org/
+After=network.target
+{% if hedgedoc_db.dialect == "postgres" %}
+After=postgresql.service
+{% elif hedgedoc_db.dialect == "mariadb" %}
+After=mariadb.service
+{% endif %}
+
+[Service]
+Type=exec
+Environment=NODE_ENV=production
+Restart=always
+RestartSec=2s
+ExecStart=/usr/bin/yarnpkg start --production
+ExecStartPost=/bin/sh -c "while ! test -e ${RUNTIME_DIRECTORY}/hedgedoc.sock; do sleep 2; done; chmod 666 ${RUNTIME_DIRECTORY}/hedgedoc.sock"
+CapabilityBoundingSet=
+NoNewPrivileges=true
+PrivateDevices=true
+RemoveIPC=true
+LockPersonality=true
+ProtectControlGroups=true
+ProtectKernelTunables=true
+ProtectKernelModules=true
+ProtectKernelLogs=true
+ProtectClock=true
+ProtectHostname=true
+ProtectProc=noaccess
+RestrictRealtime=true
+RestrictSUIDSGID=true
+RestrictNamespaces=true
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+ProtectSystem=strict
+ProtectHome=true
+PrivateTmp=true
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
+RuntimeDirectory=hedgedoc
+
+# You may have to adjust these settings
+User=hedgedoc
+Group=hedgedoc
+WorkingDirectory={{ hedgedoc_install_root }}
+
+ReadWritePaths={{ hedgedoc_data_root }}
+
+[Install]
+WantedBy=multi-user.target