DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • 5 Simple Tips to Keep Dockerized Apps Secure
  • Containerize Gradle Apps and Deploy to Kubernetes With JKube Kubernetes Gradle Plugin
  • Full Stack Kubernetes with Kong Ingress Controller
  • Deploy an ASP.NET Core Application in the IBM Cloud Code Engine

Trending

  • AI’s Role in Everyday Development
  • Performing and Managing Incremental Backups Using pg_basebackup in PostgreSQL 17
  • AI-Based Threat Detection in Cloud Security
  • How Trustworthy Is Big Data?
  1. DZone
  2. Software Design and Architecture
  3. Cloud Architecture
  4. Deploying a Simple Golang Web App on Heroku

Deploying a Simple Golang Web App on Heroku

A developer and DZone Core member gives an in-depth tutorial on how to create a web application using the Go language, the Gin web framework, and Heroku.

By 
Michael Bogan user avatar
Michael Bogan
DZone Core CORE ·
Jan. 19, 21 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
11.0K Views

Join the DZone community and get the full member experience.

Join For Free

The Go programming language, often referred to as "Golang," has gained a lot of well-deserved traction in the DevOps community. Many of the most popular tools such as Docker, Kubernetes, and Terraform are written in Go, but it can also be a great choice for building web applications and APIs. 

Go provides you with all the speed and performance of a compiled language, but feels like coding in an interpreted language. This comes down to the great tooling that you get out of the box with a compiler, runner, test suite, and code formatter provided by default. Add to that the robust and easily comprehensible approach to concurrency, to take maximum advantage of today's multi-core or multi-CPU execution environments, and it's clear why the Go community has grown so rapidly.

Go feels like it was created with specific goals in mind, leading to a deceptively simple language design that's straightforward to learn, without compromising on capabilities.

In this post, I'm going to show you how easy it is to develop a simple web application in Go, package it as a lightweight Docker image, and deploy it to Heroku. I'll also show a fairly new feature of Go: built-in package management.

Go Modules

I'm going to use Go's built-in module support for this article.

Go 1.0 was released in March 2012. Up until version 1.11 (released in August 2018), developing Go applications involved managing a GOPATH for each "workspace," analogous to Java's JAVA_HOME, and all of your Go source code and any third-party libraries were stored below the GOPATH.

I always found this a bit off-putting, compared to developing code in languages like Ruby or JavaScript where I could have a simpler directory structure isolating each project. In both of those languages, a single file (Gemfile for Ruby, package.json for JavaScript) lists all the external libraries, and the package manager keeps track of managing and installing dependencies for me.

I'm not saying you can't manage the GOPATH environment variable to isolate projects from one another. I particularly find the package manager approach is easier.

Thankfully, Go now has excellent package management built in, so this is no longer a problem. However, you might find GOPATH mentioned in many older blog posts and articles, which can be a little confusing.

Hello, World!

Let's get started on our web application. As usual, this is going to be a very simple "Hello, World!" app, because I want to focus on the development and deployment process, and keep this article to a reasonable length.

Pre-requisites

