Stay Ahead in Ruby!
From development processes to the most useful gems, get it all straight in your inbox. Join our Ruby blog today!

Skip to main content

Buffalo development with Docker

Buffalo development with Docker - cover image

What is Buffalo? #

Buffalo is a Rails-inspired framework for web development in Go programming language. It provides many different tools and generators that help you start working on your business logic ASAP, rather than searching for a suitable package to set up a web server eventually. Buffalo is called to make the cold world of Go development, where you’ll hear “just use the standard library” every time, more friendly for fresh “gophers”, who want to dive into the world of web development with Go.

At the same time, Buffalo rather should be considered as a good base for a side project. It contains a lot of elements that experienced developers could find redundant due to the common approach to Go programming that provides lightness and simplicity.

Why do you need this guide? #

Even though Buffalo eliminates most of the headaches caused by the development environment setup, developing an app running in a Docker container can be tricky for freshly arrived Go developers. In addition, so far the community is relatively small, and it could be difficult to find a well-grounded tutorial for basic things.

In this article, we will discuss how to:

  • Dockerize Buffalo project
  • debug Buffalo project running in Docker container using Delve (both CLI and UI)

Prerequisite #

Assume you successfully generated a Buffalo project using the command:

buffalo new <project-name>

At this moment, you already can run your project in development mode using the command:

buffalo dev

However, at the same time, you won’t be able to see your home page so far. The error that appears tells us, that we don’t have a database yet. Of course, we could create it immediately using Buffalo CLI, but since we are pretty self-confident, we’ll create it directly in the container after Dockerization!

By default, Buffalo uses the Postgres database, so let’s use an out-of-the-box config in our demo project.

Dockerization #

Setting up Dockerfile #

Any dockerization process starts with creating your project’s image using Dockerfile. Buffalo has Dockerfile out of the box; this is the first thing that can confuse developers who work with Buffalo for the first time and make dockerization process more complicated. The main mistake is that Dockerfile that goes out of the box IS NOT for development. It was created to simplify deployment, and it follows the idea of running your compiled project’s binary file in the crystal-clear Alpine image (without Buffalo or Go installed).

Let’s dive deeper into the default Dockerfile - it is a key to understanding how we can transform it for our needs. So by default you’ll have:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# This is a multi-stage Dockerfile and requires >= Docker 17.05
# https://docs.docker.com/engine/userguide/eng-image/multistage-build/
FROM gobuffalo/buffalo:v0.18.14 as builder

ENV GOPROXY http://proxy.golang.org

RUN mkdir -p /src/buffalo_demo
WORKDIR /src/buffalo_demo

# this will cache the npm install step, unless package.json changes
ADD package.json .
ADD yarn.lock .yarnrc.yml ./
RUN mkdir .yarn
COPY .yarn .yarn
RUN yarn install
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download

ADD . .
RUN buffalo build --static -o /bin/app

FROM alpine
RUN apk add --no-cache bash
RUN apk add --no-cache ca-certificates

WORKDIR /bin/

COPY --from=builder /bin/app .

# Uncomment to run the binary in "production" mode:
# ENV GO_ENV=production

# Bind the app to 0.0.0.0 so it can be seen from outside the container
ENV ADDR=0.0.0.0

EXPOSE 3000

# Uncomment to run the migrations before running the binary:
# CMD /bin/app migrate; /bin/app
CMD exec /bin/app

Consider the sequence of actions briefly:

  1. Using Buffalo image as a base image (line 3)
  2. Creating workdir for your project (lines 7 - 8)
  3. Adding and installing libs for the front-end part of the project (it will be skipped if you generated the project as an API only) (lines 11 - 15)
  4. Copying go.mod and go.sum files to the workdir (lines 17-18)
  5. Installing Go dependencies (line 21)
  6. Adding rest of the files (line 23)
  7. Building buffalo project (line 24)
  8. Creating additional clean alpine image (lines 26 - 32)
  9. Running project binary inside newly created alpine image (line 44)

