diff --git a/.travis.yml b/.travis.yml
index 4d8715574418768902a14c482ccc22200e1f2c7c..2176f307c4b7862535cfa74513683de328e35bbb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,9 @@
 ---
 language: go
 go:
-  - '1.10'
   - '1.11'
   - '1.12'
+  - '1.13'
 
 script:
   - make lint
diff --git a/Dockerfile b/Dockerfile
index d09d92cf3b05895467aae969c99f29761d085218..b75fd4a5f859a6b083a74c8a678b704b513221e6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.11
+FROM golang:1.13
 
 WORKDIR /go/src/github.com/mccutchen/go-httpbin
 
diff --git a/Makefile b/Makefile
index 0cdcfcf23c29316ea41b2066983fe5a811731e64..b2759bd15a935d420a5e88025aba2c681c48c1ba 100644
--- a/Makefile
+++ b/Makefile
@@ -4,8 +4,9 @@
 # go-httpbin:latest image use `make imagepush VERSION=latest)`
 VERSION        ?= $(shell git rev-parse --short HEAD)
 
-# Override this to deploy to a different App Engine project
+# Override these values to deploy to a different App Engine project
 GCLOUD_PROJECT ?= httpbingo
+GCLOUD_ACCOUNT ?= mccutchen@gmail.com
 
 # Built binaries will be placed here
 DIST_PATH  	  ?= dist
@@ -72,10 +73,10 @@ lint: $(GOLINT)
 # deploy & run locally
 # =============================================================================
 deploy: build
-	gcloud app deploy --quiet --project=$(GCLOUD_PROJECT) --version=$(VERSION) --promote
+	gcloud --account=$(GCLOUD_ACCOUNT) app deploy --quiet --project=$(GCLOUD_PROJECT) --version=$(VERSION) --promote
 
 stagedeploy: build
-	gcloud app deploy --quiet --project=$(GCLOUD_PROJECT) --version=$(VERSION) --no-promote
+	gcloud --account=$(GCLOUD_ACCOUNT) app deploy --quiet --project=$(GCLOUD_PROJECT) --version=$(VERSION) --no-promote
 
 run: build
 	$(DIST_PATH)/go-httpbin
