DEV Community

Cover image for Dockerizing Go API and Caddy
Daniel Pepuho
Daniel Pepuho

Posted on • Edited on • Originally published at danielcristho.site

Dockerizing Go API and Caddy

Hello! In this post I'll walk through how to use Caddy as a reverse proxy and Docker for containerization to deploy a simple Go API. This method offers a quick and modern to getting your Go API up and running.

Before we dive into the deployment steps, let's briefly discuss why Docker and Caddy are an excellent combination.

  • Docker is a containerization platform that packages your app and all its dependecies into an isolated unit. This guarantess that your app runs consistenly everywhere, eliminating the classic "it works on my machine" problem.

it works on my machine meme

  • Dockerile is the blueprint that defines how your Docker image is built. It specifies the base image, the steps to compile your application, and how the container should run.
  • Docker Compose is a tool for defining and running multi-container Docker applications. Instead of starting each container manually, we can describe the entire stack in a single YAML file.
  • Caddy is a modern reverse proxy and web server built using Go. Caddy is renowned for its ease of use, especially its automatic HTTPS feature. Its simple configuration makes it an ideal choice for serving our API.

Requirements

Before we start, you'll need to install Go, Docker and Docker Compose on your system.

1. Go

Make sure Go is installed. You can check your version with:

$ go version

go version go1.24.0 linux/amd6
Enter fullscreen mode Exit fullscreen mode

If it's not installed, you can download it from the official site.

2. Docker & Docker Compose

Make sure Docker & Docker Compose are installed. You can check your docker version with:

$ docker --version

Docker version 27.5.1, build 9f9e405
Enter fullscreen mode Exit fullscreen mode
$ docker compose version

Docker Compose version v2.3.3
Enter fullscreen mode Exit fullscreen mode

Follow the official guides to install Docker and Docker Compose for your operating system if you haven't installed them before. The official site.

Setting Up the Project

Step 1: Create and Run a Simple Go API

  • First, create a new directory for the project and initialize a Go module:
$ mkdir go-api && cd go-api
Enter fullscreen mode Exit fullscreen mode
$ go mod init example.com/go-api
Enter fullscreen mode Exit fullscreen mode

This command creates a Go module (go.mod) named example.com/go-api, which helps manage dependencies and makes the project reproducible.

  • Next, create a new file main.go and define a simple HTTP server using Chi as the router.

The server exposes two routes:

  • Root path / -> return an ASCII banner generated with the go-figure package.
func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    asciiArt := figure.NewFigure("Go API - Caddy", "", true).String()
    fmt.Fprintln(w, asciiArt)
}
Enter fullscreen mode Exit fullscreen mode
  • /api/hello -> returns a simple JSON response.
func apiHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"message":"Hello, World!"}`)
}
Enter fullscreen mode Exit fullscreen mode
  • In the main function, we:
  1. Initialize the Chi router and register both routes.
  2. Configure an HTTP server to listen on port 8081.
  3. Run the server in a goroutine and listen for shutdown signals (SIGINT, SIGTERM).
  4. Gracefully shut down the server with a 5-second timeout when a termination signal is received.
func main() {

    r := chi.NewRouter()

    // API Route
    r.Get("/", handler)
    r.Get("/api/hello", apiHandler)

    srv := &http.Server{
        Addr:    ":8081",
        Handler: r,
    }

    stop := make(chan os.Signal, 1)
    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        fmt.Println("Server started at :8081")
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            fmt.Printf("Could not listen on port 8081: %v\n", err)
        }
    }()

    <-stop

    fmt.Println("Shutting down server...")

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        fmt.Printf("Server shutdown failed: %v\n", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Here is the full code:

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/common-nighthawk/go-figure"
    "github.com/go-chi/chi/v5"
)

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    asciiArt := figure.NewFigure("Go API - Caddy", "", true).String()
    fmt.Fprintln(w, asciiArt)
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"message":"Hello, World!"}`)
}

