https://awstip.com/containerize-go-sqlite-with-docker-6d7fbecd14f0
TL;DR — 🚀 The story starts with notes about why this is an extraordinary condition. Two dockerfiles are provided, skip to the last one for the better solution. A demo repository is provided at the end of the story.
Notes
Containerizing a backend application with a database together is an extraordinary case. One of the important best practices on containerization is packing a single app per container. (see sidecar pattern)
Apps can have different lifecycles, different states. Multiple apps in single container scenarios may require developing custom health check functionalities. At this point, Docker or Kubernetes can’t understand if your container is actually healthy. Also, this is not scalable. Data is another level, requires a stateful approach. (read more).
Having a single app per container will preserve functionalities such as scalability, health checks, self-healing &, etc.
Feel free to comment below for the parts you agree or disagree with.
Scenario
In my case, I’m building a mock server called Mocktail which will be compatible with multiple databases. PostgreSQL & MongoDB are commonly used databases. Besides external databases, I also wanted to provide a plug-and-play option with SQLite.
Good Solution ☑️
The following multi-stage Dockerfile is similar to the usual way of containerizing Go applications. C libraries are required to interact with SQLite, therefore CGO must be enabled. Previously, I was using an alpine image instead of debian:buster-slim. Running with CGO enabled flag developed new issues. Once the app was built, required many directories which were missing in the Alpine image. Instead of detecting missing packages, I went with the debian:buster-slim solution. The compressed image size is around 35MB. The downside here is, we have unused folders/libraries in the debian image. Which means the image could be smaller.
FROM golang:1.17 AS builder
WORKDIR /src
COPY . .
RUN go mod download
RUN go build -o /app .
FROM debian:buster-slim
COPY --from=builder /app /app
EXPOSE 4000
ENTRYPOINT ["/app"]
Better Solution ✅
While writing this story, I’ve found another blog called 7thzero, they’ve bumped into the same problem. Turns out the build command required additional -ldflags. I updated my build command. The advantage here is, now I can build from scratch (completely empty image). The compressed image size is around 9MB. 😏
FROM golang:1.17 AS builder
WORKDIR /src
COPY . .
RUN go mod download
RUN CGO_ENABLED=1 GOOS=linux go build -o /app -a -ldflags '-linkmode external -extldflags "-static"' .
FROM scratch
COPY --from=builder /app /app
EXPOSE 4000
ENTRYPOINT ["/app"]
Why does image size matter?
Small images have many advantages.
- Reduced time-to-build and time-to-pull.
- Small containers usually have a smaller attack surface as compared to containers that use large base images.
- Scaling/Recovery — Assume one of the nodes of a multi-node cluster is down and Kubernetes spins up a new node. Small image size directly affects the deployment duration. (time-to-pull)
- Reduced time-to-build provides higher performance especially for small machines, shared systems/cicd systems. (read more)
Thank you for reading.
Try Mocktail while developing your next milllion dollar idea.
Cheers! 🎉