CI Pipeline for FFlags.com
This is a write-up of the initial CI pipeline I set up for FFlags.com. With fflags.com I wanted to keep things clean and simple as much as possible. The pipeline is built using Github Actions, GHCR, and Railway.
The Stack
- Golang Web Server
- Postgres Database
- React Frontend
The React frontend is built and embedded into the Golang binary using go:embed.
The Pipeline
The pipeline is triggered on a new release which builds the docker image, pushes it to GHCR. The workflow file looks something like this:
# release.yml
name: Release Workflow
on:
release:
types: [created]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20"
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Build dashboard app
working-directory: ./dashboard
run: |
pnpm install
pnpm run build
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.23"
- name: Build Go binary
run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags '-extldflags "-static"' -o bin/fflags cmd/web-server/main.go
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
The pipeline builds the React frontend, the Golang binary, and pushes the docker image to GHCR. The Dockerfile
for the Golang server looks like this:
# Dockerfile
FROM alpine:latest
WORKDIR /app
COPY bin/fflags /app/
EXPOSE 8081
CMD ["/app/fflags"]
Mistakes and Learnings
Initially, I had the following build command for the Go executable:
go build -o bin/fflags cmd/web-server/main.go
This command worked fine on my local machine and on the CI. But, the binary was not executing on the alpine image and throwing the following error:
exec /app/fflags: no such file or directory
The problem here was I was building the binary on a Debian-based OS and trying to run it on an Alpine-based image. The solution was to build the binary with the following command:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags '-extldflags "-static"' -o bin/fflags cmd/web-server/main.go
Here, CGO_ENABLED=0
disables the use of cgo which is required to build a static binary. GOOS=linux
and GOARCH=amd64
specify the target OS and architecture respectively.
and -ldflags '-extldflags "-static"'
is used to link the binary statically.
Deploying to Railway
After the docker image is pushed to GHCR, I manually deploy the image to Railway. I am planning to automate this step as well in the future when the application becomes a little stable.