After such analysis, you’ll possibly already have an imagination of what steps should be performed to use Dockerfile for the development process:

  1. We need to remove the additional alpine image creation.
  2. We don’t need to copy the source code inside and pack all the dependencies inside the Docker image for the development environment. We’ll use volumes to mount the directory on our host machine into the container later in docker-compose.

Here is what updated Dockerfile can look like:

# This is a multi-stage Dockerfile and requires >= Docker 17.05
# https://docs.docker.com/engine/userguide/eng-image/multistage-build/
FROM gobuffalo/buffalo:v0.18.14 as builder

ENV GOPROXY http://proxy.golang.org

RUN mkdir -p /src/buffalo_demo
WORKDIR /src/buffalo_demo

# this will install the latest nodejs version
RUN apt-get update && \
    apt-get -y install curl gnupg && \
    curl -sL https://deb.nodesource.com/setup_20.x | bash - && \
    apt-get -y install nodejs

ENV ADDR=0.0.0.0

EXPOSE 3000

There are some issues with Buffalo Docker image:

  • As you can notice, in the updated Dcokerfile we installed a newer node version. That’s because the Buffalo image uses deprecated node v12.x and you won’t be able to build your image without node update.
  • The Buffalo image uses Go version 1.18. It means that if you use a newer one in your go.mod file - you should change it to 1.18. You can eliminate these issues by writing your own Buffalo image. You can use the source code of the existing image as a reference.

As you can see, we haven’t specified a command to run the code inside the container in the Dockerfile. We’ll do it in the docker-compose file later. Nevertheless, at this moment, you can build your Docker image and run the project inside the container.

To build your Docker image run

docker build -t buffalo-demo --no-cache .

To run the container and enter its bash

docker run -it --name buffalo-demo-1 -p 3000:3000 -v $(pwd):/src/buffalo_demo buffalo-demo /bin/bash

Inside container you can run your Buffalo project

go run ./cmd/app

However, at this stage, we still don’t have a database, so the home page still be unavailable. Before creating DB, you should take into account, that while the project is running in the Docker container, this container - is a localhost for the project. Meaning, that even though you have Postgres installed on your machine, your app can’t access it. To use Postgres from the app’s container, you should run it in a separate container too. It can be accomplished using docker-compose file.

Setting up docker-compose #

Create a docker-compose.yml file in your project’s root directory.

version: "3.8"
services:
  db:
    image: postgres:latest
    ports:
      - 5432:5432
    volumes:
      - db:/var/lib/postgresql/data
  app:
    build: .
    ports:
      - 3000:3000
    depends_on:
      - db
    volumes:
      - .:/src/buffalo_demo
      - go-modules:/go/pkg/mod
    command: go run ./cmd/app

volumes:
  db:
  go-modules:

Here we created two services:

  • db for our database container where Postgres will run
  • app for our Buffalo project

Let’s dive deeper into the app service we configured:

  • the root . directory on your host machine is mounted into the container at /src/buffalo_demo
  • the go-modules volume is used to cache the Go modules. It’s mounted at global cache directory /go/pkg/mod
  • run the project by the simple command go run ./cmd/app

The last thing left to do before we can run our containers is to change the host in the project’s database.yml file to db.

development:
  dialect: postgres
  database: buffalo_demo_development
  user: postgres
  password: postgres
  host: db
  pool: 5

Running project #

Now, you can simply run docker compose up to run the project. When the project is up and running, you can proceed to the app container’s bash:

docker exec -it buffalo_demo-app-1 sh

Here, you can create the database using command:

buffalo db create

Now, if you try to access the home page, you’ll see it works!

home page

Debugging with Delve in Docker #

Another crucial thing for efficient development is debugging. Fortunately, there is a very powerful debugger built for Go debugging from scratch - Delve. At the same time, the initial Delve setup can be tricky, especially due to the variety of ways to start the go project. Working with Delve inside a Docker container makes it even more complicated. Let’s try to configure Delve and create a configuration to run the project in debug mode.

Delve setup #

