30 September 2021
Part of our job as software engineers involves building or using local development environments. When working on service-oriented architectures, these local environments rarely run a single service at once. We often end up with services listening on various ports: 8080, 8081, 8082, 3000, 5000. The list never really stops growing. Memorizing which port maps to which service adds unnecessary cognitive load when we want 100% of our focus to be on our code.
I believe we can improve the developer experience by making our local container-based environments slightly more production-like. We may never need to type
localhost into our browser or terminal ever again.
Why I don't use localhost anymore
For engineers who work on many different services,
localhost:8080 can have a different meaning depending on which day of the week it is. If I type
localhost into my browser, I get seemingly random completions that mainly depend on what I was working on last.
People don't think in ports. We think of our applications as services, and that is what our architectures reflect. If I want to send a request to the authentication service running on my machine, I don't want to translate that destination into something unrelated like
localhost:8080. The same goes for my web frontend, which listens on
localhost:8081, and my backend on
localhost:8082. Or was it the other way around?
In production, we don't have this problem. Modern container-based environments often have a reverse proxy that forwards requests from external clients to internal services. With a setup like that, all we need, for instance, is a different hostname for each service.
.vcap.me hostname resolves to
127.0.0.1, just like
localhost does. If I send a request to http://auth.vcap.me/, whatever is listening on port 80 on my machine receives the request. By making it a reverse proxy, I can have a hostname for each service I am running locally. I don't need to specify ports anymore. All I need is a good name.
I have been using this setup for several months now, and I love it. I can type
front + ENTER in my browser to open my web frontend. I can search
curl auth in my shell history for a list of requests sent to my authentication service. Every day I save time and can maintain focus on the task at hand.
Setup with Docker Compose
For a local environment based on Compose, I recommend using nginx-proxy as the reverse proxy. It reads running containers' metadata, finds environment variables like
VIRTUAL_HOST, and uses that information to configure a high-performance web proxy. Here is what your
docker-compose.yml file could look like:
version: "3.9" services: reverse-proxy: image: jwilder/nginx-proxy:0.9-alpine ports: - 80:80 volumes: - /var/run/docker.sock:/tmp/docker.sock:ro auth: build: ./auth environment: - VIRTUAL_HOST=auth.vcap.me # for reverse proxy - VIRTUAL_PORT=8080 # for reverse proxy frontend: build: ./frontend environment: - VIRTUAL_HOST=front.vcap.me # for reverse proxy - VIRTUAL_PORT=3000 # for reverse proxy backend: build: ./backend environment: - VIRTUAL_HOST=back.vcap.me # for reverse proxy - VIRTUAL_PORT=8080 # for reverse proxy
Setup with Kubernetes
In a Kubernetes environment, the standard reverse proxy is the NGINX ingress controller. To run Kubernetes locally, I recommend using kind (aka Kubernetes in Docker). Follow these steps to deploy the ingress controller:
Configure your cluster to allow the ingress controller to run and listen on your machine's port 80. Write the following to a file called
kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane kubeadmConfigPatches: - | kind: InitConfiguration nodeRegistration: kubeletExtraArgs: node-labels: "ingress-ready=true" extraPortMappings: - containerPort: 80 hostPort: 80
Create the cluster and deploy the NGINX ingress controller:
kind create cluster --config=kind-cluster.yml kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/kind/deploy.yaml kubectl rollout status deployment --namespace=ingress-nginx ingress-nginx-controller kubectl delete validatingwebhookconfiguration ingress-nginx-admission # Workaround for this issue: https://github.com/kubernetes/ingress-nginx/issues/5401
All you need to expose a service with this setup is an Ingress resource with a
.vcap.me hostname. The NGINX reverse proxy will configure itself based on the rules you specify. Try it out with these manifests that use
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: httpbin spec: rules: - host: httpbin.vcap.me http: paths: - path: / pathType: Prefix backend: service: name: httpbin port: name: http --- apiVersion: v1 kind: Service metadata: name: httpbin spec: selector: app: httpbin ports: - name: http port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: httpbin spec: selector: matchLabels: app: httpbin template: metadata: labels: app: httpbin spec: containers: - name: httpbin image: kennethreitz/httpbin ports: - name: http containerPort: 80 resources: limits: cpu: 1 memory: 128Mi
Developer productivity is a determining factor in the success of any web product. The cognitive load of running many services locally can slow engineers down, but modern container-based development environments provide opportunities for improvement. Solutions used in production, like reverse proxies, can also apply to local development. What other enhancements can we make to improve our teams' efficiency further?