How do I package my Golang app for production? A Docker Guide.
Learn how to Dockerize a Go application with a Docker build, create a minimal container image, publish it, and deploy it on Hostly.
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.

We will then:
- Create a multi-stage Docker image.
- Test the image locally.
- Publish it to Docker Hub.
- 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 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:
mkdir hello-go-api
cd hello-go-apiInitialize a Go module:
go mod init example.com/hello-go-apiThe 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.mod2. Create the HTTP service
Create a file named main.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:
{
"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:
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 3000Open the following address in your browser:
http://localhost:3000Alternatively, use curl:
curl http://localhost:3000The response should be:
{
"message": "Hello, World!"
}Stop the service with Ctrl+C.
Test the environment variable
Start the application again with a different greeting target:
GREETING_TARGET=Go go run .On Windows PowerShell, use:
$env:GREETING_TARGET = "Go"
go run .Request the endpoint again:
curl http://localhost:3000The response should now be:
{
"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-apiThe 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:
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
└── DockerfileUnlike 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:
FROM --platform=$BUILDPLATFORM golang:1.26.5-alpine AS buildThe 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:
COPY --from=buildBuild for the target platform
ARG TARGETOS
ARG TARGETARCHDocker 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=$BUILDPLATFORMThis 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
RUN apk add --no-cache ca-certificatesOur 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
COPY go.mod ./
RUN go mod downloadThe 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:
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
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:
FROM scratchscratch 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:
COPY --from=build \
/etc/ssl/certs/ca-certificates.crt \
/etc/ssl/certs/ca-certificates.crt
COPY --from=build /out/server /serverThe 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
USER 10001:10001The 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
EXPOSE 3000EXPOSE 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
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:
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.0The final period tells Docker to use the current directory as the build context.
Docker will:
- Start the Go build stage.
- Download any declared dependencies.
- Compile the application.
- Start the empty runtime stage.
- Copy the binary and CA certificates into it.
- Save the finished image.
Confirm that the image exists:
docker image lsBecause 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:
docker run \
--rm \
-p 3000:3000 \
hello-go-api:1.0.0The port mapping follows this structure:
HOST_PORT:CONTAINER_PORTTraffic sent to port 3000 on your computer is forwarded to port 3000 inside the container.
Test it with:
curl http://localhost:3000The response should be:
{
"message": "Hello, World!"
}Stop the container with Ctrl+C.
Supply the environment variable
Run the image again with GREETING_TARGET:
docker run \
--rm \
-p 3000:3000 \
-e GREETING_TARGET=Container \
hello-go-api:1.0.0Request the endpoint:
curl http://localhost:3000The response should now be:
{
"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-apiAuthenticate from your terminal:
docker loginTag the local image with your Docker Hub username:
docker tag \
hello-go-api:1.0.0 \
YOUR_USERNAME/hello-go-api:1.0.0Replace YOUR_USERNAME with your actual Docker Hub username.
Push the image:
docker push YOUR_USERNAME/hello-go-api:1.0.0The 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:
docker.io/YOUR_USERNAME/hello-go-api:1.0.0Using 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:
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
Create a free workspace to follow this tutorial and deploy your new app!
Create a new service in Hostly and provide:
- Your desired
hostly.ninjasubdomain - Image URL:
docker.io/YOUR_USERNAME/hello-go-api:1.0.0 - Container port:
3000
Add the following environment variable:
GREETING_TARGET=HostlyThe 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:
{
"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:
- Creating a Go module with
go mod init. - Building the HTTP service with the standard library.
- Compiling a static executable with
CGO_ENABLED=0. - Separating compilation and runtime using a multi-stage build.
- Leaving the Go compiler and source code out of the final image.
- Running the binary from an otherwise empty
scratchimage. - Running the process with a non-root numeric user.
- Supplying environment-specific configuration when the container starts.
- 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.
