diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7fc523a28bca585bac39a550a71dfee795928852
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,13 @@
+language: go
+go:
+- 1.14.x
+env:
+  global:
+  - V=1
+before_script:
+- make bootstrap
+script:
+- make
+- make artifacts
+notifications:
+  email: false
diff --git a/Dockerfile b/Dockerfile
index 85de9b84fee852a228f8e999e5a07e544dbe02f5..bb9194fa94705dcb475a17d6bc394d27da87f75e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,26 +1,7 @@
-# Build the manager binary
-FROM golang:1.13 as builder
-
-WORKDIR /workspace
-# Copy the Go Modules manifests
-COPY go.mod go.mod
-COPY go.sum go.sum
-# cache deps before building and copying source so that we don't need to re-download as much
-# and so that source changes don't invalidate our downloaded layer
-RUN go mod download
-
-# Copy the go source
-COPY main.go main.go
-COPY api/ api/
-COPY controllers/ controllers/
-COPY provisioners/ provisioners/
-
-# Build
-RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go
-
 # Use distroless as minimal base image to package the manager binary
 # Refer to https://github.com/GoogleContainerTools/distroless for more details
 FROM gcr.io/distroless/static:latest
+ARG BINPATH="docker/bin/manager"
 WORKDIR /
-COPY --from=builder /workspace/manager .
+COPY $BINPATH .
 ENTRYPOINT ["/manager"]
diff --git a/Makefile b/Makefile
index abc3d637be14cb13c880030364081ad4fe8e50f1..83e45f125d5a413eab194d4fe00a259ab445ce4c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,11 @@
+PKG?=github.com/smallstep/step-issuer
+BINNAME?=manager
+PREFIX?=
+
+# Set V to 1 for verbose output from the Makefile
+Q=$(if $V,,@)
 # Image URL to use all building/pushing image targets
-IMG ?= step-issuer:latest
+IMG ?= smallstep/step-issuer:latest
 # Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
 CRD_OPTIONS ?= "crd:trivialVersions=true"
 
@@ -10,67 +16,217 @@ else
 GOBIN=$(shell go env GOBIN)
 endif
 
-all: manager
+all: build test lint
+
+.PHONY: all
+
+#########################################
+# Bootstrapping
+#########################################
+
+bootstra%:
+	# Using a released version of golangci-lint to take into account custom replacements in their go.mod
+	$Q curl -sSfL https://raw.githubusercontent.com/smallstep/cli/master/make/golangci-install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.23.8
+
+.PHONY: bootstra%
+
+#################################################
+# Determine the type of `push` and `version`
+#################################################
+
+# If TRAVIS_TAG is set then we know this ref has been tagged.
+ifdef TRAVIS_TAG
+VERSION := $(TRAVIS_TAG)
+NOT_RC  := $(shell echo $(VERSION) | grep -v -e -rc)
+	ifeq ($(NOT_RC),)
+PUSHTYPE := release-candidate
+	else
+PUSHTYPE := release
+	endif
+else
+VERSION ?= $(shell [ -d .git ] && git describe --tags --always --dirty="-dev")
+VERSION := $(or $(VERSION),v0.0.0)
+PUSHTYPE := master
+endif
+
+VERSION := $(shell echo $(VERSION) | sed 's/^v//')
+
+ifdef V
+$(info    TRAVIS_TAG is $(TRAVIS_TAG))
+$(info    VERSION is $(VERSION))
+$(info    PUSHTYPE is $(PUSHTYPE))
+endif
+
+#########################################
+# Test
+#########################################
 
-# Run tests
 test: generate fmt vet manifests
-	go test ./api/... ./controllers/... -coverprofile cover.out
+	$Q go test ./api/... ./controllers/... -coverprofile cover.out
 
-# Build manager binary
-manager: generate fmt vet
-	go build -o bin/manager main.go
+.PHONY: test
 
