{"id":22933518,"url":"https://github.com/bluebrown/echo-server","last_synced_at":"2026-05-01T12:32:23.480Z","repository":{"id":111480735,"uuid":"381203844","full_name":"bluebrown/echo-server","owner":"bluebrown","description":"dockerized go echo-server","archived":false,"fork":false,"pushed_at":"2021-06-30T10:49:28.000Z","size":12,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-05T22:32:02.406Z","etag":null,"topics":["docker","docker-image","go"],"latest_commit_sha":null,"homepage":"","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bluebrown.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-06-29T01:23:56.000Z","updated_at":"2021-06-30T10:49:31.000Z","dependencies_parsed_at":"2023-05-20T03:59:13.690Z","dependency_job_id":null,"html_url":"https://github.com/bluebrown/echo-server","commit_stats":null,"previous_names":[],"tags_count":0,"template":true,"template_full_name":null,"purl":"pkg:github/bluebrown/echo-server","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluebrown%2Fecho-server","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluebrown%2Fecho-server/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluebrown%2Fecho-server/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluebrown%2Fecho-server/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bluebrown","download_url":"https://codeload.github.com/bluebrown/echo-server/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluebrown%2Fecho-server/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32497811,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-30T13:12:12.517Z","status":"online","status_checked_at":"2026-05-01T02:00:05.856Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker","docker-image","go"],"created_at":"2024-12-14T11:30:07.874Z","updated_at":"2026-05-01T12:32:23.460Z","avatar_url":"https://github.com/bluebrown.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"When building and running images locally for development purposes, many best practices are neglected. However, eventually the image gets deployed. For a real deployment we want to take additional steps.\n\n## Motivation\n\nIn this scenario I *want* to deploy a simple \"echo-server\" written in go.\n\nThis is a good example because it is a compiled language, and it spawns a long-running process with which we can interact via HTTP. That is usually the case when deploying application in container.\n\nThe project structure looks like this\n\n```shell\n.\n├── Dockerfile\n├── .dockerignore\n├── go.mod\n├── main.go\n└── README.md\n```\n\n{% details Go Code %}\n\n```go\npackage main\n\nimport (\n \"encoding/json\"\n \"fmt\"\n \"log\"\n \"net/http\"\n)\n\nfunc main() {\n http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n  reqHeadersBytes, _ := json.Marshal(r.Header)\n  text := fmt.Sprintf(\"RemoteAddr: %s\\n\", r.RemoteAddr)\n  text += fmt.Sprintf(\"Method: %s\\n\", r.Method)\n  text += fmt.Sprintf(\"RequestURI: %s\\n\", r.RequestURI)\n  text += fmt.Sprintf(\"Proto: %s\\n\", r.Proto)\n  text += fmt.Sprintf(\"ContentLength: %x\\n\", r.ContentLength)\n  text += fmt.Sprintf(\"Headers: %s\\n\", string(reqHeadersBytes))\n  fmt.Fprint(w, text)\n })\n log.Fatal(http.ListenAndServe(\":80\", nil))\n}\n```\n\n{% enddetails %}\n\nYou can find the full [project](https://github.com/bluebrown/echo-server) on github.\n\n## Dockerignore\n\nWith the `.dockerignore` file we can exclude content from build directory. So what's listed in this file will not get send to the daemon as build part of the  context.\n\n```shell\n$ docker build --tag bluebrown/echo-server .\nsending build context to Docker daemon  6.656kB\n...\n```\n\nIts a good idea to list the `Dockerfile` and the `.dockerignore` itself here in order to avoid cache invalidation of previous steps when working on these 2 files. Additionally list everything that isn't strictly required for building the image.\n\n```shell\n.git\n.dockerignore\nDockerfile\nREADME.md\n```\n\n## Multi Staging\n\nWith multi-staging it is possible to perform work in one image and then copy only what is required to a second slim image.\n\n```Dockerfile\nFROM golang as builder\n\nWORKDIR /src/\nCOPY . .\nRUN go vet\nRUN go test\nRUN go build \\\n  -ldflags '-linkmode external -w -extldflags \"-static\"' \\\n  -o echo-server\n\n# ---\nFROM alpine as runner\n\nCMD [\"/usr/code/echo-server\"]\nCOPY --from=builder /src/echo-server /usr/code/\n```\n\n```shell\ndocker build --tag bluebrown/echo-server .\ndocker image ls\n```\n\nIf we inspect the output we can see that the final image is much smaller then the builder image. Instead of running eventually a container with the source code and the binary and a size of 868MB, we are going to run only a slim container containing the compiled binary of size 11.7MB\n\n```shell\nREPOSITORY              TAG       IMAGE ID       CREATED          SIZE\nbluebrown/echo-server   latest    ee63052b3b15   9 seconds ago    11.7MB\n\u003cnone\u003e                  \u003cnone\u003e    5eb556bfbc0f   12 seconds ago   868MB\ngolang                  latest    ee23292e2826   4 days ago       862MB\nalpine                  latest    d4ff818577bc   12 days ago      5.6MB\n```\n\nIt is not always required or useful to use multi-stage build. For example, you can compile the binary outside the image and copy in the final alpine image. However, compiling on build ensures that it is always compiled in the same environment with the same flags.\n\n## Healthcheck\n\nHealthchecks are a way to determine of the container is running ok. By default, the process ID (PID) is checked. So if a container has a PID it is considered healthy.\n\nIt is possible to customize the healthcheck per image. For example using curl to make a HTTP request to see if the application is responding ok.\n\n```Dockerfile\nFROM alpine as runner\n...\nHEALTHCHECK \\\n  --interval=30s \\\n  --timeout=30s \\\n  --start-period=5s \\\n  --retries=3 \\\n  CMD curl --head --fail localhost || exit 1\n...\n```\n\nWhen running starting the container now, we can also see the health status via CLI.\n\n```shell\n$ docker run --rm  --detach --name echo-server -p 80:80  bluebrown/echo-server\n$ docker ps -a --format '{{.Names}} - {{.Status}}'\necho-server - Up 5 seconds (health: starting)\n```\n\nHowever, if you wait 2 minutes and check again, you notice that the container is marked as unhealthy.\n\n```shell\necho-server - Up 2 minutes (unhealthy)\n```\n\nYou may also notice that even though the container is considered unhealthy, docker doesn't stop it or do anything about it. It is up to the operator or orchestration framework to handle the situation according to the health status. Docker swarm for example would restart the container now.\n\nBut why was the container *unhealthy* in the first place? If you try to curl on the published port of the the container you get actually a response.\n\n{% details Curl Output %}\n\n```shell\n$ curl localhost\nRemoteAddr: 172.17.0.1:50152\nMethod: GET\nRequestURI: /\nProto: HTTP/1.1\nContentLength: 0\nHeaders: {\"Accept\":[\"*/*\"],\"User-Agent\":[\"curl/7.68.0\"]}\n```\n\n{% enddetails %}\n\nThe reason is, that the health check command is executed inside the container, and in our case we are trying to use curl even though its not installed in the container. We can see this by inspecting the status logs in the docker inspect output.\n\n```shell\ndocker inspect echo-server --format \\\n  '{{range .State.Health.Log}}{{.End}} | Exit Code: {{.ExitCode}} | {{.Output}}{{end}}\n```\n\n```shell\n2021-06-30 10:06:05.795671501 +0000 UTC | Exit Code: 1 | /bin/sh: curl: not found\n2021-06-30 10:06:35.888445198 +0000 UTC | Exit Code: 1 | /bin/sh: curl: not found\n2021-06-30 10:07:05.959345369 +0000 UTC | Exit Code: 1 | /bin/sh: curl: not found\n```\n\nWe could simply install curl on build in order to fix this.\n\n```Dockerfile\nFROM alpine as runner\n...\nRUN apk add --update curl \u0026\u0026 rm -rf /var/cache/apk/*\n...\n```\n\n```shell\necho-server - Up 33 seconds (healthy)\n```\n\n## Build Arguments\n\nBuild arguments are a great way to customize the build behavior without having to modify the `Dockerfile`.\n\n```Dockerfile\nFROM golang as builder\n...\nARG VET_FLAGS=\"\"\nRUN go vet \"$VET_FLAGS\"\n\nARG TEST_FLAGS=\"\"\nRUN go test \"$TEST_FLAGS\"\n\nARG LD_FLAGS='-linkmode external -w -extldflags \"-static\"'\nARG BUILD_FLAGS=\"\"\nRUN go build -ldflags \"$LD_FLAGS\" -o echo-server \"$BUILD_FLAGS\"\n...\n```\n\nThat way we can pass additional flags on build. For example being extra verbose for debugging purposes.\n\n```shell\ndocker build --tag bluebrown/echo-server \\\n  --build-arg VET_FLAGS=\"-x\" \\\n  --build-arg BUILD_FLAGS=\"-x\" \\\n  .\n```\n\n## Using Unprivileged User\n\nBy default, Docker gives root permission to the process that runs a container. That's no good. It's commonly solved by created an unprivileged user inside the container and run the final command as this user.\n\n```Dockerfile\nFROM alpine as runner\n...\nARG UID=8080\nARG USER=\"docker-app\"\nRUN adduser \\\n    --disabled-password \\\n    --gecos \"\" \\\n    --home /usr/code \\\n    --no-create-home \\\n    --uid \"$UID\" \\\n    \"$USER\"\n...\nUSER $USER\n```\n\nNote, when working with local volumes you have to ensure the permission on the volume matches the `UID` and `GUID` of that user.\n\n```shell\nsudo chown -R 8080:8080 ./my-volume\ndocker run --volume $PWD/my-volume:/usr/data\n```\n\n## Labeling the Image\n\nLabel systems are a common way to work with dynamic configuration these days. They are a way to attach key value pairs to resources which can be used by other tools in order to operate given resource.\n\nThe Open Container Initiative has label suggestions which are commonly known and accepted. [OCI Annotations](https://github.com/opencontainers/image-spec/blob/master/annotations.md). The older deprecated version of the spec has better explanations in my opinion and since many labels were basically just renamed its can be useful too check out [label-schema.org documentation](http://label-schema.org/rc1/) as well.\n\nThe format of the oci labels is `org.opencontainers.image.\u003clabel\u003e` where label has to be chosen from a fixed list of labels provided by OCI. If you have custom labels, you should **not** prefix them with `org.opencontainers.image` but with your own prefix e.g. `com.myorg.env=\"production\"`.\n\n```Dockerfile\nFROM alpine as runner\n...\nARG VERSION=\"0.1.0\"\nARG ENVIRONMENT=\"dev\"\nARG BRANCH=\"main\"\nARG COMMIT_HASH=\"unknown\"\nARG CREATED_DATE=\"unknown\"\n\nLABEL org.opencontainers.image.created=\"${CREATED_DATE}\" \\\n    org.opencontainers.image.url=\"https://github.com/my-repo\"  \\\n    org.opencontainers.image.source=\"https://github.com/my-repo/Dockerfile\" \\\n    org.opencontainers.image.version=\"${VERSION}-${ENVIRONMENT}\" \\\n    org.opencontainers.image.revision=\"${COMMIT_HASH}\" \\\n    org.opencontainers.image.vendor=\"rainbowstack\" \\\n    org.opencontainers.image.title=\"echo-server\" \\\n    org.opencontainers.image.description=\"go echo server\" \\\n    org.opencontainers.image.documentation=\"https://github.com/my-repo/README.md\" \\\n    org.opencontainers.image.authors=\"nico braun\" \\\n    org.opencontainers.image.licenses=\"(BSD-1-Clause)\" \\\n    org.opencontainers.image.ref.name=\"${BRANCH}\" \\\n    dev.rainbowstack.environment=\"${ENVIRONMENT}\"\n...\n```\n\nIf you now inspect the image you can find the labels.\n\n```shell\ndocker inspect bluebrown/echo-server --format \\\n'{{range $key, $val := .ContainerConfig.Labels}}{{printf \"%s = %s\\n\" $key $val }}{{end}}'\n```\n\n{% details Output %}\n\n```shell\ndev.rainbowstack.environment = dev\norg.opencontainers.image.authors = nico braun\norg.opencontainers.image.created = unknown\norg.opencontainers.image.description = go echo server\norg.opencontainers.image.documentation = https://github.com/my-repo/README.md\norg.opencontainers.image.licenses = (BSD-1-Clause)\norg.opencontainers.image.ref.name = main\norg.opencontainers.image.revision = unknown\norg.opencontainers.image.source = https://github.com/my-repo/Dockerfile\norg.opencontainers.image.title = echo-server\norg.opencontainers.image.url = https://github.com/my-repo\norg.opencontainers.image.vendor = rainbowstack\norg.opencontainers.image.version = 0.1.0-dev\n```\n\n{% enddetails %}\n\n## The Final Dockerfile\n\nThe complete Dockerfile looks now like this. We are using `.dockerignore` and `multi-staging` to reduce the final image size drastically. An `HTTP Healthcheck` is implemented to see if the deployed server is actually functioning. `Arguments` and `Labels` improve the build customization and allow users and programs to get meta data about the image.\n\n```Dockerfile\nFROM golang as builder\n\nWORKDIR /src/\nCOPY . .\n\nARG VET_FLAGS=\"\"\nRUN go vet \"$VET_FLAGS\"\n\nARG TEST_FLAGS=\"\"\nRUN go test \"$TEST_FLAGS\"\n\nARG LD_FLAGS='-linkmode external -w -extldflags \"-static\"'\nARG BUILD_FLAGS=\"\"\nRUN go build -ldflags \"$LD_FLAGS\" -o echo-server \"$BUILD_FLAGS\"\n\n\n# ---\nFROM alpine as runner\n\nCMD [\"/usr/code/echo-server\"]\n\nRUN apk add --update curl \u0026\u0026 rm -rf /var/cache/apk/*\n\nHEALTHCHECK \\\n  --interval=30s \\\n  --timeout=30s \\\n  --start-period=5s \\\n  --retries=3 \\\n  CMD curl --head --fail localhost || exit 1\n\nARG UID=8080\nARG USER=\"docker-app\"\nRUN adduser \\\n    --disabled-password \\\n    --gecos \"\" \\\n    --home /usr/code \\\n    --no-create-home \\\n    --uid \"$UID\" \\\n    \"$USER\"\n\nARG VERSION=\"0.1.0\"\nARG ENVIRONMENT=\"dev\"\nARG BRANCH=\"main\"\nARG COMMIT_HASH=\"unknown\"\nARG CREATED_DATE=\"unknown\"\n\nLABEL org.opencontainers.image.created=\"${CREATED_DATE}\" \\\n    org.opencontainers.image.url=\"https://github.com/my-repo\"  \\\n    org.opencontainers.image.source=\"https://github.com/my-repo/Dockerfile\" \\\n    org.opencontainers.image.version=\"${VERSION}-${ENVIRONMENT}\" \\\n    org.opencontainers.image.revision=\"${COMMIT_HASH}\" \\\n    org.opencontainers.image.vendor=\"rainbowstack\" \\\n    org.opencontainers.image.title=\"echo-server\" \\\n    org.opencontainers.image.description=\"go echo server\" \\\n    org.opencontainers.image.documentation=\"https://github.com/my-repo/README.md\" \\\n    org.opencontainers.image.authors=\"nico braun\" \\\n    org.opencontainers.image.licenses=\"(BSD-1-Clause)\" \\\n    org.opencontainers.image.ref.name=\"${BRANCH}\" \\\n    dev.rainbowstack.environment=\"${ENVIRONMENT}\"\n\nCOPY --from=builder /src/echo-server /usr/code/\nUSER $USER\n```\n\n## Bonus: Content Trust\n\nIf you are using a private registry, consider opting into [content trust](https://docs.docker.com/engine/security/trust/).\n\nSince version 1.8 docker supports code signage mechanism for published images. It is not enabled by default but can be enabled via environment flag. When enabled docker will automatically sign published images and verify on pull.\n\nConsider running this in your current shell or adding it to your `~/.bashrc`.\n\n```shell\nexport DOCKER_CONTENT_TRUST=1\n```\n\nNote, if you are planning to push images you need to take additional steps to [create your private signage key](https://docs.docker.com/engine/security/trust/#signing-images-with-docker-content-trust).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbluebrown%2Fecho-server","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbluebrown%2Fecho-server","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbluebrown%2Fecho-server/lists"}