diff --git a/docs/servers.md b/docs/servers.md
index b47dc3df7390150c90b0a48f03b05416b6f53c43..d4c16955cf6c27ba23a225c56325bf61e16baa83 100644
--- a/docs/servers.md
+++ b/docs/servers.md
@@ -127,6 +127,7 @@ Medium-fast-ish packet.net Arch Linux box.
 
 ### Services
   - Redirects (nginx redirects)
+  - ping
 
 ## security.archlinux.org
 
diff --git a/playbooks/redirect.archlinux.org.yml b/playbooks/redirect.archlinux.org.yml
index 7e2f8af8a7873a68d975a6dd3c306be8a2b8a943..e02a9dece5bf572c706be2f1824587e2df6e41b3 100644
--- a/playbooks/redirect.archlinux.org.yml
+++ b/playbooks/redirect.archlinux.org.yml
@@ -13,3 +13,4 @@
     - { role: redirects }
     - { role: prometheus_exporters }
     - { role: hardening }
+    - { role: ping }
diff --git a/roles/ping/defaults/main.yml b/roles/ping/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a4aad27892f4b7519e0db16bca4594de6835164f
--- /dev/null
+++ b/roles/ping/defaults/main.yml
@@ -0,0 +1 @@
+ping_domain: 'ping.archlinux.org'
diff --git a/roles/ping/tasks/main.yml b/roles/ping/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fd53e1117abab29c78f66bf795b409981a7ad2c5
--- /dev/null
+++ b/roles/ping/tasks/main.yml
@@ -0,0 +1,14 @@
+---
+- name: create ssl cert
+  include_role:
+    name: certificate
+  vars:
+    domains: ["{{ ping_domain }}"]
+
+- name: make nginx log dir
+  file: path=/var/log/nginx/{{ ping_domain }} state=directory owner=root group=root mode=0755
+
+- name: set up nginx
+  template: src=nginx.d.conf.j2 dest="/etc/nginx/nginx.d/ping.conf" owner=root group=root mode=644
+  notify: reload nginx
+  tags: ['nginx']
diff --git a/roles/ping/templates/nginx.d.conf.j2 b/roles/ping/templates/nginx.d.conf.j2
new file mode 100644
index 0000000000000000000000000000000000000000..684291961c2e21531fd34d91c80be6cb85606589
--- /dev/null
+++ b/roles/ping/templates/nginx.d.conf.j2
@@ -0,0 +1,35 @@
+server {
+    # We don't redirect to HTTPS because a redirect is considered a captive portal.
+    listen       80;
+    listen       [::]:80;
+    listen       443 ssl http2;
+    listen       [::]:443 ssl http2;
+    server_name  {{ ping_domain }};
+
+    access_log   /var/log/nginx/{{ ping_domain }}/access.log reduced;
+    error_log    /var/log/nginx/{{ ping_domain }}/error.log;
+
+    include snippets/letsencrypt.conf;
+
+    ssl_certificate      /etc/letsencrypt/live/{{ ping_domain }}/fullchain.pem;
+    ssl_certificate_key  /etc/letsencrypt/live/{{ ping_domain }}/privkey.pem;
+    ssl_trusted_certificate /etc/letsencrypt/live/{{ ping_domain }}/chain.pem;
+
+    default_type text/plain;
+
+    location = / {
+        return 200 'This domain is used for connectivity checking (captive portal detection).\n';
+    }
+
+    # https://man.archlinux.org/man/NetworkManager.conf.5#CONNECTIVITY_SECTION
+    location /nm-check.txt {
+        access_log off;
+        add_header Cache-Control "max-age=0, must-revalidate";
+        return 200 'NetworkManager is online\n';
+    }
+
+    location / {
+        access_log off;
+        return 404;
+    }
+}
diff --git a/tf-stage1/archlinux.tf b/tf-stage1/archlinux.tf
index 6413c24853348f68901e9d3762b70e61f8638644..18af3582557d16e78d90173a047a804d2baa677a 100644
--- a/tf-stage1/archlinux.tf
+++ b/tf-stage1/archlinux.tf
@@ -284,6 +284,7 @@ locals {
     ipxe          = { value = "www" }
     mailman       = { value = "redirect" }
     packages      = { value = "www" }
+    ping          = { value = "redirect" }
     planet        = { value = "www" }
     projects      = { value = "luna" }
     repos         = { value = "gemini" }