diff --git a/.github/workflows/continuous_delivery.yaml b/.github/workflows/continuous_delivery.yaml
deleted file mode 100644
index 01e35dc1483775568f9e0b116a897183de37f851..0000000000000000000000000000000000000000
--- a/.github/workflows/continuous_delivery.yaml
+++ /dev/null
@@ -1,105 +0,0 @@
-# This workflow implements continuous delivery with automated testing and fully
-# autonomous deploys to production on merge.
-name: CD
-
-# Translated: "Execute this workflow on pushes to main OR on pull requests
-# opened against main"
-#
-# See this question and answer for what we're solving here:
-# https://github.community/t5/GitHub-Actions/How-to-trigger-an-action-on-push-or-pull-request-but-not-both/m-p/36155#M2460
-on:
-  push:
-    branches: [main]
-  pull_request:
-    branches: [main]
-
-jobs:
-  test:
-    name: Test
-    runs-on: ubuntu-latest
-    steps:
-    - name: Setup
-      uses: actions/setup-go@v2
-      with:
-        go-version: '1.18'
-
-    - name: Checkout
-      uses: actions/checkout@v2
-
-    - name: Build
-      run: make build
-
-    - name: Build docker image
-      run: make image
-
-    - name: Test
-      run: git show --stat && make testci
-
-    - name: Code coverage
-      uses: codecov/codecov-action@v1
-      with:
-        file: ./coverage.txt
-
-  regression_test:
-    name: Regression Tests
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        go_version:
-        - '1.17'
-        - '1.16'
-
-    steps:
-    - name: Setup
-      uses: actions/setup-go@v2
-      with:
-        go-version: ${{matrix.go_version}}
-
-    - name: Checkout
-      uses: actions/checkout@v2
-
-    - name: Build
-      run: make build
-
-    - name: Test
-      run: make test
-
-  production_deploy:
-    name: Production Deploy
-    if: github.ref == 'refs/heads/main'
-    runs-on: ubuntu-latest
-    needs: [test]
-
-    permissions:
-      deployments: write
-
-    steps:
-    - name: Checkout
-      uses: actions/checkout@v2
-
-    - name: Setup flyctl
-      uses: superfly/flyctl-actions/setup-flyctl@master
-
-    - name: Notify start
-      id: deployment
-      uses: bobheadxi/deployments@v1
-      with:
-        step: start
-        token: ${{ secrets.GITHUB_TOKEN }}
-        env: production
-
-    - name: Deploy
-      run: "flyctl deploy --strategy rolling"
-      env:
-        DOCKER_BUILDKIT: 1
-        FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
-
-    - name: Notify finish
-      uses: bobheadxi/deployments@v1
-      with:
-        step: finish
-        deployment_id: ${{ steps.deployment.outputs.deployment_id }}
-        token: ${{ secrets.GITHUB_TOKEN }}
-        env: production
-        status: ${{ job.status }}
-        env_url: 'https://httpbingo.org'
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 0b42e002138505342d0521d5ead4e1822f59d141..b1e57069ca2c1466b3a46c20d5707a01a5dedc67 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -15,7 +15,6 @@ jobs:
       - uses: actions/setup-go@v2
         with:
           go-version: '1.18'
-      - name: golangci-lint
-        uses: golangci/golangci-lint-action@v3.1.0
+      - uses: golangci/golangci-lint-action@v3.1.0
         with:
           version: v1.45.2
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a528f37398153dbcdce7ba7c659f06be0655710a
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,51 @@
+name: Test
+
+# Translated: "Execute this workflow on pushes to main OR on pull requests
+# opened against main"
+#
+# See this question and answer for what we're solving here:
+# https://github.community/t5/GitHub-Actions/How-to-trigger-an-action-on-push-or-pull-request-but-not-both/m-p/36155#M2460
+on:
+  push:
+    branches: [main]
+  pull_request:
+    branches: [main]
+
+jobs:
+  test:
+    name: Test
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/setup-go@v2
+      with:
+        go-version: '1.18'
+
+    - uses: actions/checkout@v2
+
+    - run: make testci
+
+    - uses: codecov/codecov-action@v1
+      with:
+        file: ./coverage.txt
+
+    - run: make image
+
+  regression_tests:
+    name: Regression Tests
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        go_version:
+        - '1.17'
+        - '1.16'
+
+    steps:
+    - uses: actions/setup-go@v2
+      with:
+        go-version: ${{matrix.go_version}}
+
+    - uses: actions/checkout@v2
+
+    - run: make test
diff --git a/Makefile b/Makefile
index 1c11590595517d87aca45ff664cb85d27a5151c1..e5e0d135d2a11b393d6a6e7f2fd83c1dcd719ed8 100644
--- a/Makefile
+++ b/Makefile
@@ -1,24 +1,7 @@
-.PHONY: clean deploy deps gcloud-auth image imagepush lint run stagedeploy test testci testcover
-
 # The version that will be used in docker tags (e.g. to push a
 # go-httpbin:latest image use `make imagepush VERSION=latest)`
