> ## Content Index
> Fetch the complete content index at: https://blog.hostly.ninja/llms.txt
> Use this file to discover other available public pages before exploring further.

# How do I package my Golang app for production? A Docker Guide.
- URL: https://blog.hostly.ninja/how-do-i-dockerize-my-golang-app-for-production-a-golang-guide/
- Published: 2026-09-29T09:30:45.000Z
- Updated: 2026-11-02T18:04:42.000Z
- Description: Learn how to Dockerize a Go application with a Docker build, create a minimal container image, publish it, and deploy it on Hostly.
- Author: Andreas Knipl
- Tags: tutorial, Docker

A Go application can be compiled into a standalone executable, which makes it particularly well suited to container deployment. The Go compiler is only required while building the application; the finished container can contain little more than the compiled binary itself.

In this tutorial, we will create a small HTTP service using only Go’s standard library. The service will read its greeting from an environment variable, return a JSON response, and listen on a configurable port.

![](https://blog.hostly.ninja/content/images/2026/09/image-2.png)

We will then:

1. Create a multi-stage Docker image.
2. Test the image locally.
3. Publish it to Docker Hub.
4. Deploy it on Hostly.

No web framework or third-party Go dependency is required.

## What you will need

Before starting, make sure you have:

- Go installed
- Docker Desktop or Docker Engine
- A free Docker Hub account
- A text editor or IDE
- A terminal
- AND of course, we recommend at least a free [hostly.ninja](https://hostly.ninja/?ref=blog.hostly.ninja) account!

This tutorial uses Go 1.26.5, which is the latest stable Go release as of August 4, 2026\. Go 1.27 is still under development at the time of writing.

## 1\. Create the project

Create a new directory and move into it:

```Bash
mkdir hello-go-api
cd hello-go-api
```

Initialize a Go module:

```Bash
go mod init example.com/hello-go-api
```

The module path normally identifies where a Go project can be downloaded, such as its Git repository. For this self-contained example, `example.com/hello-go-api` is sufficient.

The command creates a `go.mod` file, which records the module name, supported Go version, and eventually any external dependencies used by the application.

At this point, the project contains:

```
hello-go-api/
└── go.mod
```

## 2\. Create the HTTP service

Create a file named `main.go`:

```Go
package main

import (
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"os"
	"time"
)

type greetingResponse struct {
	Message string `json:"message"`
}

func main() {
	port := environmentOrDefault("PORT", "3000")
	target := environmentOrDefault("GREETING_TARGET", "World")

	mux := http.NewServeMux()

	mux.HandleFunc("GET /", func(response http.ResponseWriter, request *http.Request) {
		response.Header().Set("Content-Type", "application/json")

		result := greetingResponse{
			Message: "Hello, " + target + "!",
		}

		if err := json.NewEncoder(response).Encode(result); err != nil {
			log.Printf("could not write response: %v", err)
		}
	})

	server := &http.Server{
		Addr:              ":" + port,
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       10 * time.Second,
		WriteTimeout:      10 * time.Second,
		IdleTimeout:       60 * time.Second,
	}

	log.Printf("server listening on port %s", port)

	if err := server.ListenAndServe(); err != nil &&
		!errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}

func environmentOrDefault(key string, fallback string) string {
	value := os.Getenv(key)

	if value == "" {
		return fallback
	}

	return value
}
```

The application exposes one `GET` endpoint at `/` and returns a JSON response:

```JSON
{
  "message": "Hello, World!"
}
```

The greeting can be changed through the `GREETING_TARGET` environment variable. The port can similarly be changed through `PORT`, although it defaults to `3000`.

### Listening on all network interfaces

The server address is constructed like this:

```
Addr: ":" + port,
```

An address such as `:3000` listens on port `3000` across the available network interfaces. This allows Docker and Hostly to send traffic to the application.

Binding only to `localhost:3000` would make the process reachable only from inside the container itself.

### Setting server timeouts

Instead of calling `http.ListenAndServe` directly, the example creates an `http.Server` and configures several timeouts:

```Go
server := &http.Server{
	Addr:              ":" + port,
	Handler:           mux,
	ReadHeaderTimeout: 5 * time.Second,
	ReadTimeout:       10 * time.Second,
	WriteTimeout:      10 * time.Second,
	IdleTimeout:       60 * time.Second,
}
```

These limits prevent connections from remaining open indefinitely while slowly sending headers, reading requests, or writing responses. Go’s `net/http` package exposes these settings through the `http.Server` type.

For a larger application, the exact timeout values should reflect its expected request sizes and response times. The values here are a reasonable starting point for a small JSON API.

## 3\. Run the application locally

Start the application directly:

```
go run .
```

The log output should show:

```
server listening on port 3000
```

Open the following address in your browser:

```
http://localhost:3000
```

Alternatively, use `curl`:

```Bash
curl http://localhost:3000
```

The response should be:

```JSON
{
  "message": "Hello, World!"
}
```

Stop the service with `Ctrl+C`.

### Test the environment variable

Start the application again with a different greeting target:

```Bash
GREETING_TARGET=Go go run .
```

On Windows PowerShell, use:

```Bash
$env:GREETING_TARGET = "Go"
go run .
```

Request the endpoint again:

```Bash
curl http://localhost:3000
```

The response should now be:

```JSON
{
  "message": "Hello, Go!"
}
```

The application code has not changed. Only its runtime configuration is different.

## 4\. Add a `.dockerignore` file

Docker sends the project directory to the builder as its build context. Files that are not needed for the build should be excluded.

Create a file named `.dockerignore`:

```
.git/
.gitignore

.env
.env.*

*.log
coverage.out

hello-go-api
```

The final entry excludes a locally compiled binary named `hello-go-api`, should one exist.

Environment files are excluded because they may contain passwords, API keys, or other sensitive configuration. Environment-specific values should be supplied when the container runs rather than embedded in the image.

## 5\. Create the Dockerfile

Create a file named `Dockerfile` without a file extension:

```Dockerfile
FROM --platform=$BUILDPLATFORM golang:1.26.5-alpine AS build

ARG TARGETOS
ARG TARGETARCH

WORKDIR /src

RUN apk add --no-cache ca-certificates

COPY go.mod ./

RUN go mod download

COPY main.go ./

RUN CGO_ENABLED=0 \
    GOOS=$TARGETOS \
    GOARCH=$TARGETARCH \
    go build \
    -trimpath \
    -o /out/server \
    .

FROM scratch

COPY --from=build \
    /etc/ssl/certs/ca-certificates.crt \
    /etc/ssl/certs/ca-certificates.crt

COPY --from=build /out/server /server

USER 10001:10001

EXPOSE 3000

ENTRYPOINT ["/server"]
```

The project should now contain:

```
hello-go-api/
├── main.go
├── go.mod
├── .dockerignore
└── Dockerfile
```

Unlike the Node.js and Python examples, this Dockerfile contains two `FROM` instructions. Each one begins a separate build stage.

Docker calls this a multi-stage build. Files can be copied from one stage into another, while build tools and intermediate files are left out of the final image.

## 6\. Understanding the build stage

The first stage compiles the application:

```Dockerfile
FROM --platform=$BUILDPLATFORM golang:1.26.5-alpine AS build
```

The official Go image contains the Go compiler and related development tools. We use the Alpine variant because it keeps the builder comparatively small, although the builder’s size does not directly determine the size of the finished image. The entire stage will be discarded after the binary has been copied out of it.

The `golang:1.26.5-alpine` image is an official multi-platform image and is also published under aliases such as `1.26-alpine`.

`AS build` gives the stage a name. Later instructions can refer to it using:

```Dockerfile
COPY --from=build
```

### Build for the target platform

```Dockerfile
ARG TARGETOS
ARG TARGETARCH
```

Docker BuildKit supplies these values automatically. They describe the operating system and CPU architecture for which the final image is being built.

The builder itself uses:

```
--platform=$BUILDPLATFORM
```

This means the compiler runs using the architecture of the build machine, while Go cross-compiles the binary for the requested target architecture.

For an ordinary local build, the target will usually be Linux on AMD64 or ARM64.

### Install trusted CA certificates

```Dockerfile
RUN apk add --no-cache ca-certificates
```

Our example service does not make outbound HTTPS requests, but many real applications do. Trusted certificate-authority information is required when a program connects to HTTPS APIs, databases, or other TLS-enabled services.

The certificate bundle will later be copied into the final image.

### Copy the module file first

```Dockerfile
COPY go.mod ./

RUN go mod download
```

The module definition is copied before the source code so Docker can cache the dependency-download step.

This application uses only the Go standard library, so there are no external modules to download. A larger project will usually also contain a `go.sum` file. Its Dockerfile would typically copy both module files:

```Dockerfile
COPY go.mod go.sum ./
```

Docker can then reuse the downloaded dependencies whenever the application source changes without the module files changing.

### Compile a static binary

```Dockerfile
RUN CGO_ENABLED=0 \
    GOOS=$TARGETOS \
    GOARCH=$TARGETARCH \
    go build \
    -trimpath \
    -o /out/server \
    .
```

`go build` compiles the application and writes the result to `/out/server`.

`CGO_ENABLED=0` prevents the binary from depending on a C runtime library. For this standard-library application, that produces a self-contained executable that can run in an otherwise empty container image.

`GOOS` and `GOARCH` select the target operating system and architecture.

The `-trimpath` option removes local filesystem paths from the compiled output. This avoids embedding build-machine directory names in the binary and makes builds more reproducible.

## 7\. Understanding the runtime stage

The second stage starts from `scratch`:

```Dockerfile
FROM scratch
```

`scratch` is an empty image. It contains no Linux distribution, shell, package manager, compiler, or debugging tools.

We only copy the files that the application needs:

```Dockerfile
COPY --from=build \
    /etc/ssl/certs/ca-certificates.crt \
    /etc/ssl/certs/ca-certificates.crt

COPY --from=build /out/server /server
```

The final image therefore contains:

- The compiled Go application
- Trusted CA certificates

The Go compiler, source code, module cache, Alpine package manager, and other build tools are not included.

Separating the build environment from the runtime environment reduces image size and removes tools that are unnecessary once the application is running. Docker specifically identifies multi-stage builds and minimal runtime images as ways to reduce the final image’s attack surface.

### Run as a non-root user

```Dockerfile
USER 10001:10001
```

The container runs with numeric user and group ID `10001` rather than as root.

Because `scratch` has no `/etc/passwd` file, there are no named operating-system users. Docker can nevertheless start the process using a numeric identity.

The application does not need elevated permissions. Port `3000` is above the range of privileged ports, and the service does not need to modify operating-system files.

### Document the port

```Dockerfile
EXPOSE 3000
```

`EXPOSE` documents the port on which the application normally listens.

It does not publish the port automatically. Local Docker runs still require a `-p` mapping, and Hostly must still be configured with container port `3000`.

### Start the binary

```Dockerfile
ENTRYPOINT ["/server"]
```

When a container starts, Docker runs the compiled application directly.

There is no shell script, Go command, framework launcher, or package manager between the container runtime and the server process.

## 8\. Build the image

Build the image from the project directory:

```Bash
docker build -t hello-go-api:1.0.0 .
```

The `-t` option gives the image a name and version tag:

```
hello-go-api:1.0.0
```

The final period tells Docker to use the current directory as the build context.

Docker will:

1. Start the Go build stage.
2. Download any declared dependencies.
3. Compile the application.
4. Start the empty runtime stage.
5. Copy the binary and CA certificates into it.
6. Save the finished image.

Confirm that the image exists:

```Bash
docker image ls
```

Because the final image does not contain the Go SDK or an operating-system distribution, it should be considerably smaller than the builder image.

## 9\. Run the image locally

Start a container from the image:

```Bash
docker run \
  --rm \
  -p 3000:3000 \
  hello-go-api:1.0.0
```

The port mapping follows this structure:

```
HOST_PORT:CONTAINER_PORT
```

Traffic sent to port `3000` on your computer is forwarded to port `3000` inside the container.

Test it with:

```Bash
curl http://localhost:3000
```

The response should be:

```JSON
{
  "message": "Hello, World!"
}
```

Stop the container with `Ctrl+C`.

### Supply the environment variable

Run the image again with `GREETING_TARGET`:

```Bash
docker run \
  --rm \
  -p 3000:3000 \
  -e GREETING_TARGET=Container \
  hello-go-api:1.0.0
```

Request the endpoint:

```Bash
curl http://localhost:3000
```

The response should now be:

```JSON
{
  "message": "Hello, Container!"
}
```

The environment variable is provided when the container starts. It is not stored permanently in the image.

## 10\. Publish the image to Docker Hub

Create a public Docker Hub repository named:

```
hello-go-api
```

Authenticate from your terminal:

```Bash
docker login
```

Tag the local image with your Docker Hub username:

```Bash
docker tag \
  hello-go-api:1.0.0 \
  YOUR_USERNAME/hello-go-api:1.0.0
```

Replace `YOUR_USERNAME` with your actual Docker Hub username.

Push the image:

```Bash
docker push YOUR_USERNAME/hello-go-api:1.0.0
```

The `docker push` command uploads a tagged image to Docker Hub or another container registry. Authentication credentials are managed through `docker login`.

After the upload finishes, the complete image address is:

```Bash
docker.io/YOUR_USERNAME/hello-go-api:1.0.0
```

Using an explicit version tag makes it possible to identify exactly which application version a deployment is running.

## 11\. Publish a multi-platform image

A normal `docker build` produces an image for one CPU architecture. That may be AMD64 on a typical server or ARM64 on an Apple Silicon computer.

The Dockerfile is already prepared for Go cross-compilation through `TARGETOS` and `TARGETARCH`, so you can publish both variants with Docker Buildx:

```Bash
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t YOUR_USERNAME/hello-go-api:1.0.0 \
  --push \
  .
```

Docker publishes both variants under the same image name. When a platform pulls the image, it selects the variant matching its operating system and architecture.

Go is particularly convenient here because the compiler can produce binaries for other supported operating systems and architectures without requiring the final binary to be compiled through emulation.

## 12\. Deploy the image on Hostly

[![CTA Image](https://blog.hostly.ninja/content/images/2026/09/right_pointing-2.webp)](https://hostly.ninja/?ref=blog.hostly.ninja) 

Create a free workspace to follow this tutorial and deploy your new app!

[Create an account and get started! ](https://hostly.ninja/?ref=blog.hostly.ninja) 

Create a new service in Hostly and provide:

- Your desired `hostly.ninja` subdomain
- Image URL: `docker.io/YOUR_USERNAME/hello-go-api:1.0.0`
- Container port: `3000`

Add the following environment variable:

```
GREETING_TARGET=Hostly
```

The application does not require secrets, volumes, or configuration files, so those sections can remain empty.

Click **Deploy**.

Hostly will pull the image, start the compiled Go service, route traffic to port `3000`, and provide SSL for the selected subdomain.

Hostly checks whether it can establish a TCP connection to the configured port, so the application does not require a separate health-check endpoint.

Once the deployment is ready, open the assigned URL. The response should be:

```JSON
{
  "message": "Hello, Hostly!"
}
```

We used a public Docker Hub repository to keep the tutorial simple. Hostly can also pull images from private repositories when the corresponding registry credentials are configured.

## What you have built

You started with a small Go program and produced a portable container image that can run locally or on Hostly.

The Go-specific parts of the process were:

1. Creating a Go module with `go mod init`.
2. Building the HTTP service with the standard library.
3. Compiling a static executable with `CGO_ENABLED=0`.
4. Separating compilation and runtime using a multi-stage build.
5. Leaving the Go compiler and source code out of the final image.
6. Running the binary from an otherwise empty `scratch` image.
7. Running the process with a non-root numeric user.
8. Supplying environment-specific configuration when the container starts.
9. Publishing architecture-specific binaries under one multi-platform image.

A larger Go application may use a router, database driver, migrations, templates, or additional modules, but its deployment contract remains straightforward: compile the service, package the binary, expose its port, and provide its runtime configuration.