func main() {

    r := chi.NewRouter()

    // API Route
    r.Get("/", handler)
    r.Get("/api/hello", apiHandler)

    srv := &http.Server{
        Addr:    ":8081",
        Handler: r,
    }

    stop := make(chan os.Signal, 1)
    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        fmt.Println("Server started at :8081")
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            fmt.Printf("Could not listen on port 8081: %v\n", err)
        }
    }()

    <-stop

    fmt.Println("Shutting down server...")

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        fmt.Printf("Server shutdown failed: %v\n", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Then, try run the main.go using go run:

$ go run main.go
Enter fullscreen mode Exit fullscreen mode

After that, you should see this message in the terminal:

Server started at :8081
Enter fullscreen mode Exit fullscreen mode

Finally, you can test the API using curl or access from the browser on http://127.0.0.1:8081:

$ curl 127.0.0.1:8081

   ____                _      ____    ___                ____               _       _
  / ___|   ___        / \    |  _ \  |_ _|              / ___|   __ _    __| |   __| |  _   _
 | |  _   / _ \      / _ \   | |_) |  | |     _____    | |      / _` |  / _` |  / _` | | | | |
 | |_| | | (_) |    / ___ \  |  __/   | |    |_____|   | |___  | (_| | | (_| | | (_| | | |_| |
  \____|  \___/    /_/   \_\ |_|     |___|              \____|  \__,_|  \__,_|  \__,_|  \__, |
                                                                                        |___/
Enter fullscreen mode Exit fullscreen mode
$ curl 127.0.0.1:8081/api/hello

{"message":"Hello, World!"}
Enter fullscreen mode Exit fullscreen mode

Step 2: The Dockerfile

We'll use a multi-stage build to create a minimal and secure Docker image. This process compiles our Go application in one stage and then copies only the final binary to a much smaller final image. This keeps our final image size low.

Create a Dockerfile in your project directory and add the following code:

# First stage: builder
FROM golang:1.24 AS builder

WORKDIR /go/src/app

COPY . .

RUN go mod download

RUN CGO_ENABLED=0 go build -o /go/bin/app
Enter fullscreen mode Exit fullscreen mode
  • In the builder stage, we use the official Go image to compile our code.
  • We copy the entire project into the container and run go mod download to fetch dependencies.
  • Then we build the binary with CGO_ENABLED=0 to ensure it’s statically compiled and portable. The binary is placed in /go/bin/app.
# Second stage: final image
FROM golang:1.24-alpine

# Copy only the compiled binary from builder stage
COPY --from=builder /go/bin/app /

EXPOSE 8081

ENV PORT 8081

CMD ["/app"]
Enter fullscreen mode Exit fullscreen mode
  • In the final stage, only the compiled binary is copied over from the builder stage. This keeps the image small because source code, dependencies, and build tools are excluded.
  • We expose port 8081 so Docker knows which port the app listens on.
  • CMD ["/app"] runs the binary as the container’s main process.

Here is the full code:

FROM golang:1.24 AS builder

WORKDIR /go/src/app

COPY . .

RUN go mod download

RUN CGO_ENABLED=0 go build -o /go/bin/app

FROM golang:1.24-alpine

COPY --from=builder /go/bin/app /

EXPOSE 8081

ENV PORT 8081

CMD ["/app"]
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure Caddy

Caddy will act as a reverse proxy that forwards incoming requests to the Go API container. This allows us to:

  • Tells Caddy to listen on port 80 (HTTP).
  • Forwards requests to the Go API container, using the Docker service name go-api and port 8081.

Create a file named Caddyfile in your project directory:

:80 {
    reverse_proxy go-api:8081
}
Enter fullscreen mode Exit fullscreen mode

💡 If you later have a domain (e.g., api.example.com), you can replace :80 with your domain

api.example.com {
    reverse_proxy go-api:8081
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Configure Docker Compose

Create a file named docker-compose.yml in your project directory:

version: "3.8"

services:
  go-api:
    build: .
    container_name: go-api
    expose:
      - "8081"                 # Expose port internally (only visible to other services in this network)
    restart: unless-stopped

  caddy:
    image: caddy:2.10-alpine
    container_name: caddy
    ports:
      - "8082:80"              # Map host port 8082 -> container port 80
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
    restart: unless-stopped
    depends_on: 
      - go-api
Enter fullscreen mode Exit fullscreen mode

Explanation

go-api service

  • Built from your local Dockerfile.
  • Uses expose instead of ports → this means the Go API is reachable inside the Docker network but not directly exposed to the host machine.
  • The Go API will listen on :8081, but only Caddy can access it.

caddy service

  • Uses the official lightweight caddy:2.10-alpine image.
  • ports: "8082:80" -> exposes port 80 from the container to port 8082 on your host machine. So when you open http://127.0.0.1:8082, requests are routed through Caddy.
  • The Caddyfile is mounted so you can configure reverse proxy behavior.
  • depends_on ensures Caddy waits for the Go API container to start.

Step 5: Test the Setup

Once all files are ready (main.go, Dockerfile, Caddyfile, docker-compose.yml), run the stack using the docker compose up command:

.
├── Caddyfile
├── docker-compose.yml
├── Dockerfile
├── go.mod
├── go.sum
└── main.go
Enter fullscreen mode Exit fullscreen mode
$ docker compose up -d --build
Enter fullscreen mode Exit fullscreen mode

Make sure the containers are running using docker ps command:

$ docker ps

CONTAINER ID   IMAGE                          COMMAND                  CREATED         STATUS         PORTS
                                                      NAMES
6b6b35487d77   caddy:2.10-alpine              "caddy run --config …"   9 minutes ago   Up 9 minutes   443/tcp, 2019/tcp, 443/udp, 0.0.0.0:8082->80/tcp, [::]:8082->80/tcp   caddy
20cbb2209fe7   docker-go-api-caddy_go-api     "/app"                   9 minutes ago   Up 9 minutes   0.0.0.0:32770->8081/tcp, [::]:32770->8081/tcp
Enter fullscreen mode Exit fullscreen mode

Now, test the endpoints through Caddy (reverse proxy):

$ curl http://127.0.0.1:8082
Enter fullscreen mode Exit fullscreen mode

Expected output: the ASCII art rendered by go-figure.

$ curl http://127.0.0.1:8082/api/hello
Enter fullscreen mode Exit fullscreen mode

Expected output:

{"message":"Hello, World!"}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this guide, we successfully deployed a simple Go API containerized approach. By leveraging Docker, we created an isolated and reproducible environment for our application. We used a multi-stage build in our Dockerfile to keep the final image lightweight and secure, and Docker Compose to easily manage both our Go API and Caddy services with a single command.

Feel free to explore further by adding more features to your API or by setting up a domain with Caddy’s automatic HTTPS. Happy deployment!🚀

References:

Top comments (0)