-VERSION ?= $(shell git rev-parse --short HEAD)
-
-# Override these values to deploy to a different Cloud Run project
-GCLOUD_PROJECT ?= httpbingo
-GCLOUD_ACCOUNT ?= mccutchen@gmail.com
-GCLOUD_REGION  ?= us-central1
-
-# The version tag for the Cloud Run deployment (override this to adjust
-# pre-production URLs)
-GCLOUD_TAG ?= "v-$(VERSION)"
-
-# Run gcloud in a container to avoid needing to install the SDK locally
-GCLOUD_COMMAND ?= ./bin/gcloud
-
-# We push docker images to both docker hub and gcr.io
-DOCKER_TAG_DOCKERHUB ?= mccutchen/go-httpbin:$(VERSION)
-DOCKER_TAG_GCLOUD    ?= gcr.io/$(GCLOUD_PROJECT)/go-httpbin:$(VERSION)
+VERSION    ?= $(shell git rev-parse --short HEAD)
+DOCKER_TAG ?= mccutchen/go-httpbin:$(VERSION)
 
 # Built binaries will be placed here
 DIST_PATH  	  ?= dist
@@ -41,15 +24,18 @@ GO_SOURCES = $(wildcard **/*.go)
 # =============================================================================
 build: $(DIST_PATH)/go-httpbin
 
+buildtests: $(DIST_PATH)/go-httpbin.test
+
 $(DIST_PATH)/go-httpbin: $(GO_SOURCES)
 	mkdir -p $(DIST_PATH)
 	CGO_ENABLED=0 go build -ldflags="-s -w" -o $(DIST_PATH)/go-httpbin ./cmd/go-httpbin
 
-buildtests:
+$(DIST_PATH)/go-httpbin.test: $(GO_SOURCES)
 	CGO_ENABLED=0 go test -ldflags="-s -w" -v -c -o $(DIST_PATH)/go-httpbin.test ./httpbin
 
 clean:
 	rm -rf $(DIST_PATH) $(COVERAGE_PATH)
+.PHONY: clean
 
 
 # =============================================================================
@@ -57,6 +43,7 @@ clean:
 # =============================================================================
 test:
 	go test $(TEST_ARGS) ./...
+.PHONY: test
 
 
 # Test command to run for continuous integration, which includes code coverage
@@ -65,15 +52,18 @@ test:
 testci: build
 	go test $(TEST_ARGS) $(COVERAGE_ARGS) ./...
 	git diff --exit-code
+.PHONY: testci
 
 testcover: testci
 	go tool cover -html=$(COVERAGE_PATH)
+.PHONY: testcover
 
 lint: $(TOOL_GOLINT) $(TOOL_STATICCHECK)
 	test -z "$$(gofmt -d -s -e .)" || (echo "Error: gofmt failed"; gofmt -d -s -e . ; exit 1)
 	go vet ./...
 	$(TOOL_GOLINT) -set_exit_status ./...
 	$(TOOL_STATICCHECK) ./...
+.PHONY: lint
 
 
 # =============================================================================
@@ -81,61 +71,26 @@ lint: $(TOOL_GOLINT) $(TOOL_STATICCHECK)
 # =============================================================================
 run: build
 	$(DIST_PATH)/go-httpbin
+.PHONY: run
 
 watch: $(TOOL_REFLEX)
 	reflex -s -r '\.(go|html)$$' make run
-
-
-# =============================================================================
-# deploy to fly.io
-# =============================================================================
-deploy:
-	flyctl deploy --strategy=rolling
-
-
-# =============================================================================
-# deploy to google cloud run
-# =============================================================================
-deploy-cloud-run: gcloud-auth imagepush
-	$(GCLOUD_COMMAND) beta run deploy \
-		$(GCLOUD_PROJECT) \
-		--image=$(DOCKER_TAG_GCLOUD) \
-		--revision-suffix=$(VERSION) \
-		--tag=$(GCLOUD_TAG) \
-		--project=$(GCLOUD_PROJECT) \
-		--region=$(GCLOUD_REGION) \
-		--allow-unauthenticated \
-		--platform=managed
-	$(GCLOUD_COMMAND) run services update-traffic --to-latest
-
-stagedeploy-cloud-run: gcloud-auth imagepush
-	$(GCLOUD_COMMAND) beta run deploy \
-		$(GCLOUD_PROJECT) \
-		--image=$(DOCKER_TAG_GCLOUD) \
-		--revision-suffix=$(VERSION) \
-		--tag=$(GCLOUD_TAG) \
-		--project=$(GCLOUD_PROJECT) \
-		--region=$(GCLOUD_REGION) \
-		--allow-unauthenticated \
-		--platform=managed \
-		--no-traffic
-
-gcloud-auth:
-	@$(GCLOUD_COMMAND) auth list | grep '^\*' | grep -q $(GCLOUD_ACCOUNT) || $(GCLOUD_COMMAND) auth login $(GCLOUD_ACCOUNT)
-	@$(GCLOUD_COMMAND) auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://gcr.io
+.PHONY: watch
 
 
 # =============================================================================
 # docker images
 # =============================================================================
 image:
-	DOCKER_BUILDKIT=1 docker build -t $(DOCKER_TAG_DOCKERHUB) .
+	DOCKER_BUILDKIT=1 docker build -t $(DOCKER_TAG) .
+.PHONY: image
 
 imagepush:
 	docker buildx create --name httpbin
 	docker buildx use httpbin
-	docker buildx build --push --platform linux/amd64,linux/arm64 -t $(DOCKER_TAG_DOCKERHUB) .
+	docker buildx build --push --platform linux/amd64,linux/arm64 -t $(DOCKER_TAG) .
 	docker buildx rm httpbin
+.PHONY: imagepush
 
 
 # =============================================================================
diff --git a/app.yaml b/app.yaml
deleted file mode 100644
index 0b4e957ae267e4734d382bc034418ec4d0ac42ca..0000000000000000000000000000000000000000
--- a/app.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
----
-runtime: go113
-
-main: ./cmd/go_httpbin
-
-handlers:
-  # Always redirect index requests to https
-  - url: /
-    script: auto
-    secure: always
-    redirect_http_response_code: 301
-
-  # Allow requests for any other resources via either http or https
-  - url: /.+
-    script: auto
-    secure: optional
diff --git a/bin/gcloud b/bin/gcloud
deleted file mode 100755
index 9f1ec9f500d14454c45cc7d248fea3bd65fb52f1..0000000000000000000000000000000000000000
--- a/bin/gcloud
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-#
-# A wrapper that executes the gcloud CLI in a docker container, to avoid
-# requiring a local installation.
-#
-# Adapted from this helpful blog post:
-# https://blog.scottlowe.org/2018/09/13/running-gcloud-cli-in-a-docker-container/
-
-GCLOUD_SDK_TAG="312.0.0"
-
-exec docker run \
-    --rm -it \
-    --workdir /code \
-    -v $PWD:/code \
-    -v $HOME/.config/gcloud:/root/.config/gcloud \
-    google/cloud-sdk:$GCLOUD_SDK_TAG \
-    gcloud $*
diff --git a/cmd/README.md b/cmd/README.md
deleted file mode 100644
index 267f207348ea6249cd75e4f0fe44b06001d663e4..0000000000000000000000000000000000000000
--- a/cmd/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# What is going on here?
-
-## TL;DR
-
- * `cmd/maincmd` package exposes all of this app's command line functionality in a `Main()`
-
- * `cmd/go-httpbin` and `cmd/go_httpbin` build identical binaries using the
-   `maincmd` package for backwards compatibility reasons explained below
-
-## Why tho
-
-Originally, this project exposed only one command:
-
-    cmd/go-httpbin/main.go
-
-But the dash in that path was incompatible with Google App Engine's naming
-restrictions, so in [moving httpbingo.org onto Google App Engine][pr17], that
-path was (carelessly) renamed to
-
-    cmd/go_httpbin/main.go
-
-_That_ change had a number of unintended consequences:
-
- * It broke existing workflows built around `go get github.com/mccutchen/go-httpbin/cmd/go-httpbin`,
-   as suggested in the README
-
- * It broke the Makefile, which was still looking for `cmd/go-httpbin`
-
- * It broke the absolute aesthetic truth that CLI binaries should use dashes
-   instead of underscores for word separators
-
-So, to restore the former behavior while maintaining support for deploying to
-App Engine, the actual main functionality was extracted into the `cmd/maincmd`
-package here and shared between the other two.
-
-(This is pretty dumb, I know, but it seems to work.)
-
-[pr17]: https://github.com/mccutchen/go-httpbin/pull/17
diff --git a/cmd/go-httpbin/main.go b/cmd/go-httpbin/main.go
index 830cd9e1ee16a8da0f76f85cbcc1c07c77d54aa1..0743a117f37e6dcf49e3d79df18a2d58c91329b8 100644
--- a/cmd/go-httpbin/main.go
+++ b/cmd/go-httpbin/main.go
@@ -1,7 +1,154 @@
 package main
 
-import "github.com/mccutchen/go-httpbin/v2/cmd/maincmd"
+import (
+	"context"
+	"flag"
+	"fmt"
+	"log"
+	"net"
+	"net/http"
+	"os"
+	"os/signal"
+	"strconv"
+	"syscall"
+	"time"
+
+	"github.com/mccutchen/go-httpbin/v2/httpbin"
+)
+
+const (
+	defaultHost = "0.0.0.0"
+	defaultPort = 8080
+)
+
+var (
+	host          string
+	port          int
+	maxBodySize   int64
+	maxDuration   time.Duration
+	httpsCertFile string
+	httpsKeyFile  string
+)
 
 func main() {
-	maincmd.Main()
+	flag.StringVar(&host, "host", defaultHost, "Host to listen on")
+	flag.IntVar(&port, "port", defaultPort, "Port to listen on")
+	flag.StringVar(&httpsCertFile, "https-cert-file", "", "HTTPS Server certificate file")
+	flag.StringVar(&httpsKeyFile, "https-key-file", "", "HTTPS Server private key file")
+	flag.Int64Var(&maxBodySize, "max-body-size", httpbin.DefaultMaxBodySize, "Maximum size of request or response, in bytes")
+	flag.DurationVar(&maxDuration, "max-duration", httpbin.DefaultMaxDuration, "Maximum duration a response may take")
+	flag.Parse()
+
+	// Command line flags take precedence over environment vars, so we only
+	// check for environment vars if we have default values for our command
+	// line flags.
+	var err error
+	if maxBodySize == httpbin.DefaultMaxBodySize && os.Getenv("MAX_BODY_SIZE") != "" {
+		maxBodySize, err = strconv.ParseInt(os.Getenv("MAX_BODY_SIZE"), 10, 64)
+		if err != nil {
+			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)
+		}
+	}
+	if maxDuration == httpbin.DefaultMaxDuration && os.Getenv("MAX_DURATION") != "" {
+		maxDuration, err = time.ParseDuration(os.Getenv("MAX_DURATION"))
+		if err != nil {
+			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)
+		}
+	}
+	if host == defaultHost && os.Getenv("HOST") != "" {
+		host = os.Getenv("HOST")
+	}
+	if port == defaultPort && os.Getenv("PORT") != "" {
+		port, err = strconv.Atoi(os.Getenv("PORT"))
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Error: invalid value %#v for env var PORT: %s\n\n", os.Getenv("PORT"), err)
+			flag.Usage()
+			os.Exit(1)
+		}
+	}
+
+	if httpsCertFile == "" && os.Getenv("HTTPS_CERT_FILE") != "" {
+		httpsCertFile = os.Getenv("HTTPS_CERT_FILE")
+	}
+	if httpsKeyFile == "" && os.Getenv("HTTPS_KEY_FILE") != "" {
+		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),
+		httpbin.WithObserver(httpbin.StdLogObserver(logger)),
+	)
+
+	listenAddr := net.JoinHostPort(host, strconv.Itoa(port))
+
+	server := &http.Server{
+		Addr:    listenAddr,
+		Handler: h.Handler(),
+	}
+
+	// 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)
+		}
+
+		close(exitCh)
+	}()
+
+	var listenErr error
+	if serveTLS {
+		serverLog("go-httpbin listening on https://%s", listenAddr)
+		listenErr = server.ListenAndServeTLS(httpsCertFile, httpsKeyFile)
+	} else {
+		serverLog("go-httpbin listening on http://%s", listenAddr)
+		listenErr = server.ListenAndServe()
+	}
+	if listenErr != nil && listenErr != http.ErrServerClosed {
+		logger.Fatalf("failed to listen: %s", listenErr)
+	}
+
+	<-exitCh
+	serverLog("shutdown finished")
 }
diff --git a/cmd/go_httpbin/main.go b/cmd/go_httpbin/main.go
deleted file mode 100644
index 830cd9e1ee16a8da0f76f85cbcc1c07c77d54aa1..0000000000000000000000000000000000000000
--- a/cmd/go_httpbin/main.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package main
-
-import "github.com/mccutchen/go-httpbin/v2/cmd/maincmd"
-
-func main() {
-	maincmd.Main()
-}
diff --git a/cmd/maincmd/main.go b/cmd/maincmd/main.go
deleted file mode 100644
index 0778d5c5feba5d30e2ff01c0359a080475c6b2d1..0000000000000000000000000000000000000000
--- a/cmd/maincmd/main.go
+++ /dev/null
@@ -1,153 +0,0 @@
-package maincmd
-
-import (
-	"context"
-	"flag"
-	"fmt"
-	"log"
-	"net"
-	"net/http"
-	"os"
-	"os/signal"
-	"strconv"
-	"syscall"
-	"time"
-
-	"github.com/mccutchen/go-httpbin/v2/httpbin"
-)
-
-const defaultHost = "0.0.0.0"
-const defaultPort = 8080
-
-var (
-	host          string
-	port          int
-	maxBodySize   int64
-	maxDuration   time.Duration
-	httpsCertFile string
-	httpsKeyFile  string
-)
-
-// Main implements the go-httpbin CLI's main() function in a reusable way
-func Main() {
-	flag.StringVar(&host, "host", defaultHost, "Host to listen on")
-	flag.IntVar(&port, "port", defaultPort, "Port to listen on")
-	flag.StringVar(&httpsCertFile, "https-cert-file", "", "HTTPS Server certificate file")
-	flag.StringVar(&httpsKeyFile, "https-key-file", "", "HTTPS Server private key file")
-	flag.Int64Var(&maxBodySize, "max-body-size", httpbin.DefaultMaxBodySize, "Maximum size of request or response, in bytes")
-	flag.DurationVar(&maxDuration, "max-duration", httpbin.DefaultMaxDuration, "Maximum duration a response may take")
-	flag.Parse()
-
-	// Command line flags take precedence over environment vars, so we only
-	// check for environment vars if we have default values for our command
-	// line flags.
-	var err error
-	if maxBodySize == httpbin.DefaultMaxBodySize && os.Getenv("MAX_BODY_SIZE") != "" {
-		maxBodySize, err = strconv.ParseInt(os.Getenv("MAX_BODY_SIZE"), 10, 64)
-		if err != nil {
-			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)
-		}
-	}
-	if maxDuration == httpbin.DefaultMaxDuration && os.Getenv("MAX_DURATION") != "" {
-		maxDuration, err = time.ParseDuration(os.Getenv("MAX_DURATION"))
-		if err != nil {
-			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)
-		}
-	}
-	if host == defaultHost && os.Getenv("HOST") != "" {
-		host = os.Getenv("HOST")
-	}
-	if port == defaultPort && os.Getenv("PORT") != "" {
-		port, err = strconv.Atoi(os.Getenv("PORT"))
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "Error: invalid value %#v for env var PORT: %s\n\n", os.Getenv("PORT"), err)
-			flag.Usage()
-			os.Exit(1)
-		}
-	}
-
-	if httpsCertFile == "" && os.Getenv("HTTPS_CERT_FILE") != "" {
-		httpsCertFile = os.Getenv("HTTPS_CERT_FILE")
-	}
-	if httpsKeyFile == "" && os.Getenv("HTTPS_KEY_FILE") != "" {
-		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),
-		httpbin.WithObserver(httpbin.StdLogObserver(logger)),
-	)
-
-	listenAddr := net.JoinHostPort(host, strconv.Itoa(port))
-
-	server := &http.Server{
-		Addr:    listenAddr,
-		Handler: h.Handler(),
-	}
-
-	// 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)
-		}
-
-		close(exitCh)
-	}()
-
-	var listenErr error
-	if serveTLS {
-		serverLog("go-httpbin listening on https://%s", listenAddr)
-		listenErr = server.ListenAndServeTLS(httpsCertFile, httpsKeyFile)
-	} else {
-		serverLog("go-httpbin listening on http://%s", listenAddr)
-		listenErr = server.ListenAndServe()
-	}
-	if listenErr != nil && listenErr != http.ErrServerClosed {
-		logger.Fatalf("failed to listen: %s", listenErr)
-	}
-
-	<-exitCh
-	serverLog("shutdown finished")
-}
diff --git a/fly.toml b/fly.toml
deleted file mode 100644
index b798550d545462d576f04c59aadc695555800996..0000000000000000000000000000000000000000
--- a/fly.toml
+++ /dev/null
@@ -1,21 +0,0 @@
-app = "httpbingo"
-
-[[services]]
-  internal_port = 8080
-  protocol = "tcp"
-
-  [services.concurrency]
-    hard_limit = 25
-    soft_limit = 20
-
-  [[services.ports]]
-    handlers = ["http"]
-    port = "80"
-
-  [[services.ports]]
-    handlers = ["tls", "http"]
-    port = "443"
-
-  [[services.tcp_checks]]
-    interval = 10000
-    timeout = 2000