Tag: Go

  • Adventures in Deploying Golang and Docker

    I have some functionality on my app up and running. Wanted to start the process of figuring out the deployment process. The app consist of a Golang executable running Gin and SQLite. I want to be able to deploy the application using a Docker/OCI image so that I have maximum flexibility of where I can deploy.

    Building the initial image

    The first thing I had to do was create the initial Docker file and build the initial image. I used the Golang image based on apline. Here is what the Docker file looks like:

    # Latest golang image on apline linux
    FROM golang:1.24.1-bookworm
    
    # Work directory
    WORKDIR /build
    
    # Installing dependencies
    COPY go.mod go.sum ./
    
    # Installs Go dependencies
    
    RUN go mod download
    
    # Copying all the files
    COPY . .
    
    # Starting our application
    CMD ["go", "run", "main.go"]
    
    # Exposing server port
    EXPOSE 8080

    I built the initial image using this command:

    docker build -f Dockerfile -t mygoapp:latest .

    Built the initial image and was able to run the application. There was a problem, the image was almost 2GB in size. The Go code wasn’t substantial at this point so most of the 2GB overhead was the image itself.

    Reducing the image size

    To reduce the size, I decided to do a multi-stage build and compile the application statically. In order to do this I had to compile the Go code using the previous image (golang:1.24.1-bookworm) and copy the executable to a small base image, in this case scratch. Here is what the Dockerfile looks like

    # Use Go 1.24 bookworm as base image
    FROM golang:1.24.1-bookworm AS builder
    
    # Creates an app directory to hold your app’s source code
    WORKDIR /build
     
    # Copies everything from your root directory into /app
    COPY go.mod go.sum ./
     
    # Installs Go dependencies
    RUN go mod download
    
    # Copy the entire source code into the container
    COPY . .
     
    # Builds your app with optional configuration
    RUN CGO_ENABLED=0 GOOS=linux go build -o /app 
     
    # Tells Docker which network port your container listens on
    
    FROM scratch
    
    # Copy the executable to the scratch image
    COPY --from=builder /app /app
    
    # Expose the port for Gin
    EXPOSE 8080
    
    ENTRYPOINT ["/app"]

    I built the image using this command:

    docker build -f Dockerfile -t mygoapp:latest .

    I ran the Docker image and got this error

    [error] failed to initialize database, got error Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work.

    Golang and SQLite Present an Interesting problem

    It seems that in order to use SQLite and Golang together, you have to have CGO_ENABLED set to 1 use additional linker options to create a completely static executable. Here are the options I used

    RUN CGO_ENABLED=1 GOOS=linux go build -o /app -a -ldflags '-linkmode external -extldflags "-static"' .

    I rebuilt the image and reran it. The image started without an issue. The image size was around 70 megabytes. I ran some initial tests against the endpoints exposed and ran into this error:

    x509: certificate signed by unknown authority

    The service hits a 3rd party API and scratch doesn’t have any ssl certs installed. I could download the certificates manually but I’m lazy and don’t want to complicate the build process and dockerfile.

    Google Distroless to the Rescue

    Google actually provides minimal Docker images that are a little larger than scratch but contain things like libc, libssl, ca-certificates, etc. I’m going to use the base image (gcr.io/distroless/base). Here is what my new Dockerfile looks like:

    # Use Go 1.24 bookworm as base image
    FROM golang:1.24.1-bookworm AS builder
    
    # Creates an app directory to hold your app’s source code
    WORKDIR /build
     
    # Copies everything from your root directory into /app
    COPY go.mod go.sum ./
     
    # Installs Go dependencies
    RUN go mod download
    
    # Copy the entire source code into the container
    COPY . .
     
    # Builds your app with optional configuration
    RUN CGO_ENABLED=1 GOOS=linux go build -o /app -a -ldflags '-linkmode external -extldflags "-static"' .
     
    # Tells Docker which network port your container listens on
    
    FROM gcr.io/distroless/base
    COPY --from=builder /app /app
    COPY --from=builder /build/templates/* ./templates/
    EXPOSE 8080
    
    ENTRYPOINT ["/app"]

    Everything worked locally and the Docker image was about 100 megabytes. In order to deploy it to a cloud provider I chose to do a multi-platform image that will run on my M4 Mac (arm64) and in the cloud on non-arm hardware (amd64). Here is the command I used to build the image:

    docker buildx build --platform linux/amd64,linux/arm64 . -t mygoapp:latest

    Questions or comments feel free to reach out to me on BlueSky (https://bsky.app/profile/rooseveltanderson.bsky.social)