GCP

All posts tagged GCP

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)

I’m working on a new project. Can’t really disclose a lot but I can tell you its going to be based on Golang/REACT and I’m hoping to scale it to handle at least 100k hits per day. Because I’m a cheap bastard, I’m going to try to spend as little money as possible. I plan to document the entire process here.

Technologies Used

  • Golang
  • Docker
  • SQLite
  • React

3rd Party Services Used

  • Oracle Cloud Infrastructure/Google Cloud
    • Hosting the Golang backend
  • OCI NoSQL database/Firebase
    • Hosting user generated data
  • SupaBase
    • User authenitcation
  • PostHog
    • Analytics
  • Vercel
    • Hosting the React Frontend

Why SQLITE?

The project that I’m working on relies on a lot of static data. By storing this data in an SQLite, I’m decreasing the need to make additional network calls to a remote database. The initial data would consist of about 50,000 rows in the SQLite database which would take up about 5-6 megabytes. Each database would be deployed in a docker container with the Golang application.

SQLite can easily handle 100k http requests per day (You can read more about uses for SQLite here). I’m enabling WAL mode (Write-Ahead Logging) to make the database more performant since the database would only be accessed via a single host.

Feel free to comment or reach out to me on Bluesky (https://bsky.app/profile/rooseveltanderson.bsky.social)