-# Run against the configured Kubernetes cluster in ~/.kube/config
-run: generate fmt vet
-	go run ./main.go
+#########################################
+# Build
+#########################################
+
+DATE    := $(shell date -u '+%Y-%m-%d %H:%M UTC')
+LDFLAGS := -ldflags='-w -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
+GOFLAGS := CGO_ENABLED=0
+
+build: $(PREFIX)bin/$(BINNAME)
+	@echo "Build Complete!"
+
+download:
+	$Q go mod download
+
+$(PREFIX)bin/$(BINNAME): download generate $(call rwildcard,*.go)
+	$Q mkdir -p $(@D)
+	$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG)
+
+#########################################
+# Generate
+#########################################
+
+# Generate code
+generate: controller-gen
+	$Q $(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths=./api/...
+
+# find or download controller-gen
+# download controller-gen if necessary
+controller-gen:
+ifeq (, $(shell which controller-gen))
+	$Q go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.2.5
+CONTROLLER_GEN=$(GOBIN)/controller-gen
+else
+CONTROLLER_GEN=$(shell which controller-gen)
+endif
+
+#########################################
+# Install
+#########################################
+
+# Generate manifests e.g. CRD, RBAC etc.
+manifests: controller-gen
+	$Q $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
 
 # Install CRDs into a cluster
 install: manifests
-	kubectl apply -f config/crd/bases
+	$Q kubectl apply -f config/crd/bases
 
 # Deploy controller in the configured Kubernetes cluster in ~/.kube/config
 deploy: manifests
-	kubectl apply -f config/crd/bases
-	kustomize build config/default | kubectl apply -f -
+	$Q kubectl apply -f config/crd/bases
+	$Q kustomize build config/default | kubectl apply -f -
 
-# Generate manifests e.g. CRD, RBAC etc.
-manifests: controller-gen
-	$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
+#########################################
+# Format and Linting
+#########################################
 
 # Run go fmt against code
 fmt:
-	go fmt ./...
+	$Q go fmt ./...
 
 # Run go vet against code
 vet:
-	go vet ./...
+	$Q go vet ./...
 
-# Generate code
-generate: controller-gen
-	$(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths=./api/...
+lint:
+	$Q LOG_LEVEL=error golangci-lint run
+
+.PHONY: fmt vet lint
+
+#########################################
+# Dev
+#########################################
+
+# Run against the configured Kubernetes cluster in ~/.kube/config
+run: generate
+	$Q go run ./main.go
+
+.PHONY: run
+
+#########################################
+# Clean
+#########################################
+
+clean:
+ifneq ($(BINNAME),"")
+	$Q rm -f bin/$(BINNAME)
+endif
 
-# Build the docker image
-docker-build: test
-	docker build . -t ${IMG}
+.PHONY: clean
+
+#################################################
+# Docker
+#################################################
+
+DOCKER_OUTPUT=$(OUTPUT_ROOT)docker/
+DOCKER_MAKE=V=$V GOOS_OVERRIDE='GOOS=linux GOARCH=amd64' PREFIX=$(1) make $(1)bin/$(2)
+DOCKER_BUILD=$Q docker build -t $(IMG) -f $(2) --build-arg BINPATH=$(DOCKER_OUTPUT)bin/$(1) .
+
+docker: docker-make Dockerfile
+	$(call DOCKER_BUILD,manager,Dockerfile)
 	@echo "updating kustomize image patch file for manager resource"
-	sed -i'' -e 's@image: .*@image: '"${IMG}"'@' ./config/default/manager_image_patch.yaml
+	$Q sed -i'' -e 's@image: .*@image: '"${IMG}"'@' ./config/default/manager_image_patch.yaml
 
-# Push the docker image
-docker-push:
-	docker push ${IMG}
+docker-make:
+	$Q mkdir -p $(DOCKER_OUTPUT)
+	$(call DOCKER_MAKE,$(DOCKER_OUTPUT),manager)
+
+.PHONY: docker docker-make
 
 # Make sure to run a local registry
 # docker run -d -p 5000:5000 --restart=always --name registry registry:2
-docker-dev:
-	docker tag ${IMG} localhost:5000/${IMG}
-	docker push localhost:5000/${IMG}
+docker-dev: docker
+	$Q docker tag ${IMG} localhost:5000/${IMG}
+	$Q docker push localhost:5000/${IMG}
 
-# find or download controller-gen
-# download controller-gen if necessary
-controller-gen:
-ifeq (, $(shell which controller-gen))
-	go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.2.5
-CONTROLLER_GEN=$(GOBIN)/controller-gen
-else
-CONTROLLER_GEN=$(shell which controller-gen)
-endif
+.PHONY: docker-dev
+
+#################################################
+# Releasing Docker Images
+#################################################
+
+DOCKER_TAG=docker tag smallstep/$(1):latest smallstep/$(1):$(2)
+DOCKER_PUSH=docker push smallstep/$(1):$(2)
+
+docker-tag:
+	$(call DOCKER_TAG,step-issuer,$(VERSION))
+
+docker-push-tag: docker-tag
+	$(call DOCKER_PUSH,step-issuer,$(VERSION))
+
+docker-push-tag-latest:
+	$(call DOCKER_PUSH,step-issuer,latest)
+
+# Rely on DOCKER_USERNAME and DOCKER_PASSWORD being set inside the CI or
+# equivalent environment
+docker-login:
+	# $Q docker login -u="$(DOCKER_USERNAME)" -p="$(DOCKER_PASSWORD)"
+
+.PHONY: docker-login docker-tag docker-push-tag docker-push-tag-latest
+
+#################################################
+# Targets for pushing the docker images
+#################################################
+
+# For all builds we build the docker container
+docker-master: docker
+
+# For all builds with a release candidate tag
+docker-release-candidate: docker-master docker-login docker-push-tag
+
+# For all builds with a release tag
+docker-release: docker-release-candidate docker-push-tag-latest
+
+.PHONY: docker-master docker-release-candidate docker-release
+
+#################################################
+# Targets for creating step artifacts
+#################################################
+
+# This command is called by travis directly *after* a successful build
+artifacts: docker-$(PUSHTYPE)
+
+.PHONY: artifacts
diff --git a/config/default/manager_image_patch.yaml b/config/default/manager_image_patch.yaml
index 4a371c546066454e826512518b3ac685df44d37b..4676f064563e15e014034bac2ad4fdaa9702d70b 100644
--- a/config/default/manager_image_patch.yaml
+++ b/config/default/manager_image_patch.yaml
@@ -8,5 +8,5 @@ spec:
     spec:
       containers:
       # Change the value of image field below to your controller image URL
-      - image: step-issuer:latest
+      - image: smallstep/step-issuer:latest
         name: manager
diff --git a/config/samples/deployment.yaml b/config/samples/deployment.yaml
index b2a7b8b8b99fac5d18ecf137ca41e7e78465f62b..fbdfe1a9b872c3be132a23232f8132baf6e5eca4 100644
--- a/config/samples/deployment.yaml
+++ b/config/samples/deployment.yaml
@@ -337,7 +337,7 @@ spec:
         - --enable-leader-election
         command:
         - /manager
-        image: smallstep/step-issuer:0.1.0
+        image: smallstep/step-issuer:latest
         name: manager
         resources:
           limits:
diff --git a/config/samples/stepissuer.yaml b/config/samples/stepissuer.yaml
index bc972f10b21e6af24daf38b5b45ecc8828ae9992..bb61f720bb708fb919814a920f285436bc9d6b6e 100644
--- a/config/samples/stepissuer.yaml
+++ b/config/samples/stepissuer.yaml
@@ -7,7 +7,7 @@ spec:
   # The CA URL.
   url: https://step-certificates.default.svc.cluster.local
   # The base64 encoded version of the CA root certificate in PEM format.
-  caBundle:  LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJpekNDQVRHZ0F3SUJBZ0lRTytFQWg4eS8wVjlQMFhwSHJWajVOVEFLQmdncWhrak9QUVFEQWpBa01TSXcKSUFZRFZRUURFeGxUZEdWd0lFTmxjblJwWm1sallYUmxjeUJTYjI5MElFTkJNQjRYRFRFNU1EZ3hNekU1TVRVdwpNbG9YRFRJNU1EZ3hNREU1TVRVd01sb3dKREVpTUNBR0ExVUVBeE1aVTNSbGNDQkRaWEowYVdacFkyRjBaWE1nClVtOXZkQ0JEUVRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQkFNVkw3VzBQbTNvSlVmSTR3WGQKa2xERW5uNVhTbWo4NlgwYW1DQTBnY08xdElUUG1DVzNCcGU0cE9vV1V2WlZlUWRvU2NxN3pua1V0Mi9HMnQxTgo3MWlqUlRCRE1BNEdBMVVkRHdFQi93UUVBd0lCQmpBU0JnTlZIUk1CQWY4RUNEQUdBUUgvQWdFQk1CMEdBMVVkCkRnUVdCQlJ1Y1ByVm5QdlpOMHI0QVU5TGcyL2VCcng3a2pBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlCUlJBdGsKNXpMY0doQ2FobVBuVzIwZExpdEMzRVdNaVE0bERwN2FFeitFUEFJaEFJOWZWczVxb0l0bVQ4anA2WktVNVEydQphRFBrOGsyQ25OMjdyRnNZV3VwTAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
+  caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJpekNDQVRHZ0F3SUJBZ0lRTytFQWg4eS8wVjlQMFhwSHJWajVOVEFLQmdncWhrak9QUVFEQWpBa01TSXcKSUFZRFZRUURFeGxUZEdWd0lFTmxjblJwWm1sallYUmxjeUJTYjI5MElFTkJNQjRYRFRFNU1EZ3hNekU1TVRVdwpNbG9YRFRJNU1EZ3hNREU1TVRVd01sb3dKREVpTUNBR0ExVUVBeE1aVTNSbGNDQkRaWEowYVdacFkyRjBaWE1nClVtOXZkQ0JEUVRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQkFNVkw3VzBQbTNvSlVmSTR3WGQKa2xERW5uNVhTbWo4NlgwYW1DQTBnY08xdElUUG1DVzNCcGU0cE9vV1V2WlZlUWRvU2NxN3pua1V0Mi9HMnQxTgo3MWlqUlRCRE1BNEdBMVVkRHdFQi93UUVBd0lCQmpBU0JnTlZIUk1CQWY4RUNEQUdBUUgvQWdFQk1CMEdBMVVkCkRnUVdCQlJ1Y1ByVm5QdlpOMHI0QVU5TGcyL2VCcng3a2pBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlCUlJBdGsKNXpMY0doQ2FobVBuVzIwZExpdEMzRVdNaVE0bERwN2FFeitFUEFJaEFJOWZWczVxb0l0bVQ4anA2WktVNVEydQphRFBrOGsyQ25OMjdyRnNZV3VwTAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
   # The provisioner name, kid, and a reference to the provisioner password secret.
   provisioner:
     name: admin