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)