How to Keep Your Development Environment Clean

Tin Rabzelj
10 October, 2017

cover

Imagine you're working on a project which includes a PostgreSQL database, Redis cache layer, Elasticsearch engine, Consul for dynamic configuration, and more. The last thing you want is to install all of these services on your local machine during development.

Enter Docker.

Docker is a tool for working with containers, which are isolated spaces where apps can run. It enables painless installation of services and sharing development environments with others. The same applies for production, amongst many other benefits.

Getting started

Install Docker and Docker Compose.

Basic example

Lets say you need a MySQL database. Simply run the following command.

$ docker run --name database -p 3306:3306 -e MYSQL_ROOT_PASSWORD=1234 -d mysql

This creates a container named database running a MySQL instance on port 3306. Environment variable MYSQL_ROOT_PASSWORD holds the root user's password.

To get container's IP address, run the following command.

$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' database

You can use MySQL CLI tool like this.

$ docker exec -it database mysql -u root -p

You could then run another container, with a different version of MySQL server, on port 3307 by specifying a tag.

$ docker run --name database2 -p 3307:3306 -e MYSQL_ROOT_PASSWORD=1234 -d mysql:5.6

To find all available Docker images visit Docker Hub.

Using Docker Compose

This section shows how to set up a hypothetical development environment involving a MongoDB database, a back-end service in Go and a static frontend site. The end product is a website, which displays anonymous posts.

MongoDB

Inside project's root directory create a docker-compose.yaml file with the following content.

version: '3.3'
services:
  mongo:
    image: 'mongo:latest'
    container_name: 'mongo'
    ports:
      - '27100:27017'

This defines a MongoDB service called mongo.

In the same directory run this command to build it.

$ docker-compose up -d --build

Or by specifying the location of docker-compose.yaml file.

$ docker-compose up -d --build -f ./docker-compose.yaml

List currently running containers.

$ docker-compose ps
Name              Command             State            Ports
----------------------------------------------------------------------
mongo   docker-entrypoint.sh mongod   Up      0.0.0.0:27100->27017/tcp

Web service in Go

Define another service inside docker-compose.yaml, which depends on mongo.

version: '3.3'
services:
  api:
    container_name: 'api'
    build: './api'
    ports:
      - '8080:8080'
    volumes:
      - './api:/go/src/app'
    depends_on:
      - 'mongo'
  mongo:
    image: 'mongo:latest'
    container_name: 'mongo'
    ports:
      - '27100:27017'

The api service will be built using a custom "Dockerfile". Create it inside api directory. Name it exactly Dockerfile, without an extension.

FROM golang:1.8

WORKDIR /go/src/app
COPY . .

RUN go get github.com/pilu/fresh
RUN go-wrapper download
RUN go-wrapper install

CMD [ "fresh" ]

This Dockerfile extends official golang image, installs the pilu/fresh package and all other dependencies using go-wrapper utility commands. The default command is set to fresh. This will enable live reloading for development.

Create main.go file inside api directory.

package main

import (
  "encoding/json"
  "io/ioutil"
  "log"
  "net/http"
  "os"
  "time"
  "github.com/rs/cors"
  "github.com/gorilla/mux"
  "gopkg.in/mgo.v2"
)

type Post struct {
  Text      string    `json:"text" bson:"text"`
  CreatedAt time.Time `json:"createdAt" bson:"created_at"`
}

var posts *mgo.Collection

func main() {
  // Connect to mongo
  session, err := mgo.Dial("mongo:27017")
  if err != nil {
    log.Fatalln(err)
    log.Fatalln("mongo err")
    os.Exit(1)
  }
  defer session.Close()
  session.SetMode(mgo.Monotonic, true)
  // Get posts collection
  posts = session.DB("app").C("posts")

  // Set up routes
  r := mux.NewRouter()
  r.HandleFunc("/posts", createPost).
    Methods("POST")
  http.ListenAndServe(":8080", cors.AllowAll().Handler(r))
  log.Println("Listening on port 8080...")
}

