diff --git a/.gitignore b/.gitignore
index c88b06b..1011d1f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@ rootfs/usr/bin/
 ./builder
 coverage.txt
 testdata/hooks/pre-receive
+.idea/
+
diff --git a/charts/builder/templates/builder-deployment.yaml b/charts/builder/templates/builder-deployment.yaml
index 28375e7..1d4ed7f 100644
--- a/charts/builder/templates/builder-deployment.yaml
+++ b/charts/builder/templates/builder-deployment.yaml
@@ -64,6 +64,8 @@ spec:
             value: "{{ .Values.global.storage }}"
           - name: "DRYCC_REGISTRY_LOCATION"
             value: "{{ .Values.global.registry_location }}"
+          - name: "TTL_SECONDS_AFTER_FINISHED"
+            value: "{{ .Values.global.ttl_seconds_after_finished }}"
           # Set GIT_LOCK_TIMEOUT to number of minutes you want to wait to git push again to the same repository
           - name: "GIT_LOCK_TIMEOUT"
             value: "30"
diff --git a/charts/builder/templates/builder-role.yaml b/charts/builder/templates/builder-role.yaml
index 91c2ed9..f4ba568 100644
--- a/charts/builder/templates/builder-role.yaml
+++ b/charts/builder/templates/builder-role.yaml
@@ -16,4 +16,7 @@ rules:
 - apiGroups: [""]
   resources: ["pods/log"]
   verbs: ["get"]
+- apiGroups: ["batch"]
+  resources: ["jobs"]
+  verbs: ["create"]
 {{- end -}}
diff --git a/charts/builder/values.yaml b/charts/builder/values.yaml
index c54b3d8..73d115e 100644
--- a/charts/builder/values.yaml
+++ b/charts/builder/values.yaml
@@ -11,6 +11,10 @@ service:
 # limits_memory: "50Mi"
 # builder_pod_node_selector: "disk:ssd"
 
+# When the TTL controller cleans up the Job. default: 6h
+# see: https://kubernetes.io/docs/concepts/workloads/controllers/job/#ttl-mechanism-for-finished-jobs
+ttl_seconds_after_finished: 21600
+
 global:
   # Role-Based Access Control for Kubernetes >= 1.5
   use_rbac: false
