diff --git a/type/__nginx/man.rst b/type/__nginx/man.rst
new file mode 100644
index 0000000..71d47e7
--- /dev/null
+++ b/type/__nginx/man.rst
@@ -0,0 +1,57 @@
+cdist-type__nginx(7)
+===================================
+
+NAME
+----
+cdist-type__nginx - Serve web content with NGINX
+
+
+DESCRIPTION
+-----------
+Leverages `__nginx_vhost` to serve web content.
+
+REQUIRED PARAMETERS
+-------------------
+domain
+  Domain name to be served.
+
+OPTIONAL PARAMETERS
+-------------------
+config
+  Custom NGINX logic, templated within a standard `server` section with
+  `server_name` and TLS parameters set. Defaults to simple static hosting.
+
+altdomains
+  Alternative domain names for this vhost and related TLS certificate.
+
+uacme-hookscript
+  Custom hook passed to the __uacme_obtain type: useful to integrate the
+  dns-01 challenge with third-party DNS providers.
+
+EXAMPLES
+--------
+
+.. code-block:: sh
+
+  # TLS-enabled vhost serving static files in $WEBROOT/domain.tld (OS-specific,
+  # usually `/var/www` on GNU/Linux systemd).
+  __nginx domain.tld
+
+  # TLS-enabled vhost with custom configuration.
+  __nginx files.domain.tld \
+    --config - <<- EOF
+      root /var/www/files.domain.tld/;
+      autoindex on;
+    EOF
+
+AUTHORS
+-------
+Timothée Floure <timothee.floure@posteo.net>
+Joachim Desroches <joachim.desroches@epfl.ch>
+
+COPYING
+-------
+Copyright \(C) 2020 Joachim Desroches. You can redistribute it
+and/or modify it under the terms of the GNU General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
diff --git a/type/__nginx/manifest b/type/__nginx/manifest
new file mode 100644
index 0000000..b552319
--- /dev/null
+++ b/type/__nginx/manifest
@@ -0,0 +1,76 @@
+#!/bin/sh
+
+os="$(cat "${__global:?}"/explorer/os)"
+case "$os" in
+	alpine)
+		nginx_user=nginx
+		nginx_certdir=/etc/nginx/ssl
+		;;
+	debian|ubuntu)
+		nginx_user=www-data
+		nginx_certdir=/etc/nginx/ssl
+		;;
+	*)
+		echo "This type does not support $os yet. Aborting." >&2;
+		exit 1;
+		;;
+esac
+
+if [ -f "${__object:?}/parameter/domain" ];
+then
+	domain="$(cat "${__object:?}/parameter/domain")"
+else
+	domain="${__object_id:?}"
+fi
+
+altdomains=
+if [ -f "${__object:?}/parameter/altdomains" ];
+then
+	altdomains="$(cat "${__object:?}/parameter/altdomains")"
+fi
+
+set_custom_uacme_hookscript=
+if [ -f "${__object:?}/parameter/uacme-hookscript" ];
+then
+	uacme_hookscript="$(cat "${__object:?}/parameter/uacme-hookscript")"
+	set_custom_uacme_hookscript="--hookscript $uacme_hookscript"
+fi
+
+# Deploy simple HTTP vhost, allowing to serve ACME challenges.
+__nginx_vhost "301-to-https-$domain" \
+	--domain "$domain" --altdomains "$altdomains" --to-https
+
+# Obtaining TLS cert.
+cert_ownership=$nginx_user
+if [ -f "${__object:?}/parameter/force-cert-ownership-to" ]; then
+	cert_ownership=$(cat "${__object:?}/parameter/force-cert-ownership-to")
+fi
+
+__uacme_account
+# shellcheck disable=SC2086
+require="__nginx_vhost/301-to-https-$domain __uacme_account" \
+	__uacme_obtain "$domain" \
+		--altdomains "$altdomains" \
+		$set_custom_uacme_hookscript \
+		--owner "$cert_ownership" \
+		--install-key-to "$nginx_certdir/$domain/privkey.pem" \
+		--install-cert-to "/$nginx_certdir/$domain/fullchain.pem" \
+		--renew-hook "service nginx reload"
+
+# Deploy HTTPS nginx vhost.
+if [ -f "${__object:?}/parameter/config" ]; then
+	if [ "$(cat "${__object:?}/parameter/config")" = "-" ]; then
+		nginx_logic="${__object:?}/stdin"
+	else
+		nginx_logic="${__object:?}/parameter/config"
+	fi
+
+	mkdir -p "${__object:?}/files"
+	cat "$nginx_logic" > "${__object:?}/files/config"
+
+	require="__uacme_obtain/$domain" __nginx_vhost "$domain" \
+		--altdomains "$altdomains" --config "${__object:?}/files/config"
+else
+	require="__uacme_obtain/$domain" __nginx_vhost "$domain" \
+		--altdomains "$altdomains"
+fi
diff --git a/type/__nginx/parameter/default/http-port b/type/__nginx/parameter/default/http-port
new file mode 100644
index 0000000..d15a2cc
--- /dev/null
+++ b/type/__nginx/parameter/default/http-port
@@ -0,0 +1 @@
+80
diff --git a/type/__nginx/parameter/default/https-port b/type/__nginx/parameter/default/https-port
new file mode 100644
index 0000000..6a13cf6
--- /dev/null
+++ b/type/__nginx/parameter/default/https-port
@@ -0,0 +1 @@
+443
diff --git a/type/__nginx/parameter/optional b/type/__nginx/parameter/optional
new file mode 100644
index 0000000..1a5fb95
--- /dev/null
+++ b/type/__nginx/parameter/optional
@@ -0,0 +1,5 @@
+config
+domain
+altdomains
+uacme-hookscript
+force-cert-ownership-to
diff --git a/type/__nginx_vhost/files/301-to-https b/type/__nginx_vhost/files/301-to-https
new file mode 100644
index 0000000..2675732
--- /dev/null
+++ b/type/__nginx_vhost/files/301-to-https
@@ -0,0 +1,4 @@
+# Redirect request to this page in HTTPS.
+location / {
+	return 301 https://$host$request_uri;
+}
diff --git a/type/__nginx_vhost/files/generic.conf.sh b/type/__nginx_vhost/files/generic.conf.sh
new file mode 100755
index 0000000..13e36aa
--- /dev/null
+++ b/type/__nginx_vhost/files/generic.conf.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+# Template for static NGINX hosting.
+
+echo 'server {'
+
+# Listen
+cat <<- EOF
+	listen ${LPORT:?} $TLS;
+	listen [::]:${LPORT:?} $TLS;
+EOF
+
+# Name
+echo "server_name ${DOMAIN:?} $ALTDOMAINS;"
+
+# ACME challenges.
+cat << EOF
+location /.well-known/acme-challenge/ {
+	alias ${ACME_CHALLENGE_DIR:?};
+}
+EOF
+
+if [ -n "$TLS" ];
+then
+	if [ -n "$HSTS" ];
+	then
+		echo 'include snippets/hsts;'
+	fi
+
+	cat <<- EOF
+		ssl_certificate ${NGINX_CERTDIR:?}/${DOMAIN:?}/fullchain.pem;
+		ssl_certificate_key ${NGINX_CERTDIR:?}/${DOMAIN:?}/privkey.pem;
+	EOF
+fi
+
+echo "${NGINX_LOGIC:?}"
+
+echo '}'
diff --git a/type/__nginx_vhost/files/hsts b/type/__nginx_vhost/files/hsts
new file mode 100644
index 0000000..7e4a854
--- /dev/null
+++ b/type/__nginx_vhost/files/hsts
@@ -0,0 +1 @@
+add_header Strict-Transport-Security "max-age=31536000" always;
diff --git a/type/__nginx_vhost/files/index.html b/type/__nginx_vhost/files/index.html
new file mode 100644
index 0000000..bcadf4d
--- /dev/null
+++ b/type/__nginx_vhost/files/index.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>cdist configured!</title>
+  </head>
+  <body>
+    You have successfully configured a vhost with
+    <a href="https://cdi.st">cdist</a>. You can now upload content!
+  </body>
+</html>
diff --git a/type/__nginx_vhost/files/static.conf.sh b/type/__nginx_vhost/files/static.conf.sh
new file mode 100755
index 0000000..363f228
--- /dev/null
+++ b/type/__nginx_vhost/files/static.conf.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+# Template for static NGINX hosting.
+
+NGINX_LOGIC="$(cat << EOF
+	location / {
+		root ${NGINX_WEBROOT:?}/${DOMAIN:?};
+		index index.html;
+	}
+EOF
+)"
+export NGINX_LOGIC
+
+"${__type:?}/files/generic.conf.sh"
diff --git a/type/__nginx_vhost/files/to-https.conf.sh b/type/__nginx_vhost/files/to-https.conf.sh
new file mode 100755
index 0000000..77dd45b
--- /dev/null
+++ b/type/__nginx_vhost/files/to-https.conf.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+# Template for HTTPS redirection.
+
+echo 'server {'
+
+# Listen
+cat <<- EOF
+	listen ${LPORT:?};
+	listen [::]:${LPORT:?};
+EOF
+
+# Name
+echo "server_name ${DOMAIN:?} $ALTDOMAINS;"
+
+# ACME challenges.
+cat << EOF
+location /.well-known/acme-challenge/ {
+	alias ${ACME_CHALLENGE_DIR:?};
+}
+EOF
+
+# HTTPS redirection.
+echo 'include snippets/301-to-https;'
+
+echo '}'
diff --git a/type/__nginx_vhost/gencode-remote b/type/__nginx_vhost/gencode-remote
new file mode 100644
index 0000000..dd6539d
--- /dev/null
+++ b/type/__nginx_vhost/gencode-remote
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+os="$(cat "${__global:?}"/explorer/os)"
+init=$(cat "$__global/explorer/init")
+nginx_confdir="/etc/nginx"
+
+# The nginx service is not automatically started on alpine.
+if [ "$os" = "alpine" ]; then
+	echo "service nginx --ifstopped start"
+fi
+
+if grep -qE "^__file$nginx_confdir" "${__messages_in:?}"; then
+	case "$init" in
+		systemd)
+			reload_hook="systemctl reload-or-restart nginx"
+			;;
+		busybox-init+openrc)
+			reload_hook="service nginx reload"
+			;;
+		*)
+			echo "Unknown init $init." >&2
+			exit 1
+			;;
+	esac
+
+	cat <<- EOF
+	if nginx -t; then
+	  $reload_hook
+	else
+	  echo "NGINX configuration is invalid. Exiting." >2&
+	  nginx -t >2&
+	  exit 1
+	fi
+	EOF
+fi
diff --git a/type/__nginx_vhost/man.rst b/type/__nginx_vhost/man.rst
new file mode 100644
index 0000000..c078b10
--- /dev/null
+++ b/type/__nginx_vhost/man.rst
@@ -0,0 +1,82 @@
+cdist-type__nginx_vhost(7)
+===================================
+
+NAME
+----
+cdist-type__nginx_vhost - Have nginx serve content for a virtual host
+
+
+DESCRIPTION
+-----------
+This type setups up nginx with reasonable defaults and creates a vhost to be
+served, optionally with TLS certificates obtained from the Let's Encrypt CA
+through the ACME HTTP-01 challenge-response mechanism.
+
+By default, if no rules are specified, then the vhost will serve as-is the
+contents of the `WEBROOT/foo.com` directory, where WEBROOT is
+determined depending on the OS, adhering as close to `hier(7)` as possible.
+
+NGINX expects files in the vhost to be served to be at least readable by the
+`USER` group, that it creates if it does not exist. It is recommended to have
+the user owning the files to be someone else, and the files beeing
+group-readable but not writeable.
+
+Finally, if TLS is not disabled, then this type makes nginx expect the
+fullchain certificate and the private key in
+`CERTDIR/domain/{fullchain,privkey}.pem`.
+
++------------------+---------+-------------------+-----------------------------+
+| Operating System | USER    | WEBROOT           | CERTDIR                     |
++==================+=========+===================+=============================+
+| Alpine Linux     | `nginx` | `/srv/www/`       | `/etc/nginx/ssl/`           |
++------------------+---------+-------------------+-----------------------------+
+| Arch Linux       | `www`   | `/srv/www/`       | `/etc/nginx/ssl/`           |
++------------------+---------+-------------------+-----------------------------+
+
+OPTIONAL PARAMETERS
+-------------------
+
+config
+  A custom configuration file for the vhost, inserted in a server section
+  populated with `server_name` and TLS parameters unless `--standalone-config`
+  is specified. Can be specified either as a file path, or if the value of this
+  flag is '-', then the configuration is read from stdin.
+
+domain
+  The domain this server will respond to. If this is omitted, then the
+  `__object_id` is used.
+
+lport
+  The port to which we listen. If this is omitted, the defaults of `80` for
+  HTTP and `443` for HTTPS are used.
+
+altdomains
+  Alternative domain names for this vhost.
+
+BOOLEAN PARAMETERS
+------------------
+
+no-hsts
+  Do not use HSTS pinning.
+
+no-tls
+  Do not serve over HTTPS.
+
+to-https
+  Ignore --config flag and redirect to HTTPS. Implies --no-tls.
+
+standalone-config
+  Use as-in the vhost configuration (= do not wrap in generic server section)
+  the content of the `config` parameter.
+
+AUTHORS
+-------
+Joachim Desroches <joachim.desroches@epfl.ch>
+Timothée Floure <timothee.floure@posteo.net>
+
+COPYING
+-------
+Copyright \(C) 2020 Joachim Desroches. You can redistribute it
+and/or modify it under the terms of the GNU General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
diff --git a/type/__nginx_vhost/manifest b/type/__nginx_vhost/manifest
new file mode 100644
index 0000000..f9ad84d
--- /dev/null
+++ b/type/__nginx_vhost/manifest
@@ -0,0 +1,163 @@
+#!/bin/sh
+#
+# 2020 Joachim Desroches <joachim.desroches@epfl.ch>
+# 2021 Timothée Floure <timothee.floure@posteo.net>
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see <http://www.gnu.org/licenses/>.
+#
+# Create NGINX vhosts
+
+os="$(cat "${__global:?}"/explorer/os)"
+mkdir -p "${__object:?}/files"
+
+case "$os" in
+	alpine)
+		__package nginx
+
+		nginx_confdir="/etc/nginx"
+		install_reqs="__package/nginx"
+
+		require="$install_reqs" __start_on_boot nginx
+
+		export NGINX_SITEDIR="$nginx_confdir/conf.d"
+		export NGINX_CERTDIR="$nginx_confdir/ssl"
+		export NGINX_SNIPPETSDIR="$nginx_confdir/snippets"
+		export NGINX_WEBROOT="/var/www"
+		export ACME_CHALLENGE_DIR="$NGINX_WEBROOT/.well-known/acme-challenge/"
+		;;
+	debian|ubuntu)
+		__package nginx
+
+		nginx_confdir="/etc/nginx"
+		install_reqs="__package/nginx"
+
+		export NGINX_SITEDIR="$nginx_confdir/sites-enabled"
+		export NGINX_CERTDIR="$nginx_confdir/ssl"
+		export NGINX_SNIPPETSDIR="$nginx_confdir/snippets"
+		export NGINX_WEBROOT="/var/www"
+		export ACME_CHALLENGE_DIR="$NGINX_WEBROOT/.well-known/acme-challenge/"
+		;;
+	*)
+		echo "This type does not support $os yet. Aborting." >&2;
+		exit 1;
+esac
+
+# Domain
+if [ -f "${__object:?}/parameter/domain" ];
+then
+	DOMAIN="$(cat "${__object:?}/parameter/domain")"
+else
+	DOMAIN="${__object_id:?}"
+fi
+export DOMAIN
+
+ALTDOMAINS=
+if [ -f "${__object:?}/parameter/altdomains" ];
+then
+	ALTDOMAINS="$(cat "${__object:?}/parameter/altdomains")"
+fi
+export ALTDOMAINS
+
+# Use TLS ?
+if [ -f "${__object:?}/parameter/no-tls" ];
+then
+	TLS=
+	echo "WARNING: you have disabled TLS for vhost $DOMAIN" >&2
+else
+	TLS=ssl
+fi
+export TLS
+
+# Use HSTS ?
+if [ -f "${__object:?}/parameter/no-hsts" ];
+then
+	HSTS=
+else
+	HSTS=true
+fi
+export HSTS
+
+# Redirect to HTTPS ?
+if [ -f "${__object:?}/parameter/to-https" ];
+then
+	TO_HTTPS=true
+else
+	TO_HTTPS=
+fi
+export HSTS
+
+# Port to listen on
+if [ -f "${__object:?}/parameter/lport" ];
+then
+	LPORT="$(cat "${__object:?}/parameter/lport")"
+else
+	if [ -n "$TLS" ] && [ -z "$TO_HTTPS" ];
+	then
+		LPORT=443
+	else
+		LPORT=80
+	fi
+fi
+export LPORT
+
+# Server definition
+if [ -n "$TO_HTTPS" ];
+then
+	# Ignore configuration, simply serve ACME challenge and redirect to HTTPS.
+	"${__type:?}/files/to-https.conf.sh" > "${__object:?}/files/vhost.conf"
+	vhost_conf="${__object:?}/files/vhost.conf"
+elif [ -f "${__object:?}/parameter/config" ];
+then
+	# Extract nginx config from type parameter.
+	if [ "$(cat "${__object:?}/parameter/config")" = "-" ];
+	then
+		vhost_partial="${__object:?}/stdin"
+	else
+		vhost_partial=$(cat "${__object:?}/parameter/config")
+	fi
+
+	# Either use config as-in or template it in generic vhost structure.
+	if [ -f "${__object:?}/parameter/standalone-config" ]; then
+		vhost_conf=$vhost_partial
+	else
+		NGINX_LOGIC=$(cat "$vhost_partial") "${__type:?}/files/generic.conf.sh" \
+			> "${__object:?}/files/vhost.conf"
+
+		vhost_conf="${__object:?}/files/vhost.conf"
+	fi
+else
+	# Default to simple static configuration.
+	"${__type:?}/files/static.conf.sh" > "${__object:?}/files/vhost.conf"
+	vhost_conf="${__object:?}/files/vhost.conf"
+
+	require="$install_reqs" __directory "$NGINX_WEBROOT/$DOMAIN"
+	require="__directory$NGINX_WEBROOT/$DOMAIN" \
+		__file "$NGINX_WEBROOT/$DOMAIN/index.html" --state exists \
+		--source "${__type:?}/files/index.html" \
+		--mode 0644
+fi
+
+# Install snippets.
+require="$install_reqs" __directory "$NGINX_SNIPPETSDIR"
+for snippet in hsts 301-to-https; do
+	require="__directory/$NGINX_SNIPPETSDIR" __file \
+		"$NGINX_SNIPPETSDIR/$snippet" --source "${__type:?}/files/$snippet"
+done
+
+# Install vhost.
+require="$install_reqs" __file "$NGINX_SITEDIR/$__object_id.conf" \
+	--source "$vhost_conf" \
+	--mode 0644
diff --git a/type/__nginx_vhost/parameter/boolean b/type/__nginx_vhost/parameter/boolean
new file mode 100644
index 0000000..aa06036
--- /dev/null
+++ b/type/__nginx_vhost/parameter/boolean
@@ -0,0 +1,4 @@
+no-tls
+no-hsts
+to-https
+standalone-config
diff --git a/type/__nginx_vhost/parameter/default/index b/type/__nginx_vhost/parameter/default/index
new file mode 100644
index 0000000..d5b7a40
--- /dev/null
+++ b/type/__nginx_vhost/parameter/default/index
@@ -0,0 +1 @@
+index.html index.htm
diff --git a/type/__nginx_vhost/parameter/optional b/type/__nginx_vhost/parameter/optional
new file mode 100644
index 0000000..9c47616
--- /dev/null
+++ b/type/__nginx_vhost/parameter/optional
@@ -0,0 +1,4 @@
+domain
+config
+altdomains
+lport