diff --git a/charts/nodepool/Chart.yaml b/charts/nodepool/Chart.yaml
new file mode 100644
index 0000000..da75d89
--- /dev/null
+++ b/charts/nodepool/Chart.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+name: nodepool
+description: Nodepool is a system for managing test node resources.
+home: https://zuul-ci.org/docs/nodepool/
+appVersion: 3.10.0
+version: 0.0.1
diff --git a/charts/nodepool/templates/_helpers.tpl b/charts/nodepool/templates/_helpers.tpl
new file mode 100644
index 0000000..e5d2a0e
--- /dev/null
+++ b/charts/nodepool/templates/_helpers.tpl
@@ -0,0 +1,51 @@
+{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "nodepool.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "nodepool.fullname" -}}
+{{- if .Values.fullnameOverride -}}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- if contains $name .Release.Name -}}
+{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Generate basic labels
+*/}}
+{{- define "nodepool.common.labels" }}
+app.kubernetes.io/name: {{ include "nodepool.fullname" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+app.kubernetes.io/part-of: nodepool
+{{- end }}
+
+{{/*
+Generate Nodepool-builder labels
+*/}}
+{{- define "nodepool.builder.labels" }}
+{{- include "nodepool.common.labels" . }}
+app.kubernetes.io/component: nodepool-builder
+{{- end }}
+
+{{/*
+Generate Nodepool-launcher labels
+*/}}
+{{- define "nodepool.launcher.labels" }}
+{{- include "nodepool.common.labels" . }}
+app.kubernetes.io/component: nodepool-launcher
+{{- end }}
+
diff --git a/charts/nodepool/templates/builder/statefulset.yaml b/charts/nodepool/templates/builder/statefulset.yaml
new file mode 100644
index 0000000..acd4efc
--- /dev/null
+++ b/charts/nodepool/templates/builder/statefulset.yaml
@@ -0,0 +1,66 @@
+---
+{{ if .Values.builder.enabled }}
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  namespace: {{ .Release.Namespace }}
+  name: {{ include "nodepool.fullname" . }}-builder
+  labels:
+{{- include "nodepool.builder.labels" . | indent 4 }}
+spec:
+  replicas: {{ .Values.builder.replicas }}
+  serviceName: nodepool-builder
+  selector:
+    matchLabels:
+{{- include "nodepool.builder.labels" . | indent 6 }}
+  template:
+    metadata:
+      labels:
+{{- include "nodepool.builder.labels" . | indent 8 }}
+      annotations:
+        checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
+    spec:
+      containers:
+      - name: launcher
+        image: zuul/nodepool-builder:latest
+        env:
+        - name: OS_CLIENT_CONFIG_FILE
+          value: /etc/nodepool/clouds.yaml
+        volumeMounts:
+        - name: nodepool-config
+          mountPath: /etc/nodepool
+        - name: nodepool-logs
+          mountPath: /var/log/nodepool
+        - name: {{ include "nodepool.fullname" . }}-images-dir
+          mountPath: {{ index .Values.config "images-dir" }}
+        - name: dev
+          mountPath: /dev
+        securityContext:
+          privileged: true
+      volumes:
+      - name: nodepool-config
+        secret:
+          secretName: {{ include "nodepool.fullname" . }}
+      - name: nodepool-logs
+        emptyDir: {}
+      - name: dev
+        hostPath:
+          path: /dev
+    {{- with .Values.tolerations }}
+      tolerations:
+{{ toYaml . | indent 8 }}
+    {{- end }}
+    {{- with .Values.nodeSelector }}
+      nodeSelector:
+{{ toYaml . | indent 8 }}
+    {{- end }}
+  volumeClaimTemplates:
+  - metadata:
+      name: {{ include "nodepool.fullname" . }}-images-dir
+    spec:
+      accessModes:
+      - ReadWriteOnce
+      resources:
+        requests:
+          storage: 80G
+{{ end }}
\ No newline at end of file
diff --git a/charts/nodepool/templates/launcher/deployment.yaml b/charts/nodepool/templates/launcher/deployment.yaml
new file mode 100644
index 0000000..5bd1b69
--- /dev/null
+++ b/charts/nodepool/templates/launcher/deployment.yaml
@@ -0,0 +1,43 @@
+---
+{{ if .Values.launcher.enabled }}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  namespace: {{ .Release.Namespace }}
+  name: {{ include "nodepool.fullname" . }}-launcher
+  labels:
+{{- include "nodepool.launcher.labels" . | indent 4 }}
+spec:
+  replicas: {{ .Values.launcher.replicas }}
+  selector:
+    matchLabels:
+{{- include "nodepool.launcher.labels" . | indent 6 }}
+  template:
+    metadata:
+      labels:
+{{- include "nodepool.launcher.labels" . | indent 8 }}
+      annotations:
+        checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
+    spec:
+      containers:
+      - name: launcher
+        image: zuul/nodepool-launcher:latest
+        env:
+        - name: OS_CLIENT_CONFIG_FILE
+          value: /etc/nodepool/clouds.yaml
+        volumeMounts:
+        - name: nodepool-config
+          mountPath: /etc/nodepool
+      volumes:
+      - name: nodepool-config
+        secret:
+          secretName: {{ include "nodepool.fullname" . }}
+    {{- with .Values.tolerations }}
+      tolerations:
+{{ toYaml . | indent 8 }}
+    {{- end }}
+    {{- with .Values.nodeSelector }}
+      nodeSelector:
+{{ toYaml . | indent 8 }}
+    {{- end }}
+{{ end }}
\ No newline at end of file
diff --git a/charts/nodepool/templates/secret.yaml b/charts/nodepool/templates/secret.yaml
new file mode 100644
index 0000000..e55cf57
--- /dev/null
+++ b/charts/nodepool/templates/secret.yaml
@@ -0,0 +1,14 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  namespace: {{ .Release.Namespace }}
+  name: {{ include "nodepool.fullname" . }}
+  labels:
+{{- include "nodepool.common.labels" . | indent 4 }}
+stringData:
+  nodepool.yaml: |
+{{ toYaml .Values.config | indent 4 }}
+  clouds.yaml: |
+    clouds:
+{{ toYaml .Values.clouds | indent 6 }}
\ No newline at end of file
diff --git a/charts/nodepool/values.yaml b/charts/nodepool/values.yaml
new file mode 100644
index 0000000..f784e5f
--- /dev/null
+++ b/charts/nodepool/values.yaml
@@ -0,0 +1,14 @@
+---
+builder:
+  enabled: true
+  replicas: 1
+
+launcher:
+  enabled: true
+  replicas: 1
+
+config:
+  webapp:
+    port: 8005
+    listen_address: 0.0.0.0
+  images-dir: /opt/nodepool
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
new file mode 100644
index 0000000..88f7a9e
--- /dev/null
+++ b/zuul.d/project.yaml
@@ -0,0 +1,7 @@
+- project:
+    check:
+      jobs:
+        - chart-testing-lint
+    gate:
+      jobs:
+        - chart-testing-lint