DEV Community

Trystan Sarrade
Trystan Sarrade

Posted on

Live-Reload and Debugging Go Applications Within a Docker Container

During my journey of learning Golang, I wanted to replicate my preferred development workflow: live-reloading while debugging inside a Docker container. This approach had been a cornerstone of my work with Node.js, and I aimed to achieve the same seamless experience with Go. Surprisingly, I found limited resources online addressing the integration of live-reloading and debugging within a Docker environment.

To clarify, live-reloading refers to the automatic termination and relaunch of a process when changes are detected in the source files. This differs from hot-reloading, where parts of the memory are patched dynamically without restarting the process.

Debugging, on the other hand, is an essential tool for inspecting the execution flow of a program. While it may seem daunting to beginners, once mastered, debugging proves far more efficient and effective than scattering logs throughout your codebase.

Finally, working within a Docker container has become a modern and reliable method for developing applications. This approach ensures that your application runs consistently across virtual machines or PaaS platforms, eliminating risks tied to operating system or server configuration discrepancies. Combining Docker with live-reloading and debugging not only streamlines the development process but also sets a robust foundation for delivering reliable software.

Here’s a quick guide to help you integrate live-reloading and debugging into your development workflow for Golang within a Docker container.

Quick note: This guide is based on my setup using Windows 11 with WSL (Windows Subsystem for Linux). If you're on Linux, the steps should be identical, and for macOS users, the process is likely very similar.

If you're using Windows for Go development, I highly recommend leveraging WSL. The Windows file system is significantly slower than Linux (and WSL), which means compiling your Go programs natively on Windows can take much longer. By using a WSL host, you'll notice substantial improvements in both compilation speed and overall development efficiency.

Stack used

Here’s the stack we’ll be using to set up live-reloading and debugging for Golang in a Dockerized development environment:

Docker is the cornerstone of our setup, providing a reproducible and isolated environment for development. By leveraging Docker, we can ensure that our application behaves consistently between local development and production environments, minimizing any surprises caused by differences in OS or server configurations.

For live-reloading. We'll use Air, a lightweight Golang tool that monitors changes to your source code and automatically recompiles and restarts your server. While tools like Nodemon or Inotify-tools can achieve similar functionality, Air is specifically designed for Go, making it an ideal choice for this setup.

Since Golang doesn’t have a built-in debugger, we’ll use Delve a robust debugging tool for Go. Delve integrates seamlessly with popular IDEs like Visual Studio Code, allowing us to set breakpoints, step through code, and inspect variables directly.

Our objective is to create a development workflow where the server automatically reloads when changes are saved to the codebase. Debugging remains fully functional, enabling us to troubleshoot and inspect the application in real time (e.g., through Visual Studio Code’s built-in debugger).

Golang Server using Fiber

Here's a minimal example of a Go server using the Fiber framework, which you can use to test your live-reload and debugging configuration. Create a file named main.go and add the following code:

package main

import "github.com/gofiber/fiber/v2"

func main() {
    app := fiber.New()

    app.Get("/", func(c *fiber.Ctx) error {
        str := "Hello, World!"
        return c.SendString(str)
    })

    app.Listen(":3000")
}

Enter fullscreen mode Exit fullscreen mode

This is as simple as it gets—a lightweight server that responds with "Hello, World!" when accessed at http://localhost:3000.

Write go run . in your terminal to test that out.

Setup Docker

Here’s a basic docker-compose.yml file to start your api container. This example is minimal and can be extended to include other services like a database, Redis, or Nginx as needed:

api:
    build:
      context: ./api
      dockerfile: Dockerfile
    ports:
      - '3000:3000'
      - '2345:2345'
    stop_grace_period: 0.1s
    volumes:
      - ./api:/app
    networks:
      - internal
Enter fullscreen mode Exit fullscreen mode

build.context: used to tell docker-compose to check the dockerfile present on the folder "api". I prefer to put my sources in another folder if I need to develop multiples other go services on this repository later on.
ports: expose the webserver running on port 3000. And we will expose port 2345 for the debugger server (we will define it later)
volumes: Bind directly to the container the content of the /api folder (where all my go code is located on the Host) and put it to /app on the docker volume. This way all changes to the project will visible directly to the docker container.

Dockerfile of the '/api' folder

We saw previously that Docker-Compose is using the dockerfile inside the /api folder. Here is the content you should have for your dockerfile.

Don't overthink the delve and air part. We will see how they work just after.

# Use the official Golang image as the base image
FROM golang:1.23.2-alpine3.20

# Set the Current Working Directory inside the container
WORKDIR /app

# Install delve, used to debug the application (golang doesn't have a built-in debugger)
RUN go install github.com/go-delve/delve/cmd/dlv@latest

# Install air, used to hot reload the application
RUN go install github.com/air-verse/air@latest

# Copy go mod and sum files and install dependencies
COPY go.mod go.sum ./
RUN go mod download

# Switch to root user (if necessary, though it should work, do it only for development purposes)
USER root

# Expose the ports of the container. Docker-Compose do it automatically but it's good to have it here
EXPOSE 2345
EXPOSE 3000

# Run air for live-reload. The -c flag is used to specify the configuration file
CMD ["air", "-c", "air.toml"]
Enter fullscreen mode Exit fullscreen mode

Air and Delve setup

Air will watch the source files of your project and reload your application when changes are saved. But since we also want to use Delve as debugger, it is him (delve) that will launch our server instead of Air.

For that, we will create a air.toml file in the same place your DockerFile is placed (inside /api folder).

root = "."
tmp_dir = "tmp"

[build]
full_bin = "dlv debug --build-flags=\"-gcflags='all=-N -l'\" --listen 0.0.0.0:2345 --headless --continue --accept-multiclient --output=dist/debug"
Enter fullscreen mode Exit fullscreen mode

This configuration tells air to run a specific command when the binary is recreated (when your server reload).

dlv debug: It is the delve command that build and start your project with the debugger server attached to it.
--build-flags: Compile your golang program without optimization (make it faster to compile) but also without inline function (slower, but make your function accessible by the debugger).
--listen: Make your debugger server listen on the base host IP (0.0.0.0, useful for your docker container) and on the port 2345.
--headless --continue --accept-multiclient: start directly your golang application with the debugger without waiting a client to connect to it first.
--output: Put the debug server compiled files into dist/debug

It is all we need to make live-reloading and debugging work together !

Setup VSCode debugger

Now, last step, we need to tell VSCode how to access the debugging server.

create a new debug configuration by creating a new file under .vscode/launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Connect to container",
            "type": "go",
            "debugAdapter": "dlv-dap",
            "request": "attach",
            "mode": "remote",
            // The container port and host must match
            "port": 2345,
            "host": "localhost",
            "trace": "verbose",
            "substitutePath": [{"from": "/home/user/project/api", "to": "/app"}]
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

debugAdapter: We need to use that since the default debug adapter is not compatible with delve.
port: The port of the debugger server
host: The IP to connect to
substitutePath: This is the most useful setting, it will tell VSCode to match your host project path with the docker container volume project path. If you miss-configure this part, your breakpoints won't work since VSCode will be unable to match the line you set your breakpoint to with the docker container running application.

And that's it ! Everything you need to have Live-Reload, Debugging and Docker working together.

Billboard image

Imagine monitoring that's actually built for developers

Join Vercel, CrowdStrike, and thousands of other teams that trust Checkly to streamline monitor creation and configuration with Monitoring as Code.

Start Monitoring

Top comments (0)

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay