I tend to use Docker containers more and more over Vagrant boxes for my small (and not-so-small) projects or experiments. It’s not breaking news that containerization has a lot of advantages over virtualization, especially when you are working on multiple projects on the same machine, or on projects with a service-oriented architecture.

One of the things I love the most with using Docker for local development is that instead of setting up every service used by my application, I can just use prebuilt images, configure and run them quickly. This is made even easier with Docker compose, since you can quickly setup, build, run or destroy your entire environment with just a few configuration and commands.

Let’s begin by creating a small and straightforward HTTP server with Go:

$ mkdir app
$ cd app
$ touch main.go
// main.go
package main

import (  
    "fmt"
    "net/http"
    "log"
)

func main() {

    http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello from Docker")
    })

    fmt.Println("Listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

The Dockerfile needed to build the image is straightforward:

FROM golang:1.8

RUN mkdir -p /go/src/app  
WORKDIR /go/src/app

ADD . /go/src/app

RUN go get -v  

We can now create a basic docker-compose.yml file listing the single service app. We also mount the host working directory as the directory containing the code to run in the container, so we can make changes to the code without having to rebuild everything.

version: '3'  
services:  
  app:
    build: .
    command: ["go", "run", "main.go"]
    volumes:
      - .:/go/src/app
    ports:
      - "8080:8080"

Run docker-compose up to build the image and launch the container. You should be able to request the page at http://localhost:8080 and get the response served by the Go application.

We now have a basic HTTP server running in Docker, great. Let’s say we also need PostgreSQL and Elasticsearch to run the application. We’ll add two services to the docker-compose.yml file using the existing postgres image from the official Docker repository and the elasticsearch image provided by Elastic.

We can use environment variables to customize the setup of the Postgres server, providing the DB name, user and password to use. We also expose the different ports needed to access these services.

version: '3'  
services:  
  db:
    image: postgres
    environment:
      POSTGRES_DB: dev
      POSTGRES_USER: postgres-dev
      POSTGRES_PASSWORD: s3cr3tp4ssw0rd
    ports:
      - 5432:5432
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:5.4.0
    ports:
      - 9200:9200
  app:
    build: .
    command: ["go", "run", "main.go"]
    volumes:
      - .:/go/src/app
    ports:
      - "80:8080"
    depends_on:
      - db
      - elasticsearch
    links:
      - db
      - elasticsearch

It is likely that you will want to control the order in which containers are launched, to ensure that the Postgres server is running before the Go server starts and tries to connect to it. You can use the wait-for-it.sh script for that. This is basically a wrapper that will wait for a specific TCP address to respond before launching the given command. We can use it to wrap the go run command so it can wait for the db service before executing it.

$ wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh

Finally, update the docker-compose.yml file to wrap the go run main.go command:

services:  
  app:
     command: ["./wait-for-it.sh", "db:5432", "--", "go", "run", "main.go"]

You can now run your service containers with docker-compose up -d. If you ever need to rebuild your entire environment from scratch, you can use docker-compose down --rmi all and then restart.

It can be cumbersome to manually restart the app container each time you make changes to the code. I like to use watchexec to automatically call docker-compose restart when files are being modified.

$ brew install watchexec
$ docker-compose up -d
$ watchexec --restart --exts "go" --watch . "docker-compose restart app"

Here we are, with a development environment ready, in only a matter of minutes. That’s what I like with using Docker and compose, I can just focus on application development and not care about long setup of the different services needed for my code to run locally.

Need to use Redis tomorrow to do some caching? Well, just add another service. Redis also has its official Docker image.

version: '3'  
services:  
  # ...
  redis:
    image: redis

And there you go, link this service to your app service, and you have a Redis instance ready to use, listening at redis:6379.

Happy coding!