First of all, we need to add dlv executable to our app’s container. Put the following line after downloading dependencies in the Dockerfile to install Delve:

RUN go install github.com/go-delve/delve/cmd/dlv@latest

Now, if you try to build and run the project, you will have the opportunity to run dlv CLI command inside the app’s container. But, all of these commands are useless so far =)

According to documentation, while using Delve with Docker, we need to start a headless instance of Delve inside the container

dlv exec --headless --continue --listen :4040 --accept-multiclient /path/to/executable

and then connect to it

dlv connect :4040

As you can see, we need to use the precompiled binary of the project to run it in debug mode. I suggest creating a separate docker-compose.debug.yml file where we’ll override commands to run the project in debug mode and add port 4040.

# docker-compose.debug.yml

version: "3.8"
services:
  app:
    command: >
      bash -c 'go build -gcflags="all=-N -l" -o /bin/app ./cmd/app
      && dlv exec --headless --continue --listen :4040 --accept-multiclient --check-go-version=false /bin/app'      
    ports:
      - 3000:3000
      - 4040:4040
    environment:
      - GODEBUG=netdns=go

Note, that we override the command to run the container and set two commands to do it:

  • go build -gcflags="all=-N -l" -o /bin/app ./cmd/app to compile the project. Here -gcflags="all=-N -l" flag is used to build the binary without optimization
  • dlv exec --headless --continue --listen :4040 --accept-multiclient --check-go-version=false /bin/app to execute compiled binary in debug mode

Pay attention to two important configuration details:

  1. Since I use Go version 1.18, I had to set the --check-go-version=false flag for dlv exec command. That’s because my Delve version is newer.
  2. We need to set the environment variable GODEBUG=netdns=go. This force Go runtime to use pure go DNS resolver instead of using operating system’s API to resolve DNS. Learn more.

Now, you can run the project in debug mode:

docker compose -f docker-compose.yml -f docker-compose.debug.yml up

Using CLI #

While your project is running in debug mode, connect to the app’s container:

docker exec -it buffalo_demo-app-1 sh

Then, you need to connect to the debug server:

dlv connect :4040

After the connection, you’ll be able to use dlv CLI for debugging. Let’s set the breakpoint on the 12th line of the default HomeHandler:

break /src/buffalo_demo/actions/home.go:12

When the breakpoint is set, try to load the home page again. Execution will be stopped on the breakpoint we set. To see it, run dlv CLI continue command.

debugging

Learn more about dlv CLI commands in the official documentation.

Using VS Code UI #

If you use VS Code with the Go extension for development, you have the option to debug the project using VS Code UI. To do it, we need to configure the default Run and Debug functionality. Open Run and Debug window and press create a launch.json file.

Run and Debug window

VS Code will guide you through the main configuration settings. In particular, it will ask you about the language to debug and debugging approach. Choose Go -> Go: Connect to server. Such configuration will force VS Code to connect to the headless debug server, as we’ve done through CLI.

Here is my launch.json file.

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Connect to server",
            "type": "go",
            "request": "attach",
            "mode": "remote",
            "port": 4040,
            "host": "127.0.0.1"
        }
    ]
}

When your configuration is ready:

  1. Run the project in debug mode
  2. Click Connect to server button in VS Code
  3. Set a breakpoint using UI
Debugging using UI
  1. Try to access the home page again

After performing these steps, you’ll be able to debug using VS Code UI.

Debugging using UI

Summary #

If you are new to Go programming all these exotic configuration details can look pretty complicated, especially if you come from the world of dynamically-typed languages. On the other hand, an additional layer of complexity during setup is a common thing for statically-typed programming languages and usually, it is a reasonable price for lightning fast execution of precompiled binaries.

Talking about the Buffalo framework, even though it doesn’t have a big portfolio of real-world projects, it is still a good choice to dive deeper into Go development. It is a great playground where you can get familiar with many popular packages, and find out how we can use Go with databases, task runners, and more.

We are ready to provide expert's help with your product
or build a new one from scratch for you!

Contact MobiDev’s tech experts!