You'll need:

  • A recent version of Golang (I'm using 1.14.9.)
  • Docker
  • A Heroku account (the free account works fine for this example.)
  • The Heroku command-line client
  • git

Go mod init

To create our new project, we need to create a directory for it, and use the go mod init command to initialize it as a Go module.

Shell
 




xxxxxxxxxx
1


 
1
mkdir helloworld
2
cd helloworld
3
go mod init digitalronin/helloworld


It's common practice to use your GitHub username to keep your project names globally unique, and avoid name conflicts with any of your project dependencies, but you can use any name you like.

You'll see a go.mod file in the directory now. This is where Go will track any project dependencies. If you look at the contents of the file, they should look something like this:

Plain Text
 




xxxxxxxxxx
1


 
1
module digitalronin/helloworld
2

          
3
go 1.14


Let's start committing our changes:

Java
 




xxxxxxxxxx
1


 
1
git init
2
git add *
3
git commit -m "Initial commit"


Gin

We're going to use Gin for our web application. Gin is a lightweight web framework, similar to Sinatra for Ruby, Express.js for Javascript, or Flask for Python.

Create a file called hello.go containing this code:

Go
 




x
13


 
1
package main
2

          
3
import "github.com/gin-gonic/gin"
4

          
5
func main() {
6
    r := gin.Default()
7

          
8
    r.GET("/hello", func(c *gin.Context) {
9
        c.String(200, "Hello, World!")
10
    })
11

          
12
    r.Run(":3000")
13
}


Let's break this down a little:

Go
 




xxxxxxxxxx
1


 
1
r := gin.Default()


This creates a router object, r, using the built-in defaults that come with Gin.

Then, we assign a handler function to be called for any HTTP GET requests to the path /hello, and to return the string "Hello, World!" and a 200 (HTTP OK) status code:

Go
 




xxxxxxxxxx
1


 
1
    r.GET("/hello", func(c *gin.Context) {
2
        c.String(200, "Hello, World!")
3
    })


Finally, we start our web server and tell it to listen on port 3000:

Go
 




xxxxxxxxxx
1


 
1
    r.Run(":3000")


To run this code, execute:

Go
 




xxxxxxxxxx
1


 
1
go run hello.go


You should see output like this:

Plain Text
 




xxxxxxxxxx
1
11


 
1
go: finding module for package github.com/gin-gonic/gin
2
go: found github.com/gin-gonic/gin in github.com/gin-gonic/gin v1.6.3
3
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
4

          
5
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
6
 - using env:   export GIN_MODE=release
7
 - using code:  gin.SetMode(gin.ReleaseMode)
8

          
9
[GIN-debug] GET    /hello                    --> main.main.func1 (3 handlers)
10
[GIN-debug] Listening and serving HTTP on :3000go: finding module for package github.com/gin-gonic/gin go: found github.com/gin-gonic/gin in github.com/gin-gonic/gin v1.6.3 [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env:   export GIN_MODE=release - using code:  gin.SetMode(gin.ReleaseMode) [GIN-debug] GET    /hello                    --> main.main.func1 (3 handlers) [GIN-debug] Listening and serving HTTP on :3000
11

          


Now if you visit http://localhost:3000/hello in your web browser, you should see the message "Hello, World!"

Notice that we didn't have to install Gin separately, or even edit our go.mod file to declare it as a dependency. Go figures that out and makes the necessary changes for us, which is what's happening when we see these lines in the output:

Plain Text
 




xxxxxxxxxx
1


 
1
go: finding module for package github.com/gin-gonic/gin
2
go: found github.com/gin-gonic/gin in github.com/gin-gonic/gin v1.6.3


If you look at the go.mod file, you'll see it now contains this:

Plain Text
 




xxxxxxxxxx
1


 
1
module digitalronin/helloworld
2

          
3
go 1.14
4

          
5
require github.com/gin-gonic/gin v1.6.3 // indirect


You will also see a go.sum file now. This is a text file containing references to the specific versions of all the package dependencies, and their dependencies, along with a cryptographic hash of the contents of that version of the relevant module.

The go.sum file serves a similar function to package-lock.json for a JavaScript project, or Gemfile.lock in a Ruby project, and you should always check it into version control along with your source code.

Let's do that now:

Shell
 




xxxxxxxxxx
1


 
1
git add *
2
git commit -m "Add 'Hello world' web server"


Serving HTML and JSON

I'm not going very far into what you can build with Gin, but I do want to demonstrate a little more of its functionality. In particular, sending JSON responses and serving static files.

Let's look at JSON responses first. Add the following code to your hello.go file, right after the r.GET block:

Go
 




xxxxxxxxxx
1


 
1
api := r.Group("/api")
2

          
3
api.GET("/ping", func(c *gin.Context) {
4
  c.JSON(200, gin.H{
5
    "message": "pong",
6
  })
7
})


Here we're creating a "group" of routes behind the path /api with a single path /ping which will return a JSON response.

With this code in place, run the server with go run and then hit the new API endpoint:

Shell
 




xxxxxxxxxx
1


 
1
curl http://localhost:3000/api/ping


You should get the response:

JSON
 




xxxxxxxxxx
1


 
1
{"message":"pong"}


Finally, let's make our web server serve static files. Gin has an additional library for this.

Change the import block at the top of the hello.go file to this:

Go
 




xxxxxxxxxx
1


 
1
import (
2
    "github.com/gin-gonic/contrib/static"
3
    "github.com/gin-gonic/gin"
4
)


The most popular code editors have Golang support packages you can install which will take care of the import declarations for you automatically, updating them for you whenever you use a new module in your code.

Then, add this line inside the main function:

Go
 




xxxxxxxxxx
1


 
1
r.Use(static.Serve("/", static.LocalFile("./views", true)))


The full code for our web application now looks like this:

hello.go

Go
 




xxxxxxxxxx
1
26


 
1
package main
2

          
3
import (
4
    "github.com/gin-gonic/contrib/static"
5
    "github.com/gin-gonic/gin"
6
)
7

          
8
func main() {
9
    r := gin.Default()
10

          
11
    r.GET("/hello", func(c *gin.Context) {
12
        c.String(200, "Hello, World!")
13
    })
14

          
15
    api := r.Group("/api")
16

          
17
    api.GET("/ping", func(c *gin.Context) {
18
        c.JSON(200, gin.H{
19
            "message": "pong",
20
        })
21
    })
22

          
23
    r.Use(static.Serve("/", static.LocalFile("./views", true)))
24

          
25
    r.Run()
26
}


The r.Use(static.Serve... line enables our web server to serve any static files from the views directory, so let's add a few:

Shell
 




xxxxxxxxxx
1


 
1
mkdir -p views/css


views/css/stylesheet.css

CSS
 




xxxxxxxxxx
1


 
1
body {
2
  font-family: Arial;
3
}
4

          
5
h1 {
6
  color: red;
7
}


views/index.html

HTML
 




xxxxxxxxxx
1


 
1
<html>
2
  <head>
3
    <link rel="stylesheet" href="/css/stylesheet.css" />
4
  </head>
5
  <body>
6
    <h1>Hello, World!</h1>
7
  </body>
8
</html>


Now restart the web server using go run hello.go and visit http://localhost:3000 and you should see the styled message:

Dockerize

We've written our Go web application, now let's package it up as a Docker image. We could create it as a Heroku buildpack, but one of the nice features of Go is that you can distribute your software as a single binary file. This is an area where Go really shines, and using a Docker-based Heroku deployment lets us take advantage of that. Also, this technique isn't limited to Go applications: you can use Docker-based deployment to Heroku for projects in any language. So, it's a good technique to understand.

So far, we've been running our code with the go run command. To compile it into a single, executable binary, we simply run:

Shell
 




xxxxxxxxxx
1


 
1
go build


This will compile all our Go source code and create a single file. By default, the output file will be named according to the module name, so in our case it will be called helloworld.

We can run this:

Shell
 




xxxxxxxxxx
1


 
1
./helloworld


And we can hit the same HTTP endpoints as before, either with curl or our web browser.

The static files are not compiled into the binary, so if you put the helloworld file in a different directory, it will not find the views directory to serve our HTML and CSS content.

That's all we need to do to create a binary for whatever platform we're developing on (in my case, my Mac laptop). However, to run inside a Docker container (for eventual deployment to Heroku) we need to compile a binary for whatever architecture our Docker container will run on.

I'm going to use Alpine Linux, so let's build our binary on that OS. Create a Dockerfile with the following content:

Dockerfile
 




xxxxxxxxxx
1


 
1
FROM golang:1.14.9-alpine
2
RUN mkdir /build
3
ADD go.mod go.sum hello.go /build/
4
WORKDIR /build
5
RUN go build


In this image, we start with the golang base image, add our source code, and run go build to create our helloworld binary.

We can build our Docker image like this:

Shell
 




xxxxxxxxxx
1


 
1
docker build -t helloworld .


Don't forget the trailing . at the end of that command. It tells Docker we want to use the current directory as the build context.

This creates a Docker image with our helloworld binary in it, but it also contains all the Go tools needed to compile our code, and we don't want any of that in our final image for deployment, because it makes the image unnecessarily large. It can also be a security risk to install unnecessary executables on your Docker images.

We can see the size of our Docker image like this:

Shell
 




xxxxxxxxxx
1


 
1
$ docker images helloworld
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
3
helloworld          latest              9657ec1ca905        4 minutes ago 


For comparison, the alpine image (a lightweight Linux distribution, often used as a base for Docker images) is much smaller:

Shell
 




xxxxxxxxxx
1


 
1
$ docker images alpine
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
3
alpine              latest              caf27325b298        20 months ago 


On my Mac, the helloworld binary is around 14MB, so the Golang image is much bigger than it needs to be.

What we want to do is use this Dockerfile to build our helloworld binary to run on Alpine Linux, then copy the compiled binary into an Alpine base image, without all the extra Golang tools.

We can do this using a "multistage" Docker build. Change the Dockerfile to look like this:

Dockerfile
 




xxxxxxxxxx
1
13


 
1
FROM golang:1.14.9-alpine AS builder
2
RUN mkdir /build
3
ADD go.mod go.sum hello.go /build/
4
WORKDIR /build
5
RUN go build
6

          
7
FROM alpine
8
RUN adduser -S -D -H -h /app appuser
9
USER appuser
10
COPY --from=builder /build/helloworld /app/
11
COPY views/ /app/views
12
WORKDIR /app
13
CMD ["./helloworld"]


On the first line, we label our initial Docker image AS builder.

Later, we switch to a different base image FROM alpine and then copy the helloworld binary from our builder image like this:

Shell
 




xxxxxxxxxx
1


 
1
COPY --from=builder /build/helloworld /app/


Build the new Docker image:

Shell
 




xxxxxxxxxx
1


 
1
docker build -t helloworld .


Now, it's the size you would expect for a base Alpine image plus our helloworld binary:

Shell
 




xxxxxxxxxx
1


 
1
$ docker images helloworld
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
3
helloworld          latest              1d6d9cb64c7e        8 seconds ago  


We can run our web server from the Docker image like this. (If you have another version running using go run hello.go or ./helloworld, you'll need to stop that one first, to free up port 3000.)

docker run --rm -p 3000:3000 helloworld

The dockerized webserver should behave just like the go run hello.go and ./helloworld versions except that it has its own copies of the static files. So, if you change any of the files in views/ you won't see the changes until you rebuild the Docker image and restart the container.

Deploy to Heroku

Now that we have our dockerized web application, let's deploy it to Heroku. Heroku is a PaaS provider that makes it simple to deploy and host an application. You can set up and deploy your application through the Heroku UI, or through the Heroku CLI. For this example, we'll use the Heroku command-line application.

Getting PORT From an Environment Variable

We've hard-coded our web server to run on port 3000, but that won't work on Heroku. Instead, we need to alter it to run on whichever port number is specified in the PORT environment variable, which Heroku will supply automatically.

To do this, alter the r.Run line near the bottom of our hello.go file, and remove the ":3000" string value so the line becomes:

Go
 




xxxxxxxxxx
1


 
1
r.Run()


The default behavior of Gin is to run on whatever port is in the PORT environment variable (or port 8080 if nothing is specified). This is exactly the behavior Heroku needs.

Setting up Our Heroku app

First, log into Heroku:

Shell
 




xxxxxxxxxx
1


 
1
heroku login


Now, create an app:

Shell
 




xxxxxxxxxx
1


 
1
heroku create


Tell Heroku we want to build this project using a Dockerfile, rather than a buildpack:

Shell
 




xxxxxxxxxx
1


 
1
heroku stack:set container


To do this, we also need to create a heroku.yml file like this:

YAML
 




xxxxxxxxxx
1


 
1
build:
2
  docker:
3
    web: Dockerfile
4

          
5
run:
6
  web: ./helloworld


The heroku.yml file is a manifest that defines our app and allows us to specify add-ons and config vars to use during app provisioning. 

Next, add git and commit these files, then push to Heroku to deploy:

Shell
 




xxxxxxxxxx
1


 
1
git push heroku main


My git configuration uses main as the default branch. If your default branch is called master, then run git push heroku master instead.

You should see Heroku building the image from your Dockerfile, and pushing it to the Heroku Docker registry. Once the command completes, you can view the deployed application in your browser by running:

Shell
 




xxxxxxxxxx
1


 
1
heroku open


Conclusion

To recap, here's a summary of what we covered today:

  • Creating a Golang web application, using Go modules and the Gin web framework to serve strings, JSON, and static files.
  • Using a multistage Dockerfile to create a lightweight Docker image.
  • Deploying a Docker-based application to Heroku using heroku stack:set container and a heroku.yml file.

I've only scratched the surface in this article on how to use Go to build web applications and APIs. I hope this gives you enough to get started on your own Golang web applications.

Web application Docker (software) app Golang shell Web server Plain text Build (game engine)

Opinions expressed by DZone contributors are their own.

Related

  • 5 Simple Tips to Keep Dockerized Apps Secure
  • Containerize Gradle Apps and Deploy to Kubernetes With JKube Kubernetes Gradle Plugin
  • Full Stack Kubernetes with Kong Ingress Controller
  • Deploy an ASP.NET Core Application in the IBM Cloud Code Engine

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!