diff --git a/site.yml b/site.yml
index 4a721d3cd93f08c74ba1637e413e373563e0d2df..e7205d660f5fd3a111d0a8917dab7f9045ad09c6 100644
--- a/site.yml
+++ b/site.yml
@@ -66,7 +66,12 @@
         basedir: "/srv/{{ servicename }}",
         domain: "tandoor.test-warpzone.de" 
       }
-
+    - { 
+        role: testserver/docker_vpnserver, tags: [ test_vpnserver, docker_services ],
+        servicename: "vpnserver",
+        basedir: "/srv/{{ servicename }}", 
+        domain: "vpn.test-warpzone.de"
+      }
 
 ##################################################
 # Produktive Server
diff --git a/testserver/docker_icinga/Documentation.md b/testserver/docker_icinga/Documentation.md
index 2c6b8a0b64dbfb541b857bdbe23d9db678c7c47f..6cfbf16c34d47e4457fd47fa63ee95a0bd9ce5a3 100644
--- a/testserver/docker_icinga/Documentation.md
+++ b/testserver/docker_icinga/Documentation.md
@@ -3,6 +3,6 @@
 3.  uffd configurieren
     1. neuen Dienst erstellen
     2. OAuth2 Client-ID erstellen
-        - Client-ID: gitea
+        - Client-ID: icinga
         - Client-Secret: /srv/icinga/secrets/oauth_client_secret