func createPost(w http.ResponseWriter, r *http.Request) {
  // Read body
  data, err := ioutil.ReadAll(r.Body)
  if err != nil {
    responseError(w, err.Error(), http.StatusBadRequest)
    return
  }
  // Read post
  post := &Post{}
  err = json.Unmarshal(data, post)
  if err != nil {
    responseError(w, err.Error(), http.StatusBadRequest)
    return
  }
  post.CreatedAt = time.Now().UTC()
  // Insert new post
  if err := posts.Insert(post); err != nil {
    responseError(w, err.Error(), http.StatusInternalServerError)
    return
  }
  responseJSON(w, post)
}

func responseError(w http.ResponseWriter, message string, code int) {
  w.Header().Set("Content-Type", "application/json")
  w.WriteHeader(code)
  json.NewEncoder(w).Encode(map[string]string{"error": message})
}

func responseJSON(w http.ResponseWriter, data interface{}) {
  w.Header().Set("Content-Type", "application/json")
  json.NewEncoder(w).Encode(data)
}

Rebuild containers.

$ docker-compose up -d --build

Try calling the /posts endpoint.

$ curl localhost:8080/posts -d '{"text":"hello"}' 

Declare a new handler which reads all posts.

func readPosts(w http.ResponseWriter, r *http.Request) {
  result := []Post{}
  if err := posts.Find(nil).Sort("-created_at").All(&result); err != nil {
    responseError(w, err.Error(), http.StatusInternalServerError)
  } else {
    responseJSON(w, result)
  }
}

Hook it up inside the main function.

r.HandleFunc("/posts", readPosts).
  Methods("GET")

Without rebuilding containers, you should be able to call this endpoint.

$ curl localhost:8080/posts

Serving a static site with NGINX

Declare a new service inside docker-compose.yaml file called web.

web:
  container_name: 'web'
  image: 'nginx:latest'
  ports:
    - '8081:80'
  volumes:
    - './web:/usr/share/nginx/html'
  depends_on:
    - 'api'

Create a Dockerfile inside web directory.

FROM nginx
COPY . /usr/share/nginx/html

Create the index.html file.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Dockerized Posts</title>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<body>
  <h1>Dockerized Posts</h1>
  <form id="form">
    <input type="text" placeholder="New post..." id="post-input">
  </form>
  <div id="posts"></div>
  <script>
    $(document).ready(function() {
      $.get('http://localhost:8080/posts', function(posts) {
        $list = $('#posts');
        for (var i = 0; i < posts.length; i++) {
          $list.append('<p>' + posts[i].text + '</p>');
        }
      });
      $('#form').submit(function(event) {
        event.preventDefault();
        var text = $('#post-input').val();
        $.post('http://localhost:8080/posts', JSON.stringify({text: text}), function() {
          location.reload();
        });
      });
    });
  </script>
</body>
</html>

Now build it.

$ docker-compose up -d --build

Navigate to http://localhost:8081/ with your browser and try creating a few posts.

Site

Testing

Here's how you can run Go tests. Inside api create sanity_test.go test file.

package main

import "testing"

func TestSanity(t *testing.T) {
  t.Log("Has sanity")
}

Then run go test -v inside api container.

$ docker-compose run api go test -v
=== RUN   TestSanity
--- PASS: TestSanity (0.00s)
        sanity_test.go:6: Has sanity
PASS
ok      app     0.004s

If you need a temporary database during testing, you could simply define another service.

Conclusion

These were just a couple of examples of why the world of containers is awesome. If you haven't already, give Docker a try. If not for production use, at least during development, because managing local installations of services is extremely unpleasant.

Get source code for the Docker Compose example on GitHub.

Newsletter

Get awesome articles delivered right to your doorstep

Protected by reCAPTCHA - Privacy - Terms

Comments

Sign in to post a comment