Creating local Go dev environment with Docker and live code reloading

This post is a quick how-to for starting a new project in Go. It features:

  • Hot code reloading
  • Running multiple Docker containers with Docker Compose
  • Using Go Modules for managing dependencies

It’s best to show the above working together with an example project. We’re going to set up two separate services communicating with messages over NATS. The first one will receive messages on an HTTP endpoint and then publish them to a NATS topic. The other will subscribe to this topic and print incoming messages on the standard output.

It’s pretty simple architecture, right? To keep the examples short and make handling messages easier, we’re going to use the Watermill library.

I won’t go through all Go code here, but you can clone the example repository.

Requirements

You’re going to need Go 1.11+, Docker and Docker Compose installed on your local machine.

Go Modules

Go Modules were introduced in Go 1.11 and are (hopefully) the final solution for managing dependencies.

I recommend keeping the repository outside of GOPATH if you’re going to use them. If you’d prefer to use GOPATH anyway, make sure you’ve set GO111MODULE=on variable.

Let’s initialize modules for each of our packages (inside subscriber and publisher directories).

go mod init github.com/ThreeDotsLabs/nats-example/subscriber
go mod init github.com/ThreeDotsLabs/nats-example/publisher

Download dependencies:

go get github.com/ThreeDotsLabs/watermill
go get github.com/nats-io/go-nats-streaming  

Add an extra ULID library only for the publisher:

go get github.com/oklog/ulid

Dockerfile

The next step is creating a Docker image for containers that will run both applications. The Dockerfile is fairly simple:

FROM golang:1.11.2-stretch
RUN go get github.com/cespare/reflex
COPY reflex.conf /
ENTRYPOINT ["reflex", "-c", "/reflex.conf"]

The image is based on the official Go image. Usually, you’d want to stick with the smaller alpine version, but since it is missing some dependencies for compiling reflex, I’m using the stretch version here.

It installs and runs reflex which will be used for hot recompiling the code. It can be very useful for quickly testing your changes.

Reflex is configured by reflex.conf, which in our case is just one line:

-r '(\.go$|go\.mod)' -s go run .

What the command means is: “watch for changes to go.mod and all files ending in .go and execute go run . when it happens. The -s flag stands for service and will make reflex kill previously run command before starting it again, which is exactly what we want.

Note

Reflex is not a regular dependency. It is installed and used only in the docker image and won’t be deployed with your application. Thanks to /u/habarnam for pointing that out.

Docker Compose

We’ll use Docker Compose for running multiple services within a shared network. It allows you to spin up containers with services or databases without cluttering your local machine. Also, other developers can set up the project by cloning the repository and running one command, instead of manually installing dependencies and compiling the application.

Put the docker-compose.yml file in the repository:

version: '3'
services:
  publisher:
    build: .
    volumes:
      - ./publisher:/app
      - $GOPATH/pkg/mod/cache:/go/pkg/mod/cache
    working_dir: /app
    env_file:
      - .env
    ports:
      - 5000:5000

  subscriber:
    build: .
    volumes:
      - ./subscriber:/app
      - $GOPATH/pkg/mod/cache:/go/pkg/mod/cache
    working_dir: /app
    env_file:
      - .env

  nats:
    image: nats-streaming:0.11.2
    restart: on-failure

It defines three services. Both publisher and subscriber are based on the Dockerfile we’ve created before (this is done by the build: . option). Each application’s code is mounted in the working directory, so go run . will run the proper package. The ports part in the publisher service will bind port 5000 on your local system and map it to the port inside container.

I’ve also mounted $GOPATH/pkg/mod/cache directory, to use existing cache for dependencies, speeding up downloads.

The services are configured by environment variables, kept in a separate file:

PORT=5000
NATS_TOPIC=example
NATS_CLUSTER_ID=test-cluster
NATS_URL=nats://nats:4222

Notice the nats hostname in the NATS_URL variable - this is the name of the service defined in the docker-compose.yml file.

Running

To start the environment, run:

docker-compose up

You’ll see the output of all three services. In another terminal, try sending an example request to the publisher service with curl:

$ curl -X POST http://localhost:5000 --data "this is my message"
Sent message: this is my message with ID 01D09P02SBW5D0QPWP14QQZJWH

You should see the message delivered in the subscriber log output:

subscriber_1  | [00] 2019/01/03 11:01:27 received message: 01D09P02SBW5D0QPWP14QQZJWH, payload: this is my message

Now, try editing either of the main.go files in your editor. After you save the file, appropriate service should be recompiled and restarted.

When finished, you can kill the environment with Ctrl-C in the terminal it is running. You can also delete all containers by running docker-compose down.

What’s next?

Now that you’ve built Docker images of the services it’s the first step towards deploying it. We’ll look into it in the next post.

If you’d like to explore more docker-compose definitions, check Watermill Getting started for Kafka and Google Cloud Pub/Sub examples.

Last update:
  • January 3, 2019
comments powered by Disqus