-        - Redirect-URIs: https://<icingaß-domain>/user/oauth2/uffd/callback
\ No newline at end of file
+        - Redirect-URIs: https://<icinga-domain>/user/oauth2/uffd/callback
\ No newline at end of file
diff --git a/testserver/docker_vpnserver/Documentation.md b/testserver/docker_vpnserver/Documentation.md
new file mode 100644
index 0000000000000000000000000000000000000000..c1c789427af0d7e45b944e2b669c6aa62eb15eb1
--- /dev/null
+++ b/testserver/docker_vpnserver/Documentation.md
@@ -0,0 +1,8 @@
+1. /srv/vpnserver und /srv/vpnserver/secrets erstellen, "wg genkey > wg_private_key" in /srv/vpnserver/secrets
+2. deployen
+3.  uffd configurieren
+    1. neuen Dienst erstellen
+    2. OAuth2 Client-ID erstellen
+        - Client-ID: vpnserver
+        - Client-Secret: /srv/vpnserver/secrets/oauth_client_secret
+        - Redirect-URIs: https://<vpnserver-domain>/user/oauth2/uffd/callback
\ No newline at end of file
diff --git a/testserver/docker_vpnserver/tasks/main.yml b/testserver/docker_vpnserver/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..db205d305bec18d16af21d57124bd2cdf789d0d6
--- /dev/null
+++ b/testserver/docker_vpnserver/tasks/main.yml
@@ -0,0 +1,55 @@
+---
+
+- include_tasks: ../functions/get_secret.yml
+  with_items:
+    - { path: "{{ basedir }}/secrets/wg_admin_pass",  length: 32 }
+    - { path: "{{ basedir }}/secrets/oauth_client_secret", length: 64 }
+    - { path: "{{ basedir }}/secrets/wg_private_key",  length: -1 } # 'wg genkey'
+
+
+- name: install wireguard
+  ansible.builtin.package:
+    name:
+      - wireguard
+      - iptables 
+    state: present
+
+- name: enable wireguard and iptables modules
+  community.general.modprobe:
+    name: "{{ item }}"
+    state: present
+    persistent: present
+  loop:
+    - wireguard
+    - iptables
+
+- name: create folder struct for vpnserver
+  file:
+    path: "{{ item }}"
+    state: "directory"
+  with_items:
+    - "{{ basedir }}"
+    - "{{ basedir }}/data"
+
+
+- name: "copy {{ servicename }} config files"
+  template:
+    src: "{{ item }}"
+    dest: "{{ basedir }}/{{ item }}"
+  with_items:
+    - docker-compose.yml
+    - config.yml
+  register: config
+
+
+- name: "stop {{ servicename }} docker"
+  community.docker.docker_compose_v2:
+    project_src: "{{ basedir }}"
+    state: absent
+  when: config.changed
+
+
+- name: "start {{ servicename }} docker"
+  community.docker.docker_compose_v2:
+    project_src: "{{ basedir }}"
+    state: present
diff --git a/testserver/docker_vpnserver/templates/config.yml b/testserver/docker_vpnserver/templates/config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3b0aa47121fbcdd78391cdbe3a0857bf015b08aa
--- /dev/null
+++ b/testserver/docker_vpnserver/templates/config.yml
@@ -0,0 +1,44 @@
+# You can disable the builtin admin account by leaving out 'adminPassword'. Requires another backend to be configured.
+adminPassword: "{{ wg_admin_pass }}"
+# adminUsername sets the user for the Basic/Simple Auth admin account if adminPassword is set.
+# Every user of the basic and simple backend with a username matching adminUsername will have admin privileges.
+adminUsername: "vpnadmin"
+# Configure zero or more authentication backends
+auth:
+  oidc:
+    # A name for the backend (is shown on the login page and possibly in the devices list of the 'all devices' admin page)
+    name: "uffd"
+    # Should point to the OIDC Issuer (excluding /.well-known/openid-configuration)
+    issuer: "{{ oidc_global.provider_url }}"
+    # Your OIDC client credentials which would be provided by your OIDC provider
+    clientID: "{{ servicename }}"
+    clientSecret: "{{ oauth_client_secret }}"
+    # The full redirect URL
+    # The path can be almost anything as long as it doesn't
+    # conflict with a path that the web UI uses.
+    # /callback is recommended.
+    redirectURL: "{{ oidc_global.provider_url }}/callback"
+    # List of scopes to request claims for. Must include 'openid'.
+    # Must include 'email' if 'emailDomains' is used. Can include 'profile' to show the user's name in the UI.
+    # Add custom ones if required for 'claimMapping'.
+    # Defaults to ["openid"]
+    scopes:
+      - openid
+      - profile
+      - email
+    # You can optionally restrict access to users with an email address
+    # that matches an allowed domain.
+    # If empty or omitted then all email domains will be allowed.
+    # This is an advanced feature that allows you to define OIDC claim mapping expressions.
+    # This feature is used to define wg-access-server admins based off a claim in your OIDC token.
+    # A JSON-like object of claimKey: claimValue pairs as returned by the issuer is passed to the evaluation function. 
+    # See https://github.com/Knetic/govaluate/blob/9aa49832a739dcd78a5542ff189fb82c3e423116/MANUAL.md for the syntax.
+    claimMapping:
+      # This example works if you have a custom group_membership claim which is a list of strings 
+      admin: "'vpnserver_admin' in group_membership"
+      access: "'vpnserver_access' in group_membership"
+    # Let wg-access-server retrieve the claims from the ID Token instead of querying the UserInfo endpoint.
+    # Some OIDC authorization provider implementations (e.g. ADFS) only publish claims in the ID Token.
+    claimsFromIDToken: false
+    # require this claim to be "true" to allow access for the user
+    accessClaim: "access"
\ No newline at end of file
diff --git a/testserver/docker_vpnserver/templates/docker-compose.yml b/testserver/docker_vpnserver/templates/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f67ebaac112278dbf762c2c31e685359dec01ceb
--- /dev/null
+++ b/testserver/docker_vpnserver/templates/docker-compose.yml
@@ -0,0 +1,38 @@
+services:
+
+  app:
+    image: ghcr.io/freifunkmuc/wg-access-server:latest
+    restart: always
+    cap_add:
+      - NET_ADMIN
+    sysctls:
+      net.ipv6.conf.all.disable_ipv6: 0
+      net.ipv6.conf.all.forwarding: 1
+    volumes:
+      - "{{ basedir }}/data:/data"
+      - "{{ basedir }}/config.yaml:/config.yml" # if you have a custom config file
+    ports:
+    #  - "8000:8000/tcp"
+      - "51820:51820/udp"
+    devices:
+      - "/dev/net/tun:/dev/net/tun"
+    environment:
+      - "WG_WIREGUARD_PRIVATE_KEY={{ wg_private_key }}"
+      - "WG_VPN_CIDRV6=0" # to disable IPv6
+      - "WG_EXTERNAL_HOST={{ domain }}"
+      - "WG_DNS_ENABLED=true"
+      - "WG_DNS_UPSTREAM=10.0.0.1"
+      - "WG_LOG_LEVEL=info"
+    labels:
+      - traefik.enable=true
+      - traefik.http.routers.{{ servicename }}.rule=Host(`{{ domain }}`)
+      - traefik.http.routers.{{ servicename }}.entrypoints=websecure
+      - traefik.http.services.{{ servicename }}.loadbalancer.server.port=8000
+    networks:
+      - default
+      - web
+
+
+networks:
+  web:
+    external: true