diff --git a/cmd/maincmd/main.go b/cmd/maincmd/main.go
index 7393bfe516afd9840c47723444cde0d53535eae8..ebe53a010030684380e3c93da66254c791923dac 100644
--- a/cmd/maincmd/main.go
+++ b/cmd/maincmd/main.go
@@ -1,14 +1,16 @@
 package maincmd
 
 import (
-	"crypto/tls"
+	"context"
 	"flag"
 	"fmt"
 	"log"
 	"net"
 	"net/http"
 	"os"
+	"os/signal"
 	"strconv"
+	"syscall"
 	"time"
 
 	"github.com/mccutchen/go-httpbin/httpbin"
@@ -43,7 +45,7 @@ func Main() {
 	if maxBodySize == httpbin.DefaultMaxBodySize && os.Getenv("MAX_BODY_SIZE") != "" {
 		maxBodySize, err = strconv.ParseInt(os.Getenv("MAX_BODY_SIZE"), 10, 64)
 		if err != nil {
-			fmt.Printf("invalid value %#v for env var MAX_BODY_SIZE: %s\n", os.Getenv("MAX_BODY_SIZE"), err)
+			fmt.Fprintf(os.Stderr, "Error: invalid value %#v for env var MAX_BODY_SIZE: %s\n\n", os.Getenv("MAX_BODY_SIZE"), err)
 			flag.Usage()
 			os.Exit(1)
 		}
@@ -51,7 +53,7 @@ func Main() {
 	if maxDuration == httpbin.DefaultMaxDuration && os.Getenv("MAX_DURATION") != "" {
 		maxDuration, err = time.ParseDuration(os.Getenv("MAX_DURATION"))
 		if err != nil {
-			fmt.Printf("invalid value %#v for env var MAX_DURATION: %s\n", os.Getenv("MAX_DURATION"), err)
+			fmt.Fprintf(os.Stderr, "Error: invalid value %#v for env var MAX_DURATION: %s\n\n", os.Getenv("MAX_DURATION"), err)
 			flag.Usage()
 			os.Exit(1)
 		}
@@ -62,7 +64,7 @@ func Main() {
 	if port == defaultPort && os.Getenv("PORT") != "" {
 		port, err = strconv.Atoi(os.Getenv("PORT"))
 		if err != nil {
-			fmt.Printf("invalid value %#v for env var PORT: %s\n", os.Getenv("PORT"), err)
+			fmt.Fprintf(os.Stderr, "Error: invalid value %#v for env var PORT: %s\n\n", os.Getenv("PORT"), err)
 			flag.Usage()
 			os.Exit(1)
 		}
@@ -75,8 +77,29 @@ func Main() {
 		httpsKeyFile = os.Getenv("HTTPS_KEY_FILE")
 	}
 
+	var serveTLS bool
+	if httpsCertFile != "" || httpsKeyFile != "" {
+		serveTLS = true
+		if httpsCertFile == "" || httpsKeyFile == "" {
+			fmt.Fprintf(os.Stderr, "Error: https cert and key must both be provided\n\n")
+			flag.Usage()
+			os.Exit(1)
+		}
+	}
+
 	logger := log.New(os.Stderr, "", 0)
 
+	// A hacky log helper function to ensure that shutdown messages are
+	// formatted the same as other messages.  See StdLogObserver in
+	// httpbin/middleware.go for the format we're matching here.
+	serverLog := func(msg string, args ...interface{}) {
+		const (
+			logFmt  = "time=%q msg=%q"
+			dateFmt = "2006-01-02T15:04:05.9999"
+		)
+		logger.Printf(logFmt, time.Now().Format(dateFmt), fmt.Sprintf(msg, args...))
+	}
+
 	h := httpbin.New(
 		httpbin.WithMaxBodySize(maxBodySize),
 		httpbin.WithMaxDuration(maxDuration),
@@ -90,22 +113,41 @@ func Main() {
 		Handler: h.Handler(),
 	}
 
-	var listenErr error
-	if httpsCertFile != "" && httpsKeyFile != "" {
-		cert, err := tls.LoadX509KeyPair(httpsCertFile, httpsKeyFile)
-		if err != nil {
-			logger.Fatal("Failed to generate https key pair: ", err)
-		}
-		server.TLSConfig = &tls.Config{
-			Certificates: []tls.Certificate{cert},
+	// shutdownCh triggers graceful shutdown on SIGINT or SIGTERM
+	shutdownCh := make(chan os.Signal, 1)
+	signal.Notify(shutdownCh, syscall.SIGINT, syscall.SIGTERM)
+
+	// exitCh will be closed when it is safe to exit, after graceful shutdown
+	exitCh := make(chan struct{})
+
+	go func() {
+		sig := <-shutdownCh
+		serverLog("shutdown started by signal: %s", sig)
+
+		shutdownTimeout := maxDuration + 1*time.Second
+		ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
+		defer cancel()
+
+		server.SetKeepAlivesEnabled(false)
+		if err := server.Shutdown(ctx); err != nil {
+			serverLog("shutdown error: %s", err)
 		}
-		logger.Printf("go-httpbin listening on https://%s", listenAddr)
-		listenErr = server.ListenAndServeTLS("", "")
+
+		close(exitCh)
+	}()
+
+	var listenErr error
+	if serveTLS {
+		serverLog("go-httpbin listening on https://%s", listenAddr)
+		listenErr = server.ListenAndServeTLS(httpsCertFile, httpsKeyFile)
 	} else {
-		logger.Printf("go-httpbin listening on http://%s", listenAddr)
+		serverLog("go-httpbin listening on http://%s", listenAddr)
 		listenErr = server.ListenAndServe()
 	}
-	if listenErr != nil {
-		logger.Fatalf("Failed to listen: %s", listenErr)
+	if listenErr != nil && listenErr != http.ErrServerClosed {
+		logger.Fatalf("failed to listen: %s", listenErr)
 	}
+
+	<-exitCh
+	serverLog("shutdown finished")
 }