├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── docker-test.yml │ └── test.yml ├── .gitignore ├── Dockerfile ├── Makefile ├── Makefile_azure ├── README.md ├── src ├── main.go └── utils.go └── website └── index.html /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | bin 3 | # Add anymore temporary directories here 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | open-pull-requests-limit: 1 13 | - package-ecosystem: "gomod" # See documentation for possible values 14 | directory: "/" # Location of package manifests 15 | schedule: 16 | interval: "weekly" 17 | open-pull-requests-limit: 1 18 | - package-ecosystem: "npm" 19 | directory: "/packages/chatbot-frontend" 20 | schedule: 21 | interval: "weekly" 22 | open-pull-requests-limit: 1 23 | -------------------------------------------------------------------------------- /.github/workflows/docker-test.yml: -------------------------------------------------------------------------------- 1 | name: Build docker image 2 | on: push 3 | 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.ref }} 6 | cancel-in-progress: true 7 | 8 | jobs: 9 | docker_build: 10 | strategy: 11 | matrix: 12 | go-version: [1.19.x, 1.20.x] 13 | platform: [ubuntu-latest] # You can test on macos-latest and windows-latest as well 14 | runs-on: ${{ matrix.platform }} 15 | steps: 16 | - uses: actions/checkout@v6 17 | - name: Install Go 18 | uses: actions/setup-go@v6 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | - name: Init repo 22 | run: make init NAME=github.com/ashishb/golang-template-repo 23 | - name: Fetch modules 24 | run: touch go.sum && go mod tidy 25 | - name: Docker build 26 | run: make docker_build 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Source: https://github.com/mvdan/github-actions-golang 2 | on: [push, pull_request] 3 | name: Test 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: [1.19.x, 1.20.x] 14 | platform: [ubuntu-latest, macos-latest] # You can test on windows-latest as well 15 | runs-on: ${{ matrix.platform }} 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v6 19 | - name: Install Go 20 | uses: actions/setup-go@v6 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | - name: Init 24 | run: make init NAME=github.com/ashishb/golang-template-repo 25 | - name: Fetch modules 26 | run: touch go.sum && go mod tidy 27 | - name: Build 28 | run: make build_debug 29 | - name: Build Linux 30 | run: make build_linux 31 | # - name: Lint 32 | # run: go get -u golang.org/x/lint/golint && make lint 33 | - name: Verify no formatting issues 34 | # Source: https://github.com/golang/go/issues/24230 35 | run: test -z $(go fmt ./src/...) 36 | - name: Test 37 | run: make test 38 | 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/18e28746b0862059dbee8694fd366a679cb812fb/Go.gitignore 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | bin/ 16 | 17 | # A secret like a credential which we don't want to reveal 18 | secret.txt 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # To run this image locally 2 | # make docker_run 3 | # You can connect to the running container using 4 | # docker exec -t -i bot /bin/sh 5 | 6 | FROM alpine:3.16 as base 7 | 8 | FROM golang:1.19.2-alpine3.16 as builder 9 | 10 | WORKDIR /codebase 11 | 12 | # Full package list is here https://pkgs.alpinelinux.org/packages 13 | # build-base = To install make 14 | RUN apk add --no-cache build-base 15 | COPY Makefile go.mod go.sum /codebase/ 16 | COPY src /codebase/src 17 | RUN make build_prod 18 | 19 | FROM base 20 | WORKDIR / 21 | ARG BINARY_NAME 22 | ENV BINARY_PATH="/bin/${BINARY_NAME}" 23 | COPY --from=builder /codebase/bin/* ${BINARY_PATH} 24 | COPY website /website 25 | RUN ls -l ${BINARY_PATH} 26 | # Optional: Copy more stuff into final image here 27 | 28 | CMD ["sh", "-c", "${BINARY_PATH}"] 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY_NAME = "binary_name" 2 | # TODO: Edit these field names based on your Google cloud setup 3 | GOOGLE_CLOUD_PROJECT_ID = "gcloud_project_id" 4 | GOOGLE_CLOUD_RUN_SERVICE_NAME = "gcloud_service_name" 5 | REGION="us-east4" 6 | 7 | # We base64 encode it to remove encode all special characters including whitespace 8 | SECRET_VALUE=`cat secret.txt | base64` 9 | 10 | DOCKER_TAG := "${REGION}-docker.pkg.dev/${GOOGLE_CLOUD_PROJECT_ID}/${GOOGLE_CLOUD_RUN_SERVICE_NAME}/${GOOGLE_CLOUD_RUN_SERVICE_NAME}:main" 11 | 12 | # One-time usage 13 | # Example: `make init NAME=calendarbot` 14 | init: 15 | GO111MODULE=on go mod init ${NAME} 16 | # A random secret value like a credential 17 | echo "random value" > secret.txt 18 | 19 | build_debug: 20 | GO111MODULE=on go build -v -o bin/${BINARY_NAME} src/*.go 21 | 22 | build_prod: 23 | # Shrink binary by removing symbol and DWARF table 24 | # Ref: https://lukeeckley.com/post/useful-go-build-flags/ 25 | GO111MODULE=on go build -v -ldflags="-s -w" -o bin/${BINARY_NAME} src/*.go 26 | 27 | # Warning: This produces the same "bot" binary as `build` command. 28 | build_linux: 29 | GOOS=linux GOARCH=amd64 go build -o bin/${BINARY_NAME} -v src/*.go 30 | 31 | go_lint: 32 | GO111MODULE=on go mod tidy 33 | GO111MODULE=on go vet ./src 34 | golint -set_exit_status ./src/... 35 | go tool fix src/ 36 | golangci-lint run 37 | 38 | docker_lint: 39 | hadolint --ignore DL3018 Dockerfile 40 | 41 | html_lint: 42 | find website -iname '*htm*' -exec htmlhint --config .htmlhintrc {} \; 43 | 44 | update_go_deps: 45 | GO111MODULE=on go get -t -u ./... 46 | 47 | lint: format go_lint docker_lint html_lint build 48 | 49 | format: 50 | go fmt ./src/... 51 | 52 | clean: 53 | GO111MODULE=on go clean --modcache 54 | rm -rf bin/* 55 | 56 | test: 57 | GO111MODULE=on go test ./src/... -v 58 | 59 | run: build_debug 60 | PORT=8080 SECRET_VALUE=${SECRET_VALUE} ./bin/${BINARY_NAME} 61 | 62 | run_debug: # watch for modifications and restart the binary if any golang file changes 63 | filewatcher --immediate --restart "**/*.go" "killall ${BINARY_NAME}; make run" 64 | 65 | docker_build: 66 | DOCKER_BUILDKIT=1 docker build --platform=linux/amd64 -f Dockerfile -t ${DOCKER_TAG} --build-arg BINARY_NAME=${BINARY_NAME} . 67 | echo "Created docker image with tag ${DOCKER_TAG} and size `docker image inspect ${DOCKER_TAG} --format='{{.Size}}' | numfmt --to=iec-i`" 68 | 69 | # For local testing 70 | docker_run: docker_build 71 | docker rm ${BINARY_NAME}; docker run --platform=linux/amd64 --name ${BINARY_NAME} -p 127.0.0.1:80:80 \ 72 | -p 127.0.0.1:443:443 \ 73 | --env PORT=80 \ 74 | --env SECRET_VALUE=${SECRET_VALUE} \ 75 | -it ${DOCKER_TAG} 76 | 77 | # One time 78 | docker_gar_login: 79 | gcloud auth configure-docker ${REGION}-docker.pkg.dev 80 | 81 | # One time: enable artifact registry by visiting 82 | # https://console.cloud.google.com/marketplace/product/google/artifactregistry.googleapis.com?project=${GOOGLE_CLOUD_PROJECT_ID} 83 | # or 84 | # Enable Google Artifact Registry via 85 | # gcloud --project ${GOOGLE_CLOUD_PROJECT_ID} services enable artifactregistry.googleapis.com 86 | docker_gar_push: docker_build 87 | docker push ${DOCKER_TAG} 88 | echo "Pushed image can be seen at https://console.cloud.google.com/run?project=${GOOGLE_CLOUD_PROJECT_ID}" 89 | 90 | # Enable Google Cloud Run via 91 | # gcloud --project ${GOOGLE_CLOUD_PROJECT_ID} services enable run.googleapis.com 92 | gcloud_deploy: docker_gar_push 93 | git tag "gcloud_deploy_$(shell date | tr ' ' '_' | tr ':' '-')" 94 | gcloud run deploy ${GOOGLE_CLOUD_RUN_SERVICE_NAME} \ 95 | --image ${DOCKER_TAG} \ 96 | --platform managed \ 97 | --region ${REGION} \ 98 | --set-env-vars=SECRET_VALUE=${SECRET_VALUE} \ 99 | --project ${GOOGLE_CLOUD_PROJECT_ID} 100 | echo "Once you are satisfied with the new deployment, delete the old one at https://console.cloud.google.com/run/detail/${REGION}/${GOOGLE_CLOUD_RUN_SERVICE_NAME}/revisions?project=${GOOGLE_CLOUD_PROJECT_ID}" 101 | 102 | 103 | -------------------------------------------------------------------------------- /Makefile_azure: -------------------------------------------------------------------------------- 1 | DOCKER_NAME := docker-image-name 2 | PORT := 80 3 | GIT_COMMIT := $(shell git rev-parse --short HEAD) 4 | 5 | # Used by Azure Container Registry - this cannot contain dashes 6 | ACR_NAME:= 7 | DOCKER_TAG := "${ACR_NAME}.azurecr.io/images/${DOCKER_NAME}:${GIT_COMMIT}" 8 | WEB_APP_NAME := "app-name" 9 | # Everyting on Azure has to belong to a resource group! 10 | WEB_APP_RESOURCE_GROUP := "" 11 | 12 | one_time_setup: one_time_login one_time_setup_azure_web_app one_time_setup_azure_container_registry 13 | 14 | # Install Azure CLI before this 15 | # https://learn.microsoft.com/en-us/cli/azure/install-azure-cli 16 | one_time_login: 17 | az login 18 | 19 | # Ref: https://learn.microsoft.com/en-us/azure/container-apps/get-started?tabs=bash 20 | one_time_setup_azure_web_app: 21 | az provider register --namespace Microsoft.App 22 | az provider register --namespace Microsoft.OperationalInsights 23 | 24 | # On GCP this is once a day 25 | # On Azure, this is almost once every few hours :/ 26 | # Ref: https://learn.microsoft.com/en-us/azure/container-registry/container-registry-get-started-docker-cli?tabs=azure-cli 27 | one_time_setup_azure_container_registry: 28 | az acr login --name ${ACR_NAME} 29 | # Ref: https://learn.microsoft.com/en-us/azure/container-registry/container-registry-authentication?tabs=azure-cli#admin-account 30 | az acr update -n ${ACR_NAME} --admin-enabled true 31 | 32 | # Change this to build your Dockerfile 33 | docker_build: 34 | DOCKER_BUILDKIT=1 docker buildx build --platform linux/amd64 -f ./Dockerfile -t ${DOCKER_TAG} . 35 | echo "Created docker image with tag ${DOCKER_TAG} and size `docker image inspect ${DOCKER_TAG} --format='{{.Size}}' | numfmt --to=iec-i`" 36 | 37 | # Change this to match your ".env" setup 38 | docker_run: docker_build 39 | echo "Web server will be available on http://localhost:${PORT}" 40 | docker rm ${DOCKER_NAME} 2>/dev/null || true 41 | echo "Starting image ${DOCKER_TAG}" 42 | docker run \ 43 | --env-file .env \ 44 | -p 80:80 ${DOCKER_TAG} 45 | 46 | docker_acr_push: docker_build 47 | echo "If you get an error 'authentication required' then run make one_time_setup_azure_container_registry" 48 | echo "Azure Container Registry requires re-login every few hours" 49 | docker push ${DOCKER_TAG} 50 | 51 | docker_awa_deploy: docker_acr_push 52 | echo "Deploying staging on Azure Web App" 53 | az webapp config container set \ 54 | --docker-custom-image-name ${DOCKER_TAG} \ 55 | --name ${WEB_APP_NAME} \ 56 | --resource-group ${WEB_APP_RESOURCE_GROUP} 57 | az webapp update \ 58 | --name ${WEB_APP_NAME} \ 59 | --resource-group ${WEB_APP_RESOURCE_GROUP} 60 | 61 | docker_awa_stream_logs: 62 | az webapp log tail -n ${WEB_APP_NAME} -g ${WEB_APP_RESOURCE_GROUP} 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoLang + Docker + Google Cloud Run template repository [![Test](https://github.com/ashishb/golang-template-repo/actions/workflows/test.yml/badge.svg)](https://github.com/ashishb/golang-template-repo/actions/workflows/test.yml) [![Build docker image](https://github.com/ashishb/golang-template-repo/actions/workflows/docker-test.yml/badge.svg)](https://github.com/ashishb/golang-template-repo/actions/workflows/docker-test.yml) 2 | 3 | 4 | ## Basic development 5 | 6 | 1. Init using your preferred GoLang module name, for example, `make init NAME=github.com/ashishb/golang-template-repo` 7 | 2. Write the code in `src/` 8 | 3. Format it using `make format` 9 | 4. Lint it using `make lint` 10 | 5. Build it using `make build`. If required, clean it using `make clean` 11 | 6. If you have written any tests then test using `make test` 12 | 7. Run using `make run` 13 | 8. Note: If you are on Mac OS, you can explicitly build for 64-bit GNU/Linux using `make build_linux` 14 | 15 | ## Docker 16 | 1. Build docker image using `make docker_build` 17 | 2. Test using `make docker_run` 18 | 19 | ## Google cloud run deployment 20 | 1. Create a new project on [Google Cloud](https://console.cloud.google.com/) 21 | 2. Put the project ID (not project name) in `GOOGLE_CLOUD_PROJECT_NAME` variable in Makefile 22 | 3. Create a new Cloud run service at [https://console.cloud.google.com/run](https://console.cloud.google.com/run) 23 | 4. Put the cloud run service name in `GOOGLE_CLOUD_RUN_SERVICE_NAME` variable in Makefile 24 | 5. Install [google-cloud-sdk](https://formulae.brew.sh/cask/google-cloud-sdk) 25 | 6. Run `make docker_gcr_login`. This is only required only once on your Google Cloud SDK installation 26 | 7. Now, push your local image to Google Cloud registry using `make docker_gcr_push` 27 | 8. And deploy the image using `make gcloud_deploy` 28 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | const homepageEndPoint = "/" 13 | const dynamicEndpoint = "/dynamic" 14 | 15 | var secret string 16 | 17 | // StartWebServer the webserver 18 | func StartWebServer() { 19 | port := os.Getenv("PORT") 20 | if len(port) == 0 { 21 | panic("Environment variable PORT is not set") 22 | } 23 | secret = strings.TrimSpace(string(GetBase64DecodedEnvVarOrFail("SECRET_VALUE"))) 24 | http.Handle(homepageEndPoint, http.FileServer(http.Dir("./website"))) 25 | http.HandleFunc(dynamicEndpoint, handleDynamic) 26 | 27 | log.Printf("Starting web server to listen on endpoints [%s] and port %s", 28 | homepageEndPoint, port) 29 | if err := http.ListenAndServe(":"+port, nil); err != nil { 30 | panic(err) 31 | } 32 | } 33 | 34 | func handleDynamic(w http.ResponseWriter, r *http.Request) { 35 | urlPath := r.URL.Path 36 | log.Printf("Web request received on url path %s", urlPath) 37 | msg := fmt.Sprintf("Hello world from GoLang at %s. Secret value is \"%s\"", 38 | time.Now().Format("Jan 2 2006 15:04:05"), secret) 39 | _, err := w.Write([]byte(msg)) 40 | if err != nil { 41 | fmt.Printf("Failed to write response, err: %s", err) 42 | } 43 | } 44 | 45 | func main() { 46 | StartWebServer() 47 | } 48 | -------------------------------------------------------------------------------- /src/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "github.com/golang/glog" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // GetEnvVarOrFail returns the value of an environment variable. 11 | // It fails if the value isn't available. 12 | func GetEnvVarOrFail(varName string) string { 13 | value := strings.TrimSpace(os.Getenv(varName)) 14 | if len(value) == 0 { 15 | glog.Fatalf("Environment variable %s is not set", varName) 16 | } 17 | return value 18 | } 19 | 20 | // GetBase64DecodedEnvVarOrFail returns the base 64 decoded value of an environment variable. 21 | // It fails if the value isn't available or it cannot be decoded via Standard Base64 decoding. 22 | func GetBase64DecodedEnvVarOrFail(varName string) []byte { 23 | value := GetEnvVarOrFail(varName) 24 | result, err := base64.StdEncoding.DecodeString(value) 25 | if err != nil { 26 | glog.Fatalf("Failed to do base 64 decoding of the value of %s: \"%s\", error: %v", 27 | varName, value, err) 28 | } 29 | return result 30 | } 31 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Hello world

4 | 5 | --------------------------------------------------------------------------------