diff --git a/pkg/gitreceive/build.go b/pkg/gitreceive/build.go
index 78fa4ae..ef590a9 100644
--- a/pkg/gitreceive/build.go
+++ b/pkg/gitreceive/build.go
@@ -148,7 +148,7 @@ func build(
 	securityContext := k8s.SecurityContextFromPrivileged(true)
 
 	imageName := fmt.Sprintf("%s:git-%s", appName, gitSha.Short())
-	buildPodName := imagebuilderPodName(appName, gitSha.Short())
+	buildJobName := imagebuilderJobName(appName, gitSha.Short())
 	registryLocation := conf.RegistryLocation
 	builderImageEnv := make(map[string]string)
 	if registryLocation != "on-cluster" {
@@ -160,9 +160,9 @@ func build(
 	builderImageEnv["DRYCC_STACK"] = stack["name"]
 	builderImageEnv["DRYCC_REGISTRY_LOCATION"] = registryLocation
 
-	pod := createBuilderPod(
+	job := createBuilderJob(
 		conf.Debug,
-		buildPodName,
+		buildJobName,
 		conf.PodNamespace,
 		appConf.Values,
 		tarKey,
@@ -181,17 +181,16 @@ func build(
 
 	log.Info("Starting build... but first, coffee!")
 	log.Debug("Use image %s: %s", stack["name"], stack["image"])
-	log.Debug("Starting pod %s", buildPodName)
-	json, err := prettyPrintJSON(pod)
+	log.Debug("Starting job %s", buildJobName)
+	json, err := prettyPrintJSON(job)
 	if err == nil {
-		log.Debug("Pod spec: %v", json)
+		log.Debug("Job spec: %v", json)
 	} else {
-		log.Debug("Error creating json representation of pod spec: %v", err)
+		log.Debug("Error creating json representation of Job spec: %v", err)
 	}
+	jobsInterface := kubeClient.BatchV1().Jobs(conf.PodNamespace)
 
-	podsInterface := kubeClient.CoreV1().Pods(conf.PodNamespace)
-
-	newPod, err := podsInterface.Create(ctx.TODO(), pod, metav1.CreateOptions{})
+	newJob, err := jobsInterface.Create(ctx.TODO(), job, metav1.CreateOptions{})
 	if err != nil {
 		return fmt.Errorf("creating builder pod (%s)", err)
 	}
@@ -201,11 +200,19 @@ func build(
 	defer close(stopCh)
 	go pw.Controller.Run(stopCh)
 
-	if err := waitForPod(pw, newPod.Namespace, newPod.Name, conf.SessionIdleInterval(), conf.BuilderPodTickDuration(), conf.BuilderPodWaitDuration()); err != nil {
+	if err := waitForPod(pw, newJob.Namespace, newJob.Name, conf.SessionIdleInterval(), conf.BuilderPodTickDuration(), conf.BuilderPodWaitDuration()); err != nil {
 		return fmt.Errorf("watching events for builder pod startup (%s)", err)
 	}
 
-	req := kubeClient.CoreV1().RESTClient().Get().Namespace(newPod.Namespace).Name(newPod.Name).Resource("pods").SubResource("log").VersionedParams(
+	options := metav1.ListOptions{
+		LabelSelector: fmt.Sprintf("heritage=%s", newJob.Name),
+	}
+	podList, err := kubeClient.CoreV1().Pods(newJob.Namespace).List(context.Background(), options)
+	if err != nil {
+		return fmt.Errorf("list pods %s fail: (%s)", newJob.Name, err)
+	}
+
+	req := kubeClient.CoreV1().RESTClient().Get().Namespace(newJob.Namespace).Name(podList.Items[0].Name).Resource("pods").SubResource("log").VersionedParams(
 		&corev1.PodLogOptions{
 			Follow: true,
 		}, scheme.ParameterCodec)
@@ -224,19 +231,19 @@ func build(
 
 	log.Debug(
 		"Waiting for the %s/%s pod to end. Checking every %s for %s",
-		newPod.Namespace,
-		newPod.Name,
+		newJob.Namespace,
+		newJob.Name,
 		conf.BuilderPodTickDuration(),
 		conf.BuilderPodWaitDuration(),
 	)
 	// check the state and exit code of the build pod.
 	// if the code is not 0 return error
-	if err := waitForPodEnd(pw, newPod.Namespace, newPod.Name, conf.BuilderPodTickDuration(), conf.BuilderPodWaitDuration()); err != nil {
+	if err := waitForPodEnd(pw, newJob.Namespace, newJob.Name, conf.BuilderPodTickDuration(), conf.BuilderPodWaitDuration()); err != nil {
 		return fmt.Errorf("error getting builder pod status (%s)", err)
 	}
 	log.Debug("Done")
 	log.Debug("Checking for builder pod exit code")
-	buildPod, err := kubeClient.CoreV1().Pods(newPod.Namespace).Get(ctx.TODO(), newPod.Name, metav1.GetOptions{})
+	buildPod, err := kubeClient.CoreV1().Pods(newJob.Namespace).Get(ctx.TODO(), podList.Items[0].Name, metav1.GetOptions{})
 	if err != nil {
 		return fmt.Errorf("error getting builder pod status (%s)", err)
 	}
diff --git a/pkg/gitreceive/k8s_util.go b/pkg/gitreceive/k8s_util.go
index d5e85a4..59cc72c 100644
--- a/pkg/gitreceive/k8s_util.go
+++ b/pkg/gitreceive/k8s_util.go
@@ -4,10 +4,13 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"os"
+	"strconv"
 	"time"
 
 	"github.com/drycc/builder/pkg/k8s"
 	"github.com/pborman/uuid"
+	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -27,7 +30,7 @@ const (
 	imagebuilderConfigPath = "/etc/imagebuilder"
 )
 
-func imagebuilderPodName(appName, shortSha string) string {
+func imagebuilderJobName(appName, shortSha string) string {
 	uid := uuid.New()[:8]
 	// NOTE(bacongobbler): pod names cannot exceed 63 characters in length, so we truncate
 	// the application name to stay under that limit when adding all the extra metadata to the name
@@ -37,7 +40,7 @@ func imagebuilderPodName(appName, shortSha string) string {
 	return fmt.Sprintf("imagebuild-%s-%s-%s", appName, shortSha, uid)
 }
 
-func createBuilderPod(
+func createBuilderJob(
 	debug bool,
 	name,
 	namespace string,
@@ -54,9 +57,9 @@ func createBuilderPod(
 	pullPolicy corev1.PullPolicy,
 	securityContext corev1.SecurityContext,
 	nodeSelector map[string]string,
-) *corev1.Pod {
+) *batchv1.Job {
 
-	pod := buildPod(debug, name, namespace, pullPolicy, securityContext, nodeSelector, env)
+	job := buildJob(debug, name, namespace, pullPolicy, securityContext, nodeSelector, env)
 
 	// inject application envvars as a special envvar which will be handled by imagebuilder to
 	// inject them as build-time variables.
@@ -67,29 +70,32 @@ func createBuilderPod(
 	// So we need to translate the map into json.
 	if _, ok := env["DRYCC_DOCKER_BUILD_ARGS_ENABLED"]; ok {
 		imageBuildArgs, _ := json.Marshal(env)
-		addEnvToPod(pod, "DOCKER_BUILD_ARGS", string(imageBuildArgs))
+		addEnvToJob(job, "DOCKER_BUILD_ARGS", string(imageBuildArgs))
 	}
+	job.Spec.Template.Spec.Containers[0].Name = builderName
+	job.Spec.Template.Spec.Containers[0].Image = builderImage
 
-	pod.Spec.Containers[0].Name = builderName
-	pod.Spec.Containers[0].Image = builderImage
-
-	addEnvToPod(pod, tarPath, tarKey)
-	addEnvToPod(pod, sourceVersion, gitShortHash)
-	addEnvToPod(pod, "IMG_NAME", imageName)
-	addEnvToPod(pod, builderStorage, storageType)
+	addEnvToJob(job, tarPath, tarKey)
+	addEnvToJob(job, sourceVersion, gitShortHash)
+	addEnvToJob(job, "IMG_NAME", imageName)
+	addEnvToJob(job, builderStorage, storageType)
 	// inject existing DRYCC_REGISTRY_PROXY_HOST and PORT info to imagebuilder
 	// see https://github.com/drycc/imagebuilder/issues/83
-	addEnvToPod(pod, "DRYCC_REGISTRY_PROXY_HOST", registryHost)
-	addEnvToPod(pod, "DRYCC_REGISTRY_PROXY_PORT", registryPort)
+	addEnvToJob(job, "DRYCC_REGISTRY_PROXY_HOST", registryHost)
+	addEnvToJob(job, "DRYCC_REGISTRY_PROXY_PORT", registryPort)
 
 	for key, value := range builderImageEnv {
-		addEnvToPod(pod, key, value)
+		addEnvToJob(job, key, value)
 	}
 
-	return &pod
+	return &job
+}
+
+func newInt32(i int32) *int32 {
+	return &i
 }
 
-func buildPod(
+func buildJob(
 	debug bool,
 	name,
 	namespace string,
@@ -97,17 +103,34 @@ func buildPod(
 	pullPolicy corev1.PullPolicy,
 	securityContext corev1.SecurityContext,
 	nodeSelector map[string]string,
-	env map[string]interface{}) corev1.Pod {
-	pod := corev1.Pod{
-		Spec: corev1.PodSpec{
-			RestartPolicy: corev1.RestartPolicyNever,
-			Containers: []corev1.Container{
-				{
-					ImagePullPolicy: pullPolicy,
-					SecurityContext: &securityContext,
+	env map[string]interface{}) batchv1.Job {
+	TTLSecondsAfterFinished := newInt32(21600)
+	if os.Getenv("TTL_SECONDS_AFTER_FINISHED") != "" {
+		ttl, err := strconv.ParseInt(os.Getenv("TTL_SECONDS_AFTER_FINISHED"), 10, 32)
+		if err == nil {
+			TTLSecondsAfterFinished = newInt32(int32(ttl))
+		}
+	}
+
+	job := batchv1.Job{
+		Spec: batchv1.JobSpec{
+			BackoffLimit:            newInt32(0),
+			TTLSecondsAfterFinished: TTLSecondsAfterFinished,
+			Template: corev1.PodTemplateSpec{
+				ObjectMeta: metav1.ObjectMeta{
+					Labels: map[string]string{
+						"heritage": name,
+					},
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							ImagePullPolicy: pullPolicy,
+							SecurityContext: &securityContext,
+						},
+					},
 				},
 			},
-			Volumes: []corev1.Volume{},
 		},
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      name,
@@ -117,8 +140,10 @@ func buildPod(
 			},
 		},
 	}
-
-	pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
+	job.Spec.Template.Spec.RestartPolicy = corev1.RestartPolicyNever
+	job.Spec.Template.Spec.Containers[0].ImagePullPolicy = pullPolicy
+	job.Spec.Template.Spec.Containers[0].SecurityContext = &securityContext
+	job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
 		Name: objectStore,
 		VolumeSource: corev1.VolumeSource{
 			Secret: &corev1.SecretVolumeSource{
@@ -127,14 +152,14 @@ func buildPod(
 		},
 	})
 
-	pod.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
+	job.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
 		{
 			Name:      objectStore,
 			MountPath: objectStorePath,
 			ReadOnly:  true,
 		},
 	}
-	pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
+	job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
 		Name: imagebuilderConfig,
 		VolumeSource: corev1.VolumeSource{
 			ConfigMap: &corev1.ConfigMapVolumeSource{
@@ -145,15 +170,15 @@ func buildPod(
 		},
 	})
 
-	pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
+	job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
 		Name:      imagebuilderConfig,
 		MountPath: imagebuilderConfigPath,
 		ReadOnly:  true,
 	})
 
-	if len(pod.Spec.Containers) > 0 {
+	if len(job.Spec.Template.Spec.Containers) > 0 {
 		for k, v := range env {
-			pod.Spec.Containers[0].Env = append(pod.Spec.Containers[0].Env, corev1.EnvVar{
+			job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{
 				Name:  k,
 				Value: fmt.Sprintf("%v", v),
 			})
@@ -161,19 +186,19 @@ func buildPod(
 	}
 
 	if len(nodeSelector) > 0 {
-		pod.Spec.NodeSelector = nodeSelector
+		job.Spec.Template.Spec.NodeSelector = nodeSelector
 	}
 
 	if debug {
-		addEnvToPod(pod, debugKey, "1")
+		addEnvToJob(job, debugKey, "1")
 	}
 
-	return pod
+	return job
 }
 
-func addEnvToPod(pod corev1.Pod, key, value string) {
-	if len(pod.Spec.Containers) > 0 {
-		pod.Spec.Containers[0].Env = append(pod.Spec.Containers[0].Env, corev1.EnvVar{
+func addEnvToJob(job batchv1.Job, key, value string) {
+	if len(job.Spec.Template.Spec.Containers) > 0 {
+		job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{
 			Name:  key,
 			Value: value,
 		})
diff --git a/pkg/gitreceive/k8s_util_test.go b/pkg/gitreceive/k8s_util_test.go
index bc8632a..b2b27de 100644
--- a/pkg/gitreceive/k8s_util_test.go
+++ b/pkg/gitreceive/k8s_util_test.go
@@ -8,17 +8,18 @@ import (
 
 	"github.com/arschles/assert"
 	"github.com/drycc/builder/pkg/k8s"
+	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 )
 
 func TestImagebuilderPodName(t *testing.T) {
-	name := imagebuilderPodName("demo", "12345678")
+	name := imagebuilderJobName("demo", "12345678")
 	if !strings.HasPrefix(name, "imagebuild-demo-12345678-") {
 		t.Errorf("expected pod name imagebuild-demo-12345678-*, got %s", name)
 	}
 
-	name = imagebuilderPodName("this-name-has-more-than-24-characters-in-length", "12345678")
+	name = imagebuilderJobName("this-name-has-more-than-24-characters-in-length", "12345678")
 	if !strings.HasPrefix(name, "imagebuild-this-name-has-more-than-24-charac-12345678-") {
 		t.Errorf("expected pod name imagebuild-this-name-has-more-than-24-charac-12345678-*, got %s", name)
 	}
@@ -42,7 +43,7 @@ type imageBuildCase struct {
 	builderPodNodeSelector      map[string]string
 }
 
-func TestBuildPod(t *testing.T) {
+func TestBuildJob(t *testing.T) {
 	emptyEnv := make(map[string]interface{})
 
 	env := make(map[string]interface{})
@@ -50,7 +51,7 @@ func TestBuildPod(t *testing.T) {
 	buildArgsEnv := make(map[string]interface{})
 	buildArgsEnv["DRYCC_DOCKER_BUILD_ARGS_ENABLED"] = "1"
 	buildArgsEnv["KEY"] = "VALUE"
-	var pod *corev1.Pod
+	var job *batchv1.Job
 
 	emptyNodeSelector := make(map[string]string)
 
@@ -73,7 +74,7 @@ func TestBuildPod(t *testing.T) {
 	}
 	buildImageEnv := map[string]string{"REG_LOC": "on-cluster"}
 	for _, build := range imageBuilds {
-		pod = createBuilderPod(
+		job = createBuilderJob(
 			build.debug,
 			build.name,
 			build.namespace,
@@ -92,41 +93,41 @@ func TestBuildPod(t *testing.T) {
 			build.builderPodNodeSelector,
 		)
 
-		if pod.ObjectMeta.Name != build.name {
-			t.Errorf("expected %v but returned %v ", build.name, pod.ObjectMeta.Name)
+		if job.ObjectMeta.Name != build.name {
+			t.Errorf("expected %v but returned %v ", build.name, job.ObjectMeta.Name)
 		}
-		if pod.ObjectMeta.Namespace != build.namespace {
-			t.Errorf("expected %v but returned %v ", build.namespace, pod.ObjectMeta.Namespace)
+		if job.ObjectMeta.Namespace != build.namespace {
+			t.Errorf("expected %v but returned %v ", build.namespace, job.ObjectMeta.Namespace)
 		}
 
-		checkForEnv(t, pod, "SOURCE_VERSION", build.gitShortHash)
-		checkForEnv(t, pod, "TAR_PATH", build.tarKey)
-		checkForEnv(t, pod, "IMG_NAME", build.imgName)
-		checkForEnv(t, pod, "REG_LOC", "on-cluster")
+		checkForEnv(t, job, "SOURCE_VERSION", build.gitShortHash)
+		checkForEnv(t, job, "TAR_PATH", build.tarKey)
+		checkForEnv(t, job, "IMG_NAME", build.imgName)
+		checkForEnv(t, job, "REG_LOC", "on-cluster")
 		if _, ok := build.env["DRYCC_DOCKER_BUILD_ARGS_ENABLED"]; ok {
-			checkForEnv(t, pod, "DOCKER_BUILD_ARGS", `{"DRYCC_DOCKER_BUILD_ARGS_ENABLED":"1","KEY":"VALUE"}`)
+			checkForEnv(t, job, "DOCKER_BUILD_ARGS", `{"DRYCC_DOCKER_BUILD_ARGS_ENABLED":"1","KEY":"VALUE"}`)
 		}
 		if build.imagebuilderImage != "" {
-			if pod.Spec.Containers[0].Image != build.imagebuilderImage {
-				t.Errorf("expected %v but returned %v", build.imagebuilderImage, pod.Spec.Containers[0].Image)
+			if job.Spec.Template.Spec.Containers[0].Image != build.imagebuilderImage {
+				t.Errorf("expected %v but returned %v", build.imagebuilderImage, job.Spec.Template.Spec.Containers[0].Image)
 			}
 		}
 		if build.imagebuilderImagePullPolicy != "" {
-			if pod.Spec.Containers[0].ImagePullPolicy != "" {
-				if pod.Spec.Containers[0].ImagePullPolicy != build.imagebuilderImagePullPolicy {
-					t.Errorf("expected %v but returned %v", build.imagebuilderImagePullPolicy, pod.Spec.Containers[0].ImagePullPolicy)
+			if job.Spec.Template.Spec.Containers[0].ImagePullPolicy != "" {
+				if job.Spec.Template.Spec.Containers[0].ImagePullPolicy != build.imagebuilderImagePullPolicy {
+					t.Errorf("expected %v but returned %v", build.imagebuilderImagePullPolicy, job.Spec.Template.Spec.Containers[0].ImagePullPolicy)
 				}
 			}
 		}
 
-		if len(pod.Spec.NodeSelector) > 0 || len(build.builderPodNodeSelector) > 0 {
-			assert.Equal(t, pod.Spec.NodeSelector, build.builderPodNodeSelector, "node selector")
+		if len(job.Spec.Template.Spec.NodeSelector) > 0 || len(build.builderPodNodeSelector) > 0 {
+			assert.Equal(t, job.Spec.Template.Spec.NodeSelector, build.builderPodNodeSelector, "node selector")
 		}
 	}
 }
 
-func checkForEnv(t *testing.T, pod *corev1.Pod, key, expVal string) {
-	val, err := envValueFromKey(pod, key)
+func checkForEnv(t *testing.T, job *batchv1.Job, key, expVal string) {
+	val, err := envValueFromKey(job, key)
 	if err != nil {
 		t.Errorf("%v", err)
 	}
@@ -135,8 +136,8 @@ func checkForEnv(t *testing.T, pod *corev1.Pod, key, expVal string) {
 	}
 }
 
-func envValueFromKey(pod *corev1.Pod, key string) (string, error) {
-	for _, env := range pod.Spec.Containers[0].Env {
+func envValueFromKey(job *batchv1.Job, key string) (string, error) {
+	for _, env := range job.Spec.Template.Spec.Containers[0].Env {
 		if env.Name == key {
 			return env.Value, nil
 		}
