├── .env ├── .github └── workflows │ ├── docker-image.yml │ └── go.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── cmd └── golang-docker-build-tutorial │ ├── main.go │ └── main_test.go ├── create_image.sh ├── go.mod ├── go.sum ├── internal └── pkg │ ├── responses.go │ └── responses_test.go ├── justfile └── start_container.sh /.env: -------------------------------------------------------------------------------- 1 | # .env 2 | # 3 | # This file pre-defines environment variables and is read by `just`. 4 | # 5 | # If needed, you can override the variables defined here when invoking `just` 6 | # by manually setting the variable in the shell environment. For example, the 7 | # command below sets `DOCKER_IMAGE_NAME` to "foo/bar" 8 | # 9 | # $ DOCKER_IMAGE_NAME="foo/bar" just ... 10 | # 11 | 12 | PROJECT_VERSION="1.0.0-alpha" 13 | 14 | # App-related settings 15 | APP_PORT=8123 # must match port setting in `main.go` and EXPOSE in `Dockerfile` 16 | 17 | # Docker-related settings 18 | DOCKER_IMAGE_NAME="miguno/golang-docker-build-tutorial" 19 | DOCKER_IMAGE_TAG="latest" 20 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | env: 15 | PROJECT_VERSION: 1.0.0-alpha 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | - name: Build the Docker image 22 | run: > 23 | docker buildx build . 24 | --file Dockerfile 25 | --build-arg PROJECT_VERSION="$PROJECT_VERSION" 26 | --tag miguno/golang-docker-build-tutorial:$(date +%s) 27 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | env: 17 | PROJECT_VERSION: 1.0.0-alpha 18 | steps: 19 | - uses: actions/checkout@v3 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | - name: Set up Go 23 | uses: actions/setup-go@v3 24 | with: 25 | go-version: 1.22 26 | 27 | - name: Build 28 | run: go build -trimpath -ldflags="-w -s -X 'main.Version=$PROJECT_VERSION'" -v ./... 29 | 30 | - name: Test 31 | run: go test -cover -v ./... 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage_profile.txt 2 | /app 3 | /app_linux-386 4 | /app_linux-amd64 5 | /app_linux-arm 6 | /app_linux-arm64 7 | /app_macos-arm64 8 | /app_windows-386.exe 9 | /app_windows-amd64.exe 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.10.x" 5 | 6 | env: 7 | - DEP_VERSION="0.4.1" 8 | 9 | before_install: 10 | - curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -o $GOPATH/bin/dep 11 | - chmod +x $GOPATH/bin/dep 12 | 13 | install: 14 | - dep ensure 15 | 16 | before_script: 17 | - GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/) # All the .go files, excluding vendor/ 18 | - go get github.com/golang/lint/golint 19 | - go get honnef.co/go/tools/cmd/megacheck 20 | 21 | script: 22 | - test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt 23 | - go test -v -race ./... # Run all the tests with the race detector enabled 24 | - go vet ./... # go vet is the official Go static analyzer 25 | - megacheck ./... # "go vet on steroids" + linter 26 | - golint -set_exit_status $(go list ./...) # one last linter 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # We use a multi-stage build setup. 4 | # (https://docs.docker.com/build/building/multi-stage/) 5 | 6 | ############################################################################### 7 | # Stage 1 (to create a "build" image, ~850MB) # 8 | ############################################################################### 9 | # Image from https://hub.docker.com/_/golang 10 | FROM golang:1.22.2 AS builder 11 | # smoke test to verify if golang is available 12 | RUN go version 13 | 14 | ARG PROJECT_VERSION 15 | 16 | COPY . /go/src/github.com/miguno/golang-docker-build-tutorial/ 17 | WORKDIR /go/src/github.com/miguno/golang-docker-build-tutorial/ 18 | RUN set -Eeux && \ 19 | go mod download && \ 20 | go mod verify 21 | 22 | RUN GOOS=linux GOARCH=amd64 \ 23 | go build \ 24 | -trimpath \ 25 | -ldflags="-w -s -X 'main.Version=${PROJECT_VERSION}'" \ 26 | -o app cmd/golang-docker-build-tutorial/main.go 27 | RUN go test -cover -v ./... 28 | 29 | ############################################################################### 30 | # Stage 2 (to create a downsized "container executable", ~5MB) # 31 | ############################################################################### 32 | 33 | # If you need SSL certificates for HTTPS, replace `FROM SCRATCH` with: 34 | # 35 | # FROM alpine:3.17.1 36 | # RUN apk --no-cache add ca-certificates 37 | # 38 | FROM scratch 39 | WORKDIR /root/ 40 | COPY --from=builder /go/src/github.com/miguno/golang-docker-build-tutorial/app . 41 | 42 | EXPOSE 8123 43 | ENTRYPOINT ["./app"] 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Template: Create a Docker image for a Go application 2 | 3 | [![GitHub forks](https://img.shields.io/github/forks/miguno/golang-docker-build-tutorial)](https://github.com/miguno/golang-docker-build-tutorial/fork) 4 | [![Docker workflow status](https://github.com/miguno/golang-docker-build-tutorial/actions/workflows/docker-image.yml/badge.svg)](https://github.com/miguno/golang-docker-build-tutorial/actions/workflows/docker-image.yml) 5 | [![Go workflow status](https://github.com/miguno/golang-docker-build-tutorial/actions/workflows/go.yml/badge.svg)](https://github.com/miguno/golang-docker-build-tutorial/actions/workflows/go.yml) 6 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 7 | 8 | A template project to create a Docker image for a Go application. 9 | The example application exposes an HTTP endpoint. 10 | 11 | > [!TIP] 12 | > 13 | > **Java developer?** Check out https://github.com/miguno/java-docker-build-tutorial 14 | 15 | Features: 16 | 17 | - The Docker build uses a 18 | [multi-stage build setup](https://docs.docker.com/build/building/multi-stage/) 19 | to minimize the size of the generated Docker image, which is 5MB 20 | - Supports [Docker BuildKit](https://docs.docker.com/build/) 21 | - Golang 1.22 22 | - [GitHub Actions workflows](https://github.com/miguno/golang-docker-build-tutorial/actions) for 23 | [Golang](https://github.com/miguno/golang-docker-build-tutorial/actions/workflows/go.yml) 24 | and 25 | [Docker](https://github.com/miguno/golang-docker-build-tutorial/actions/workflows/docker-image.yml) 26 | - Optionally, uses 27 | [just](https://github.com/casey/just) 28 | ![](https://img.shields.io/github/stars/casey/just) 29 | for running common commands conveniently, see [justfile](justfile). 30 | - Uses [.env](.env) as central configuration to set variables used by 31 | [justfile](justfile) and other helper scripts in this project. 32 | 33 | # Requirements 34 | 35 | Docker must be installed on your local machine. That's it. You do not need to 36 | have Go installed. 37 | 38 | # Usage and Demo 39 | 40 | **Step 1:** Create the Docker image according to [Dockerfile](Dockerfile). 41 | This step builds, tests, and packages the [Go application](app.go). 42 | The resulting image is 5MB in size. 43 | 44 | ```shell 45 | # ***Creating an image may take a few minutes!*** 46 | $ docker build --build-arg PROJECT_VERSION=1.0.0-alpha -t miguno/golang-docker-build-tutorial:latest . 47 | 48 | # You can also build with the new BuildKit. 49 | # https://docs.docker.com/build/ 50 | $ docker buildx build --build-arg PROJECT_VERSION=1.0.0-alpha -t miguno/golang-docker-build-tutorial:latest . 51 | ``` 52 | 53 | Optionally, you can check the size of the generated Docker image: 54 | 55 | ```shell 56 | $ docker images miguno/golang-docker-build-tutorial 57 | REPOSITORY TAG IMAGE ID CREATED SIZE 58 | miguno/golang-docker-build-tutorial latest 2de05b854c1b 11 minutes ago 4.78MB 59 | ``` 60 | 61 | **Step 2:** Start a container for the Docker image. 62 | 63 | ```shell 64 | $ docker run -p 8123:8123 miguno/golang-docker-build-tutorial:latest 65 | ``` 66 | 67 | **Step 3:** Open another terminal and access the example API endpoint of the 68 | running container. 69 | 70 | ```shell 71 | $ curl http://localhost:8123/status 72 | {"status": "idle"} 73 | ``` 74 | 75 | # Usage with just 76 | 77 | If you have [just](https://github.com/casey/just) installed, you can run the 78 | commands above more conveniently: 79 | 80 | ```shell 81 | $ just 82 | Available recipes: 83 | audit # detect known vulnerabilities (requires https://github.com/sonatype-nexus-community/nancy) 84 | build # build executable for local OS 85 | coverage # show test coverage 86 | default # print available targets 87 | deps # show dependencies 88 | docker-image-create # create a docker image (requires Docker) 89 | docker-image-run # run the docker image (requires Docker) 90 | docker-image-size # size of the docker image (requires Docker) 91 | evaluate # evaluate and print all just variables 92 | explain lint-identifier # explain lint identifier (e.g., "SA1006") 93 | format # format source code 94 | lint # run linters (requires https://github.com/dominikh/go-tools) 95 | outdated # detect outdated modules (requires https://github.com/psampaz/go-mod-outdated) 96 | release # build release executables for all supported platforms 97 | run # run executable for local OS 98 | send-request-to-app # send request to the app's HTTP endpoint (requires running container) 99 | system-info # print system information such as OS and architecture 100 | test *FLAGS # run tests with colorized output (requires https://github.com/kyoh86/richgo) 101 | test-vanilla *FLAGS # run tests (vanilla), used for CI workflow 102 | tidy # add missing module requirements for imported packages, removes requirements that aren't used anymore 103 | vulnerabilities # detect known vulnerabilities (requires https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) 104 | ``` 105 | 106 | Example: 107 | 108 | ```shell 109 | $ just docker-image-create 110 | $ just docker-image-run 111 | ``` 112 | 113 | # Notes 114 | 115 | You can run the Go application locally if you have Go installed. 116 | See [justfile](justfile) for additional commands and options. 117 | 118 | ```shell 119 | # Build 120 | $ go build -trimpath -ldflags="-w -s" -v -o app cmd/golang-docker-build-tutorial/main.go 121 | 122 | # Test 123 | $ go test -cover -v ./... 124 | 125 | # Run 126 | $ ./app 127 | ``` 128 | -------------------------------------------------------------------------------- /cmd/golang-docker-build-tutorial/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi/v5" 7 | "github.com/go-chi/chi/v5/middleware" 8 | "github.com/go-chi/render" 9 | 10 | internal "github.com/miguno/golang-docker-build-tutorial/internal/pkg" 11 | ) 12 | 13 | type Server struct { 14 | Router *chi.Mux 15 | // Other configs such as DB settings can be added here 16 | } 17 | 18 | func CreateNewServer() *Server { 19 | s := &Server{} 20 | s.Router = chi.NewRouter() 21 | // Uncomment to enable logging of incoming HTTP requests to STDOUT. 22 | // Requires `import "github.com/go-chi/chi/v5/middleware"`. 23 | //s.Router.Use(middleware.Logger) 24 | return s 25 | } 26 | 27 | // StatusResponse is just a basic example. 28 | type StatusResponse struct { 29 | Status string `json:"status,omitempty"` 30 | } 31 | 32 | func (s *Server) MountHandlers() { 33 | // Mount all Middleware here 34 | s.Router.Use(middleware.Logger) 35 | 36 | // Mount all handlers here 37 | s.Router.Get("/status", func(w http.ResponseWriter, r *http.Request) { 38 | // Create a Response object 39 | var response StatusResponse 40 | if internal.IsIdleToyFunction() { 41 | response = StatusResponse{Status: "idle"} 42 | } else { 43 | response = StatusResponse{Status: "busy"} 44 | } 45 | 46 | render.JSON(w, r, response) 47 | }) 48 | 49 | } 50 | 51 | func main() { 52 | s := CreateNewServer() 53 | s.MountHandlers() 54 | err := http.ListenAndServe(":8123", s.Router) 55 | if err != nil { 56 | panic(err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cmd/golang-docker-build-tutorial/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func executeRequest(req *http.Request, s *Server) *httptest.ResponseRecorder { 13 | rr := httptest.NewRecorder() 14 | s.Router.ServeHTTP(rr, req) 15 | return rr 16 | } 17 | 18 | func verifyResponseCode(t *testing.T, expected, actual int) { 19 | if expected != actual { 20 | t.Errorf("Expected HTTP response code %d but got %d\n", expected, actual) 21 | } 22 | } 23 | 24 | func TestStatusEndpoint(t *testing.T) { 25 | s := CreateNewServer() 26 | s.MountHandlers() 27 | 28 | req, _ := http.NewRequest("GET", "/status", nil) 29 | res := executeRequest(req, s) 30 | 31 | verifyResponseCode(t, http.StatusOK, res.Code) 32 | var sr StatusResponse 33 | _ = json.Unmarshal(res.Body.Bytes(), &sr) 34 | expectedResponse := StatusResponse{Status: "idle"} 35 | require.Equal(t, expectedResponse, sr) 36 | } 37 | -------------------------------------------------------------------------------- /create_image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ 4 | # `-u`: Errors if a variable is referenced before being set 5 | # `-o pipefail`: Prevent errors in a pipeline (`|`) from being masked 6 | set -uo pipefail 7 | 8 | # Import environment variables from .env 9 | set -o allexport && source .env && set +o allexport 10 | 11 | # Set variable from environment variable PROJECT_VERSION, if the latter exists. 12 | # If not, fall back to the specified default value (excluding the leading `-`). 13 | declare -r project_version="${PROJECT_VERSION:-1.0.0-alpha}" 14 | 15 | echo "Building image '$DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG'..." 16 | # TIP: Add `--progress=plain` to see the full docker output when you are 17 | # troubleshooting the build setup of your image. 18 | declare -r DOCKER_OPTIONS="" 19 | # Use BuildKit, i.e. `buildx build` instead of just `build` 20 | # https://docs.docker.com/build/ 21 | docker buildx build $DOCKER_OPTIONS --build-arg PROJECT_VERSION=${project_version} -t "$DOCKER_IMAGE_NAME":"$DOCKER_IMAGE_TAG" . 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/miguno/golang-docker-build-tutorial 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.0.12 7 | github.com/go-chi/render v1.0.3 8 | github.com/stretchr/testify v1.9.0 9 | ) 10 | 11 | require ( 12 | github.com/ajg/form v1.5.1 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | gopkg.in/yaml.v3 v3.0.1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= 2 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= 6 | github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 7 | github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= 8 | github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 12 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 16 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 17 | -------------------------------------------------------------------------------- /internal/pkg/responses.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | // To demonstrate having some code that can be unit-tested. 4 | func IsIdleToyFunction() bool { 5 | return true 6 | } 7 | -------------------------------------------------------------------------------- /internal/pkg/responses_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | // To demonstrate how to integrate unit testing in a project. 10 | func TestIsIdleToyFunction(t *testing.T) { 11 | assert.True(t, IsIdleToyFunction()) 12 | } 13 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # This justfile requires https://github.com/casey/just 2 | 3 | # Load environment variables from `.env` file. 4 | set dotenv-load 5 | 6 | timestamp := `date +%s` 7 | 8 | semver := env_var('PROJECT_VERSION') 9 | commit := `git show -s --format=%h` 10 | version := semver + "+" + commit 11 | 12 | coverage_profile_log := "coverage_profile.txt" 13 | 14 | # print available targets 15 | default: 16 | @just --list --justfile {{justfile()}} 17 | 18 | # evaluate and print all just variables 19 | evaluate: 20 | @just --evaluate 21 | 22 | # print system information such as OS and architecture 23 | system-info: 24 | @echo "architecture: {{arch()}}" 25 | @echo "os: {{os()}}" 26 | @echo "os family: {{os_family()}}" 27 | 28 | # detect known vulnerabilities (requires https://github.com/sonatype-nexus-community/nancy) 29 | audit: 30 | go list -json -m all | nancy sleuth --loud 31 | 32 | # benchmark the app's HTTP endpoint with plow (requires https://github.com/six-ddc/plow) 33 | benchmark-plow: 34 | @echo plow -c 100 --duration=30s http://localhost:${APP_PORT}/status 35 | @plow -c 100 --duration=30s http://localhost:${APP_PORT}/status 36 | 37 | # benchmark the app's HTTP endpoint with wrk (requires https://github.com/wg/wrk) 38 | benchmark-wrk: 39 | @echo wrk -t 10 -c 100 --latency --duration 30 http://localhost:${APP_PORT}/status 40 | @wrk -t 10 -c 100 --latency --duration 30 http://localhost:${APP_PORT}/status 41 | 42 | # build executable for local OS 43 | build: test-vanilla 44 | @echo "Building executable for local OS ..." 45 | go build -trimpath -ldflags="-X 'main.Version={{version}}'" -o app cmd/golang-docker-build-tutorial/main.go 46 | 47 | # show test coverage 48 | coverage: 49 | go test -coverprofile={{coverage_profile_log}} ./... 50 | go tool cover -html={{coverage_profile_log}} 51 | 52 | # show dependencies 53 | deps: 54 | go mod graph 55 | 56 | # create a docker image (requires Docker) 57 | docker-image-create: 58 | @echo "Creating a docker image ..." 59 | @PROJECT_VERSION={{version}} ./create_image.sh 60 | 61 | # run the docker image (requires Docker) 62 | docker-image-run: 63 | @echo "Running container from docker image ..." 64 | @./start_container.sh 65 | 66 | # size of the docker image (requires Docker) 67 | docker-image-size: 68 | @docker images $DOCKER_IMAGE_NAME 69 | 70 | # explain lint identifier (e.g., "SA1006") 71 | explain lint-identifier: 72 | staticcheck -explain {{lint-identifier}} 73 | 74 | # format source code 75 | format: 76 | @echo "Formatting source code ..." 77 | gofmt -l -s -w . 78 | 79 | # run linters (requires https://github.com/dominikh/go-tools) 80 | lint: 81 | staticcheck -f stylish ./... || \ 82 | (echo "\nRun \`just explain \` for details." && \ 83 | exit 1) 84 | 85 | # detect outdated modules (requires https://github.com/psampaz/go-mod-outdated) 86 | outdated: 87 | go list -u -m -json all | go-mod-outdated -update 88 | 89 | # build release executables for all supported platforms 90 | release: test-vanilla 91 | @echo "Building release executables (incl. cross compilation) ..." 92 | # `go tool dist list` shows supported architectures (GOOS) 93 | GOOS=darwin GOARCH=arm64 \ 94 | go build -trimpath -ldflags "-X 'main.Version={{version}}' -s -w" -o app_macos-arm64 cmd/golang-docker-build-tutorial/main.go 95 | GOOS=linux GOARCH=386 \ 96 | go build -trimpath -ldflags "-X 'main.Version={{version}}' -s -w" -o app_linux-386 cmd/golang-docker-build-tutorial/main.go 97 | GOOS=linux GOARCH=amd64 \ 98 | go build -trimpath -ldflags "-X 'main.Version={{version}}' -s -w" -o app_linux-amd64 cmd/golang-docker-build-tutorial/main.go 99 | GOOS=linux GOARCH=arm \ 100 | go build -trimpath -ldflags "-X 'main.Version={{version}}' -s -w" -o app_linux-arm cmd/golang-docker-build-tutorial/main.go 101 | GOOS=linux GOARCH=arm64 \ 102 | go build -trimpath -ldflags "-X 'main.Version={{version}}' -s -w" -o app_linux-arm64 cmd/golang-docker-build-tutorial/main.go 103 | 104 | # run executable for local OS 105 | run: 106 | @echo "Running golang-docker-build-tutorial with defaults ..." 107 | go run -ldflags="-X 'main.Version={{version}}'" cmd/golang-docker-build-tutorial/main.go 108 | 109 | # send request to the app's HTTP endpoint (requires curl and running container) 110 | send-request-to-app: 111 | @curl http://localhost:${APP_PORT}/status 112 | 113 | # run tests with colorized output (requires https://github.com/kyoh86/richgo) 114 | test *FLAGS: 115 | richgo test -cover {{FLAGS}} ./... 116 | 117 | # run tests (vanilla), used for CI workflow 118 | test-vanilla *FLAGS: 119 | go test -cover {{FLAGS}} ./... 120 | 121 | # add missing module requirements for imported packages, removes requirements that aren't used anymore 122 | tidy: 123 | go mod tidy 124 | 125 | # detect known vulnerabilities (requires https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) 126 | vulnerabilities: 127 | govulncheck ./... 128 | 129 | # watch sources for changes and trigger a rebuild (requires https://github.com/watchexec/watchexec) 130 | watch: 131 | # Watch all go files in the current directory and all subdirectories for 132 | # changes. If something changed, re-run the build. 133 | @watchexec -e go -- just build 134 | -------------------------------------------------------------------------------- /start_container.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ 4 | # `-u`: Errors if a variable is referenced before being set 5 | # `-o pipefail`: Prevent errors in a pipeline (`|`) from being masked 6 | set -uo pipefail 7 | 8 | # Import environment variables from .env 9 | set -o allexport && source .env && set +o allexport 10 | echo "Starting container for image '$DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG', exposing port ${APP_PORT}/tcp" 11 | echo "- Run 'curl http://localhost:${APP_PORT}/status' to send a test request to the containerized app." 12 | echo "- Enter Ctrl-C to stop the container." 13 | docker run -p "$APP_PORT:$APP_PORT" "$DOCKER_IMAGE_NAME":"$DOCKER_IMAGE_TAG" 14 | --------------------------------------------------------------------------------