Avoiding headaches with Go, Docker and fly.io

6 Dec, 2023 | Tech

I'm putting this here mainly to remind myself in future, but also in case it pops up in a search conducted by a soul as perplexed as I was before it finally clicked.

I'm new to Go and I took my usual approach of jumping in the deep end rather than starting out simple. Although possibly more so this time (I was on a timeline).

I got my app working locally, using a single Go module and Sqlite as my database, the app I was building didn't need much.

I figured Go would offer an easy way to deliver this -- a single file and a single locally hosted database, what could be simpler!

It would also give me a chance to try fly.io as an option for hosting.The only snag was that because I wanted to use the sqlite database, I had to deploy using a Dockerfile.

I've run Docker containers before, very occasionally, usually created by someone else. I vaguely remember writing a Dockerfile at some point too, but this was a different kettle of fish.

This video and this video got me more comfortable with the basics pertaining to Dockerfiles and Go. As did the Docker docs on the subject.

And that's where I ran into my first issue: go.mod and go.sum.

I needed those as I was using a third-party package for the Sqlite driver (why is that not a standard one?) and they provided a way to download the package in the image. Easy enough to generate those (it turns out):

go mod init <module-name>
go mod tidy

Job done, right? Well I assumed it was the third-party package I needed to put in the command. So I ended up with my script stating "import cycle not allowed" and I couldn't find anything to say why.

The answer is you need to use the name of your module in the init command (or a name of some sort, I used the name of the folder my main.go file was in). Bingo, now I had correct files and it generated a sum, which it had previously failed to do. That took me a long time to figure out.

One problem down.

Next was that my main.go file built locally, but wouldn't build properly when done through the flyctl tool.

More digging highlighted that while my local setup had CGO_ENABLED set to 1, the fly environment defaulted to 0, so I added an ENV line that fixed that.

It then also needed gcc to run the build (both of these were because of the third-party package). So I needed to add:

RUN apk add --no-cache --update go gcc g++

Now we were cooking, only my app still didn't work.

I was using a multi-stage build process to give me as small an image as possible and that meant my original COPY command needed to be repeated to copy over not just the Go file but the folders containing template files and the database.

With that done it was on to the fly.io setup.

Surprisingly, I seemed to configure my fly.toml file correctly first time -- luck rather than anything else. And some good docs from them to be fair.

The final Dockerfile that got me working was:

# Build stage
FROM golang:1.21.4-alpine3.17 AS build-stage
ENV GO111MODULE=auto
ENV CGO_ENABLED=1
ENV GOOS=linux
RUN apk add --no-cache --update go gcc g++
WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN go build -o main main.go

VOLUME [ "/data" ]

# Run stage
FROM alpine:3.17
WORKDIR /app
COPY --from=build-stage /app/templates ./templates/
COPY --from=build-stage /app/data ./data/
COPY --from=build-stage /app/main .

EXPOSE 80

CMD ["/app/main"]