Getting Started on Heroku with Go
Introduction
Complete this tutorial to deploy a sample Go app to Cedar, the legacy generation of the Heroku platform. To deploy the app to the Fir generation, only available to Heroku Private Spaces, follow this guide instead.
The tutorial assumes that you have:
- A verified Heroku Account
- An Eco dynos plan subscription (recommended)
- Go 1.23 or better installed locally (Installation instructions available here)
- Postgres installed locally
Using dynos and databases to complete this tutorial counts towards your usage. We recommend using our low-cost plans to complete this tutorial. Eligible students can apply for platform credits through our new Heroku for GitHub Students program.
Set Up
Install the Heroku Command Line Interface (CLI). Use the CLI to manage and scale your app, provision add-ons, view your logs, and run your app locally.
The Heroku CLI requires Git, the popular version control system. If you don’t already have Git installed, complete the following before proceeding:
Download and run the installer for your platform:
Download the appropriate installer for your Windows installation:
You can find more installation options for the Heroku CLI here.
After installation, you can use the heroku command from your command shell.
To log in to the Heroku CLI, use the heroku login command:
$ heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/***
heroku: Waiting for login...
Logging in... done
Logged in as me@example.com
This command opens your web browser to the Heroku login page. If your browser is already logged in to Heroku, click the Log In button on the page.
This authentication is required for the heroku and git commands to work correctly.
If you have any problems installing or using the Heroku CLI, see the main Heroku CLI article for advice and troubleshooting steps.
If you’re behind a firewall that uses a proxy to connect with external HTTP/HTTPS services, set the HTTP_PROXY or HTTPS_PROXY environment variables in your local development environment before running the heroku command.
Clone the Sample App
If you’re new to Heroku, it’s recommended that you complete this tutorial using the Heroku-provided sample application.
To deploy an existing application, follow this article instead.
Clone the sample application to get a local version of the code. Execute these commands in your local command shell or terminal:
$ git clone https://github.com/heroku/go-getting-started.git
$ cd go-getting-started
You now have a functioning Git repository that contains a simple application. It includes a go.mod file, which is used by Go and Go’s module dependency system.
Create Your App
Using a dyno and a database to complete this tutorial counts towards your usage. Delete your app, and database as soon as you’re done to control costs.
Apps use Eco dynos if you’re subscribed to Eco by default. Otherwise, it defaults to Basic dynos. The Eco dynos plan is shared across all Eco dynos in your account. It’s recommended if you plan on deploying many small apps to Heroku. Learn more here. Eligible students can apply for platform credits through our Heroku for GitHub Students program.
Create an app on Heroku to prepare the platform to receive your source code:
$ heroku create
Creating app... done, warm-everglades-94026
https://warm-everglades-94026-9f0b6c3a2b32.herokuapp.com/ | https://git.heroku.com/warm-everglades-94026.git
When you create an app, a Git remote named heroku is also created and added to your local repository configuration. Git remotes are versions of your repository that live on other servers. You can deploy your app by pushing code to that special Heroku-hosted remote associated with your app.
Heroku generates a random name for your app, in this case, warm-everglades-94026. You can specify your own app name.
Deploy the App
Using a dyno to complete this tutorial counts towards your usage. Delete your app and database as soon as you’re done to control costs.
Deploy your code. This command pushes the main branch of the sample repo to your heroku remote, which then deploys to Heroku:
$ git push heroku main
remote: Updated 19 paths from 4b580ce
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Building on the Heroku-24 stack
remote: -----> Determining which buildpack to use for this app
remote: -----> Go app detected
remote: -----> Detected go modules via go.mod
remote: -----> Detected Module Name: github.com/heroku/go-getting-started
remote: -----> New Go Version, clearing old cache
remote: -----> Installing go1.25.9
remote: -----> Fetching go1.25.9.linux-amd64.tar.gz
remote: -----> Determining packages to install
remote: go: downloading github.com/gin-gonic/gin v1.12.0
remote: go: downloading github.com/heroku/x v0.5.3
remote: go: downloading github.com/gin-contrib/sse v1.1.0
remote: go: downloading github.com/mattn/go-isatty v0.0.20
remote: go: downloading github.com/quic-go/quic-go v0.59.0
remote: go: downloading golang.org/x/net v0.51.0
remote: go: downloading github.com/go-playground/validator/v10 v10.30.1
remote: go: downloading github.com/goccy/go-yaml v1.19.2
remote: go: downloading github.com/pelletier/go-toml/v2 v2.2.4
remote: go: downloading github.com/ugorji/go/codec v1.3.1
remote: go: downloading go.mongodb.org/mongo-driver/v2 v2.5.0
remote: go: downloading google.golang.org/protobuf v1.36.10
remote: go: downloading golang.org/x/sys v0.41.0
remote: go: downloading github.com/gabriel-vasile/mimetype v1.4.12
remote: go: downloading github.com/go-playground/universal-translator v0.18.1
remote: go: downloading github.com/leodido/go-urn v1.4.0
remote: go: downloading golang.org/x/crypto v0.48.0
remote: go: downloading golang.org/x/text v0.34.0
remote: go: downloading github.com/quic-go/qpack v0.6.0
remote: go: downloading github.com/go-playground/locales v0.14.1
remote: -----> Detected the following main packages to install:
remote: github.com/heroku/go-getting-started
remote: -----> Running: go install -v -tags heroku github.com/heroku/go-getting-started
remote: internal/unsafeheader
remote: internal/goarch
remote: internal/byteorder
remote: internal/cpu
remote: internal/coverage/rtcov
remote: internal/abi
remote: internal/godebugs
remote: internal/goexperiment
remote: internal/bytealg
remote: internal/chacha8rand
remote: internal/goos
remote: internal/profilerecord
remote: internal/runtime/math
remote: internal/runtime/atomic
remote: internal/runtime/syscall
remote: internal/runtime/strconv
remote: internal/runtime/gc
remote: internal/runtime/cgroup
remote: internal/runtime/exithook
remote: internal/asan
remote: internal/msan
remote: internal/race
remote: internal/runtime/sys
remote: internal/stringslite
remote: internal/trace/tracev2
remote: sync/atomic
remote: internal/runtime/maps
remote: internal/synctest
remote: math/bits
remote: unicode
remote: unicode/utf8
remote: internal/sync
remote: cmp
remote: crypto/internal/fips140/alias
remote: crypto/internal/fips140deps/byteorder
remote: math
remote: crypto/internal/fips140/subtle
remote: runtime
remote: encoding
remote: internal/itoa
remote: unicode/utf16
remote: container/list
remote: crypto/internal/fips140deps/cpu
remote: crypto/internal/boring/sig
remote: vendor/golang.org/x/crypto/cryptobyte/asn1
remote: vendor/golang.org/x/crypto/internal/alias
remote: internal/nettrace
remote: log/internal
remote: github.com/gin-gonic/gin/internal/bytesconv
remote: github.com/go-playground/locales/currency
remote: github.com/leodido/go-urn/scim/schema
remote: github.com/pelletier/go-toml/v2/internal/characters
remote: go.mongodb.org/mongo-driver/v2/internal/bsoncoreutil
remote: google.golang.org/protobuf/internal/flags
remote: golang.org/x/crypto/internal/alias
remote: github.com/quic-go/quic-go/internal/utils/ringbuffer
remote: golang.org/x/net/internal/iana
remote: log/slog/internal
remote: iter
remote: internal/reflectlite
remote: crypto/subtle
remote: sync
remote: slices
remote: weak
remote: maps
remote: errors
remote: sort
remote: internal/bisect
remote: internal/testlog
remote: crypto/internal/fips140cache
remote: io
remote: strconv
remote: internal/oserror
remote: path
remote: internal/godebug
remote: bytes
remote: strings
remote: encoding/base64
remote: reflect
remote: syscall
remote: hash
remote: hash/crc32
remote: bufio
remote: crypto
remote: crypto/internal/fips140deps/godebug
remote: crypto/internal/impl
remote: crypto/internal/fips140
remote: math/rand/v2
remote: crypto/internal/fips140/sha256
remote: crypto/internal/fips140/sha3
remote: crypto/internal/fips140/sha512
remote: time
remote: internal/syscall/unix
remote: internal/syscall/execenv
remote: crypto/internal/fips140/hmac
remote: crypto/internal/randutil
remote: crypto/internal/fips140/check
remote: math/rand
remote: crypto/internal/fips140/aes
remote: crypto/internal/fips140/nistec/fiat
remote: crypto/internal/fips140/edwards25519/field
remote: io/fs
remote: internal/poll
remote: internal/filepathlite
remote: internal/fmtsort
remote: encoding/binary
remote: context
remote: os
remote: crypto/internal/fips140/bigmod
remote: crypto/internal/fips140/nistec
remote: crypto/sha3
remote: crypto/internal/fips140hash
remote: internal/saferio
remote: crypto/internal/fips140/edwards25519
remote: crypto/internal/fips140/hkdf
remote: crypto/internal/fips140/tls12
remote: crypto/internal/fips140/tls13
remote: vendor/golang.org/x/crypto/internal/poly1305
remote: crypto/fips140
remote: crypto/tls/internal/fips140tls
remote: encoding/pem
remote: vendor/golang.org/x/net/dns/dnsmessage
remote: internal/singleflight
remote: fmt
remote: crypto/internal/sysrand
remote: crypto/internal/entropy
remote: crypto/internal/fips140/drbg
remote: vendor/golang.org/x/sys/cpu
remote: crypto/internal/fips140/aes/gcm
remote: crypto/internal/fips140only
remote: crypto/internal/fips140/ecdh
remote: crypto/internal/fips140/ecdsa
remote: crypto/cipher
remote: encoding/xml
remote: flag
remote: encoding/json
remote: compress/flate
remote: crypto/internal/boring
remote: math/big
remote: compress/gzip
remote: crypto/aes
remote: crypto/des
remote: crypto/ecdh
remote: crypto/sha512
remote: crypto/internal/fips140/ed25519
remote: crypto/hkdf
remote: crypto/hmac
remote: crypto/internal/fips140/mlkem
remote: vendor/golang.org/x/crypto/chacha20
remote: crypto/md5
remote: vendor/golang.org/x/crypto/chacha20poly1305
remote: crypto/rc4
remote: crypto/rand
remote: crypto/elliptic
remote: crypto/internal/boring/bbig
remote: encoding/asn1
remote: crypto/ed25519
remote: crypto/internal/hpke
remote: crypto/internal/fips140/rsa
remote: crypto/sha1
remote: crypto/sha256
remote: crypto/dsa
remote: encoding/hex
remote: vendor/golang.org/x/crypto/cryptobyte
remote: unique
remote: crypto/x509/pkix
remote: crypto/rsa
remote: runtime/cgo
remote: net/netip
remote: crypto/ecdsa
remote: path/filepath
remote: vendor/golang.org/x/text/transform
remote: log
remote: vendor/golang.org/x/text/unicode/bidi
remote: net/url
remote: vendor/golang.org/x/text/unicode/norm
remote: vendor/golang.org/x/text/secure/bidirule
remote: vendor/golang.org/x/net/http2/hpack
remote: mime
remote: mime/quotedprintable
remote: net/http/internal
remote: net/http/internal/ascii
remote: github.com/gin-gonic/gin/codec/json
remote: github.com/gabriel-vasile/mimetype/internal/scan
remote: hash/adler32
remote: compress/zlib
remote: github.com/gabriel-vasile/mimetype/internal/markup
remote: vendor/golang.org/x/net/idna
remote: github.com/gabriel-vasile/mimetype/internal/charset
remote: debug/dwarf
remote: github.com/gabriel-vasile/mimetype/internal/csv
remote: github.com/gabriel-vasile/mimetype/internal/json
remote: github.com/go-playground/locales
remote: github.com/go-playground/universal-translator
remote: github.com/leodido/go-urn
remote: golang.org/x/sys/cpu
remote: golang.org/x/crypto/sha3
remote: debug/macho
remote: golang.org/x/text/internal/tag
remote: golang.org/x/text/internal/language
remote: net
remote: github.com/gabriel-vasile/mimetype/internal/magic
remote: regexp/syntax
remote: golang.org/x/text/internal/language/compact
remote: github.com/gabriel-vasile/mimetype
remote: golang.org/x/text/language
remote: regexp
remote: github.com/goccy/go-yaml/token
remote: github.com/pelletier/go-toml/v2/internal/danger
remote: github.com/pelletier/go-toml/v2/unstable
remote: github.com/goccy/go-yaml/ast
remote: github.com/goccy/go-yaml/scanner
remote: github.com/pelletier/go-toml/v2/internal/tracker
remote: github.com/pelletier/go-toml/v2
remote: github.com/goccy/go-yaml/lexer
remote: encoding/base32
remote: github.com/goccy/go-yaml/printer
remote: github.com/goccy/go-yaml/internal/format
remote: github.com/goccy/go-yaml/internal/errors
remote: encoding/gob
remote: github.com/goccy/go-yaml/parser
remote: go/token
remote: html
remote: github.com/goccy/go-yaml
remote: text/template/parse
remote: runtime/debug
remote: text/template
remote: go.mongodb.org/mongo-driver/v2/internal/decimal128
remote: go.mongodb.org/mongo-driver/v2/internal/binaryutil
remote: go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore
remote: hash/fnv
remote: google.golang.org/protobuf/internal/detrand
remote: google.golang.org/protobuf/internal/errors
remote: html/template
remote: google.golang.org/protobuf/encoding/protowire
remote: google.golang.org/protobuf/internal/pragma
remote: google.golang.org/protobuf/reflect/protoreflect
remote: go.mongodb.org/mongo-driver/v2/bson
remote: google.golang.org/protobuf/internal/encoding/messageset
remote: golang.org/x/sys/unix
remote: google.golang.org/protobuf/internal/genid
remote: crypto/x509
remote: net/textproto
remote: vendor/golang.org/x/net/http/httpguts
remote: vendor/golang.org/x/net/http/httpproxy
remote: mime/multipart
remote: net/mail
remote: github.com/go-playground/validator/v10
remote: crypto/tls
remote: google.golang.org/protobuf/internal/order
remote: google.golang.org/protobuf/internal/strs
remote: google.golang.org/protobuf/reflect/protoregistry
remote: google.golang.org/protobuf/runtime/protoiface
remote: github.com/mattn/go-isatty
remote: google.golang.org/protobuf/proto
remote: golang.org/x/net/http2/hpack
remote: github.com/quic-go/qpack
remote: github.com/quic-go/quic-go/internal/monotime
remote: github.com/quic-go/quic-go/quicvarint
remote: github.com/quic-go/quic-go/qlogwriter/jsontext
remote: golang.org/x/crypto/chacha20
remote: golang.org/x/crypto/internal/poly1305
remote: golang.org/x/crypto/hkdf
remote: github.com/quic-go/quic-go/internal/utils/linkedlist
remote: golang.org/x/net/bpf
remote: golang.org/x/crypto/chacha20poly1305
remote: golang.org/x/net/internal/socket
remote: golang.org/x/text/transform
remote: golang.org/x/text/unicode/bidi
remote: golang.org/x/text/unicode/norm
remote: golang.org/x/net/ipv4
remote: golang.org/x/net/ipv6
remote: golang.org/x/text/secure/bidirule
remote: log/slog/internal/buffer
remote: log/slog
remote: golang.org/x/net/internal/httpsfv
remote: golang.org/x/net/idna
remote: net/http/httptrace
remote: github.com/quic-go/quic-go/internal/protocol
remote: net/http/internal/httpcommon
remote: github.com/quic-go/quic-go/internal/utils
remote: net/http
remote: github.com/quic-go/quic-go/internal/qerr
remote: github.com/quic-go/quic-go/internal/wire
remote: github.com/quic-go/quic-go/qlogwriter
remote: github.com/quic-go/quic-go/internal/flowcontrol
remote: golang.org/x/net/http/httpguts
remote: golang.org/x/net/internal/httpcommon
remote: github.com/quic-go/quic-go/qlog
remote: github.com/quic-go/quic-go/internal/congestion
remote: github.com/quic-go/quic-go/internal/handshake
remote: github.com/quic-go/quic-go/internal/ackhandler
remote: github.com/quic-go/quic-go
remote: github.com/quic-go/quic-go/http3/qlog
remote: github.com/gin-contrib/sse
remote: github.com/gin-gonic/gin/internal/fs
remote: net/rpc
remote: golang.org/x/net/http2
remote: github.com/quic-go/quic-go/http3
remote: net/http/httputil
remote: github.com/heroku/x/hmetrics
remote: github.com/heroku/x/hmetrics/onload
remote: github.com/ugorji/go/codec
remote: golang.org/x/net/http2/h2c
remote: github.com/gin-gonic/gin/render
remote: github.com/gin-gonic/gin/binding
remote: github.com/gin-gonic/gin
remote: github.com/heroku/go-getting-started
remote: -----> Installed the following binaries:
remote: ./bin/go-getting-started
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote:
remote: -----> Compressing...
remote: Done: 14.8M
remote: -----> Launching...
remote: Released v3
remote: https://warm-everglades-94026-9f0b6c3a2b32.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/warm-everglades-94026.git
* [new branch] main -> main
Visit the app at the URL shown in the logs. As a shortcut, you can also open the website as follows:
$ heroku open
Understanding the Procfile
Use a Procfile, a text file in the root directory of your application, to explicitly declare what command to execute to start your app.
The Procfile in the example app looks like this:
web: go-getting-started
This Procfile declares a single process type, web, and the command needed to run it. The name web is important here. It declares that this process type is attached to Heroku’s HTTP routing stack and receives web traffic when deployed. The command used here, go-getting-started is the compiled binary of the getting started app. The heroku build process makes this compiled binary available on the $PATH.
A Procfile can contain additional process types. For example, you can declare a background worker process that processes items off a queue.
View Logs
Heroku treats logs as streams of time-ordered events, aggregated from the output streams of all your app and Heroku components. Heroku provides a single stream for all events.
View information about your running app by using one of the logging commands, heroku logs --tail:
$ heroku logs --tail
2026-05-05T22:04:00.738895+00:00 heroku[web.1]: Starting process with command `go-getting-started`
2026-05-05T22:04:01.585599+00:00 app[web.1]: [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
2026-05-05T22:04:01.585611+00:00 app[web.1]: - using env: export GIN_MODE=release
2026-05-05T22:04:01.585611+00:00 app[web.1]: - using code: gin.SetMode(gin.ReleaseMode)
2026-05-05T22:04:01.585611+00:00 app[web.1]:
2026-05-05T22:04:01.585744+00:00 app[web.1]: [GIN-debug] Loaded HTML Templates (4):
2026-05-05T22:04:01.585744+00:00 app[web.1]: -
2026-05-05T22:04:01.585745+00:00 app[web.1]: - header.tmpl.html
2026-05-05T22:04:01.585745+00:00 app[web.1]: - index.tmpl.html
2026-05-05T22:04:01.585745+00:00 app[web.1]: - nav.tmpl.html
2026-05-05T22:04:01.585745+00:00 app[web.1]:
2026-05-05T22:04:01.585758+00:00 app[web.1]: [GIN-debug] GET /static/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (2 handlers)
2026-05-05T22:04:01.585770+00:00 app[web.1]: [GIN-debug] HEAD /static/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (2 handlers)
2026-05-05T22:04:01.585771+00:00 app[web.1]: [GIN-debug] GET / --> main.main.func1 (2 handlers)
2026-05-05T22:04:01.585788+00:00 app[web.1]: [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
2026-05-05T22:04:01.585788+00:00 app[web.1]: Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.
2026-05-05T22:04:01.585788+00:00 app[web.1]: [GIN-debug] Listening and serving HTTP on :16929
2026-05-05T22:04:01.986005+00:00 heroku[web.1]: State changed from starting to up
2026-05-05T22:04:23.576248+00:00 app[web.1]: [GIN] 2026/05/05 - 22:04:23 | 200 | 449.691µs | 66.203.115.14 | GET "/"
2026-05-05T22:04:23.576587+00:00 heroku[router]: at=info method=GET path="/" host=warm-everglades-94026-9f0b6c3a2b32.herokuapp.com request_id=aa714cfa-b725-3b1c-db31-36f4c5556b2d fwd="123.456.789.0" dyno=web.1 connect=0ms service=1ms status=200 bytes=8826 protocol=http1.1 tls=false
To generate more log messages, refresh the app in your browser.
To stop streaming the logs, press Control+C.
Scale the App
After deploying the sample app, it automatically runs on a single web dyno. Think of a dyno as a lightweight container that runs the command specified in the Procfile.
You can check how many dynos are running by using the ps command:
$ heroku ps
=== web (Basic): go-getting-started (1)
web.1: up 2026/05/05 17:04:01 -0500 (~ 26s ago)
Scaling an app on Heroku is equivalent to changing the number of running dynos. Scale the number of web dynos to zero:
$ heroku ps:scale web=0
$ heroku ps:wait
Access the app again by hitting refresh in your browser, or heroku open to open it in a web tab. You get an error message because you no longer have web dynos available to serve requests.
Scale it up again:
$ heroku ps:scale web=1
$ heroku ps:wait
By default, apps use Eco dynos if you’re subscribed to Eco. Otherwise, it defaults to Basic dynos. The Eco dynos plan is shared across all Eco dynos in your account and is recommended if you plan on deploying many small apps to Heroku. Eco dynos sleep if they don’t receive any traffic for half an hour. This sleep behavior causes a few seconds delay for the first request upon waking. Eco dynos consume from a monthly, account-level quota of eco dyno hours. As long as you haven’t exhausted the quota, your apps can continue to run.
To avoid dyno sleeping, upgrade to a Basic or higher dyno type as described in the Dyno Types article. Upgrading to at least Standard dynos allows you to scale up to multiple dynos per process type.
Start a Console
You can run a command, typically scripts and applications that are part of your app, in a one-off dyno using the heroku run command. You can also run an interactive bash session in your app’s environment:
$ heroku run bash
Running bash on warm-everglades-94026... starting, run.6742
Running bash on warm-everglades-94026... connecting, run.6742
Running bash on warm-everglades-94026... up, run.6742
~ $ ls -lah
total 84K
drwx------ 8 u48780 dyno 4.0K May 5 22:04 .
drwxr-xr-x 11 root root 4.0K Apr 22 14:59 ..
-rw------- 1 u48780 dyno 86 May 5 22:03 .env
-rw------- 1 u48780 dyno 213 May 5 22:03 .gitattributes
drwx------ 2 u48780 dyno 4.0K May 5 22:03 .github
-rw------- 1 u48780 dyno 43 May 5 22:03 .gitignore
drwx------ 3 u48780 dyno 4.0K May 5 22:03 .heroku
drwx------ 2 u48780 dyno 4.0K May 5 22:03 .profile.d
-rw------- 1 u48780 dyno 545 May 5 22:03 Dockerfile
-rw------- 1 u48780 dyno 318 May 5 22:03 Makefile
-rw------- 1 u48780 dyno 24 May 5 22:03 Procfile
-rw------- 1 u48780 dyno 2.1K May 5 22:03 README.md
-rw------- 1 u48780 dyno 294 May 5 22:03 app.json
drwx------ 2 u48780 dyno 4.0K May 5 22:03 bin
-rw------- 1 u48780 dyno 1.6K May 5 22:03 go.mod
-rw------- 1 u48780 dyno 7.9K May 5 22:03 go.sum
-rw------- 1 u48780 dyno 164 May 5 22:03 heroku.yml
-rw------- 1 u48780 dyno 466 May 5 22:03 main.go
drwx------ 2 u48780 dyno 4.0K May 5 22:03 static
drwx------ 2 u48780 dyno 4.0K May 5 22:03 templates
~ $
~ $ exit
If you receive an error, Error connecting to process, configure your firewall.
Type exit to exit the shell.
Declare App Dependencies
Heroku recognizes an app as being written in Go by the existence of a go.mod file in the root directory. The demo app you deployed already has a go.mod file, and it looks something like this:
module github.com/heroku/go-getting-started
// +heroku goVersion go1.25
go 1.25.0
require (
github.com/gin-gonic/gin v1.12.0
github.com/heroku/x v0.5.3
)
require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
The go.mod file is used by Go tool and specifies both the dependencies that are required to build your application and the build configuration Heroku should use to compile the application. This Go app has a few dependencies, primarily on Gin, a HTTP web framework.
When an app is deployed, Heroku reads this file, installs an appropriate Go version and compiles your code using go install ..
Run the app locally
Running apps locally in your own dev environment requires a little more effort. Go is a compiled language and you must compile the application and ensure it available on your $PATH.
First, ensure you have $GOPATH/bin on your path:
$ export GOPATH="$HOME/go"
$ export PATH="$GOPATH/bin:$PATH"
These environment variables are commonly used by Go developers. They may be something you want to persist to your environment by
including them your .zshrc, .bash_profile.
Then compile and install the program to your $GOPATH/bin directory.
$ go install -v .
github.com/heroku/go-getting-started
Now start your application locally using the heroku local command, which was installed as part of the Heroku CLI:
$ heroku local web --port 5006
[OKAY] Loaded ENV .env File as KEY=VALUE Format
(node:15547) [DEP0060] DeprecationWarning: The `util._extend` API is deprecated. Please use Object.assign() instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
5:04:40 PM web.1 | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
5:04:40 PM web.1 | - using env: export GIN_MODE=release
5:04:40 PM web.1 | - using code: gin.SetMode(gin.ReleaseMode)
5:04:40 PM web.1 | [GIN-debug] Loaded HTML Templates (4):
5:04:40 PM web.1 | - nav.tmpl.html
5:04:40 PM web.1 | -
5:04:40 PM web.1 | - header.tmpl.html
5:04:40 PM web.1 | - index.tmpl.html
5:04:40 PM web.1 | [GIN-debug] GET /static/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (2 handlers)
5:04:40 PM web.1 | [GIN-debug] HEAD /static/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (2 handlers)
5:04:40 PM web.1 | [GIN-debug] GET / --> main.main.func1 (2 handlers)
5:04:40 PM web.1 | [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
5:04:40 PM web.1 | Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.
5:04:40 PM web.1 | [GIN-debug] Listening and serving HTTP on :5006
On Windows you will need to do two things before being able to run `heroku local`:
- Run `go build -o bin/go-getting-started.exe -v` instead of the command listed above.
- Alter Procfile so it's contents are: `web: bin\go-getting-started.exe` instead of what is in the checkout. Don't commit changes to Procfile though, otherwise your application's web process won't be able to start on Heroku.
Just like Heroku, heroku local examines the Procfile to determine what to run.
Open http://localhost:5006 with your web browser. You should see your app running locally.
To stop the app from running locally, go back to your terminal window and press Ctrl+C to exit.
Provision a Database
The sample app requires a database. Provision a Heroku Postgres database, an add-on available through the Elements Marketplace. Add-ons are cloud services that provide out-of-the-box additional services for your application, such as logging, monitoring, databases, and more.
An essential-0 Postgres size costs $5 a month, prorated to the minute. At the end of this tutorial, we prompt you to delete your database to minimize costs.
$ heroku addons:create heroku-postgresql:essential-0
Creating heroku-postgresql:essential-0 on warm-everglades-94026... ~$0.007/hour (max $5/month)
Database should be available soon
postgresql-flat-32291 is being created in the background. The app will restart when complete...
Run heroku addons:info postgresql-flat-32291 to check creation progress.
Run heroku addons:docs heroku-postgresql to view documentation.
You can wait for the database to provision by running this command:
$ heroku pg:wait
After that command exits, your Heroku app can access the Postgres database. The DATABASE_URL environment variable stores your credentials, which your app is configured to connect to. You can see all the add-ons provisioned with the addons command:
$ heroku addons
Add-on Plan Price Max Price State
──────────────────────────────────────────────────────────────────────────────────────────────
heroku-postgresql (postgresql-flat-32291) essential-0 ~$0.007/hour $5/month created
└─ as DATABASE
The table above shows add-ons and the attachments to the current app (warm-everglades-94026) or other apps.
Push Local Changes
In this step you’ll learn how to propagate a local change to the application through to Heroku. As an example, you’ll modify the application to add an additional dependency and the code to use it.
Dependencies are managed with the Go tool.
Let’s add a route to the application that will use the database we just created.
We’ll need the pq library to interact with the database. Since this
dependency is not already used by your application we need to tell go to fetch
a copy of the dependency:
$ go get github.com/lib/pq@v1
go: added github.com/lib/pq v1.12.3
This does 3 things:
- Downloads
v1of the pg module and any of it’s dependencies to the module cache. - Records the pg dependency, and its dependencies in
go.mod. - Records a cryptographic sum of pg and it’s dependencies in
go.sum
After that let’s introduce a new route, /db, which will track timestamps of
requests to that endpoint. Modify main.go so that it uses pf by adding
"github.com/lib/pq@v1" to the list of imports.
In file main.go, on line 8 add:
_ "github.com/lib/pq"
Now let’s initialize a connection to the database which is defined by the
$DATABASE_URL environment variable.
In file main.go, on line 19 add:
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("Error opening database: %q", err)
}
Now add a dbFunc to the app.
At the end of main.go add:
func dbFunc(db *sql.DB) gin.HandlerFunc {
return func(c *gin.Context) {
if _, err := db.Exec("CREATE TABLE IF NOT EXISTS ticks (tick timestamp)"); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error creating database table: %q", err))
return
}
if _, err := db.Exec("INSERT INTO ticks VALUES (now())"); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error incrementing tick: %q", err))
return
}
rows, err := db.Query("SELECT tick FROM ticks")
if err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error reading ticks: %q", err))
return
}
defer rows.Close()
for rows.Next() {
var tick time.Time
if err := rows.Scan(&tick); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error scanning ticks: %q", err))
return
}
c.String(http.StatusOK, fmt.Sprintf("Read from DB: %s\n", tick.String()))
}
}
}
Then register the new function with the router.
In file main.go, on line 27 add:
router.GET("/db", dbFunc(db))
The additional code requires new packages from Go’s standard library.
You can import them automatically with goimports. Install goimports if you
haven’t already with:
$ go install golang.org/x/tools/cmd/goimports@latest
Then add the new imports to main.go:
$ goimports -w main.go
main.go should now look like this:
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
_ "github.com/heroku/x/hmetrics/onload"
_ "github.com/lib/pq"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
log.Fatal("$PORT must be set")
}
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("Error opening database: %q", err)
}
router := gin.New()
router.Use(gin.Logger())
router.GET("/db", dbFunc(db))
router.LoadHTMLGlob("templates/*.tmpl.html")
router.Static("/static", "static")
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl.html", nil)
})
router.Run(":" + port)
}
func dbFunc(db *sql.DB) gin.HandlerFunc {
return func(c *gin.Context) {
if _, err := db.Exec("CREATE TABLE IF NOT EXISTS ticks (tick timestamp)"); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error creating database table: %q", err))
return
}
if _, err := db.Exec("INSERT INTO ticks VALUES (now())"); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error incrementing tick: %q", err))
return
}
rows, err := db.Query("SELECT tick FROM ticks")
if err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error reading ticks: %q", err))
return
}
defer rows.Close()
for rows.Next() {
var tick time.Time
if err := rows.Scan(&tick); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error scanning ticks: %q", err))
return
}
c.String(http.StatusOK, fmt.Sprintf("Read from DB: %s\n", tick.String()))
}
}
}
Next, let’s add the new dependency (pq) to the vendor folder. This will allow
go install to find the dependencies during builds both locally and during
remote builds. Vendoring also ensures that builds are repeatable and resistant
to erosion.
$ go mod vendor
In order to test the new code locally, we’ll want a local postgres database. If you don’t have postgres installed locally, follow these instructions. Then create a database for use with our app:
$ createdb go-getting-started
Now test the local changes using the local database:
$ go install -v .
github.com/gin-gonic/gin/internal/bytesconv
github.com/go-playground/locales/currency
github.com/pelletier/go-toml/v2/internal/characters
github.com/leodido/go-urn/scim/schema
go.mongodb.org/mongo-driver/v2/internal/bsoncoreutil
google.golang.org/protobuf/internal/flags
golang.org/x/crypto/internal/alias
github.com/quic-go/quic-go/internal/utils/ringbuffer
golang.org/x/net/internal/iana
github.com/lib/pq/internal/pqsql
github.com/lib/pq/oid
github.com/lib/pq/pqerror
google.golang.org/protobuf/internal/pragma
github.com/quic-go/quic-go/internal/utils/linkedlist
golang.org/x/text/internal/tag
github.com/gabriel-vasile/mimetype/internal/json
golang.org/x/text/transform
go.mongodb.org/mongo-driver/v2/internal/decimal128
github.com/gabriel-vasile/mimetype/internal/scan
go.mongodb.org/mongo-driver/v2/internal/binaryutil
github.com/quic-go/quic-go/internal/monotime
golang.org/x/crypto/internal/poly1305
golang.org/x/net/internal/httpsfv
github.com/go-playground/locales
golang.org/x/sys/cpu
google.golang.org/protobuf/internal/detrand
github.com/gabriel-vasile/mimetype/internal/markup
github.com/gin-gonic/gin/codec/json
github.com/leodido/go-urn
github.com/gabriel-vasile/mimetype/internal/csv
github.com/goccy/go-yaml/token
golang.org/x/text/internal/language
github.com/go-playground/universal-translator
github.com/pelletier/go-toml/v2/internal/danger
github.com/gabriel-vasile/mimetype/internal/charset
google.golang.org/protobuf/internal/errors
go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore
golang.org/x/sys/unix
github.com/gin-contrib/sse
github.com/pelletier/go-toml/v2/unstable
golang.org/x/crypto/sha3
github.com/gabriel-vasile/mimetype/internal/magic
github.com/goccy/go-yaml/ast
github.com/goccy/go-yaml/scanner
github.com/ugorji/go/codec
google.golang.org/protobuf/encoding/protowire
golang.org/x/text/internal/language/compact
github.com/gin-gonic/gin/internal/fs
github.com/pelletier/go-toml/v2/internal/tracker
golang.org/x/net/http2/hpack
github.com/quic-go/quic-go/quicvarint
golang.org/x/text/language
go.mongodb.org/mongo-driver/v2/bson
github.com/gabriel-vasile/mimetype
google.golang.org/protobuf/reflect/protoreflect
github.com/quic-go/quic-go/qlogwriter/jsontext
github.com/pelletier/go-toml/v2
github.com/goccy/go-yaml/lexer
github.com/quic-go/qpack
github.com/quic-go/quic-go/internal/protocol
github.com/goccy/go-yaml/printer
github.com/goccy/go-yaml/internal/format
golang.org/x/crypto/chacha20
golang.org/x/crypto/hkdf
github.com/goccy/go-yaml/internal/errors
github.com/quic-go/quic-go/internal/utils
github.com/quic-go/quic-go/internal/qerr
github.com/quic-go/quic-go/qlogwriter
golang.org/x/net/bpf
google.golang.org/protobuf/internal/encoding/messageset
github.com/goccy/go-yaml/parser
google.golang.org/protobuf/internal/genid
google.golang.org/protobuf/internal/order
google.golang.org/protobuf/internal/strs
google.golang.org/protobuf/runtime/protoiface
google.golang.org/protobuf/reflect/protoregistry
github.com/quic-go/quic-go/internal/wire
github.com/quic-go/quic-go/internal/flowcontrol
golang.org/x/crypto/chacha20poly1305
github.com/mattn/go-isatty
golang.org/x/net/internal/socket
github.com/go-playground/validator/v10
golang.org/x/text/unicode/bidi
golang.org/x/text/unicode/norm
github.com/heroku/x/hmetrics
github.com/lib/pq/internal/pqutil
google.golang.org/protobuf/proto
github.com/quic-go/quic-go/qlog
github.com/lib/pq/internal/pqtime
github.com/heroku/x/hmetrics/onload
golang.org/x/net/ipv4
golang.org/x/net/ipv6
github.com/lib/pq/internal/pgpass
github.com/goccy/go-yaml
github.com/lib/pq/internal/pgservice
golang.org/x/text/secure/bidirule
github.com/lib/pq/internal/proto
github.com/lib/pq/scram
github.com/quic-go/quic-go/internal/congestion
github.com/quic-go/quic-go/internal/handshake
github.com/lib/pq
github.com/quic-go/quic-go/internal/ackhandler
golang.org/x/net/idna
golang.org/x/net/http/httpguts
github.com/quic-go/quic-go
golang.org/x/net/internal/httpcommon
golang.org/x/net/http2
golang.org/x/net/http2/h2c
github.com/quic-go/quic-go/http3/qlog
github.com/quic-go/quic-go/http3
github.com/gin-gonic/gin/render
github.com/gin-gonic/gin/binding
github.com/gin-gonic/gin
github.com/heroku/go-getting-started
$ heroku local --port=5006
Visit your application at http://localhost:5006/db. If your changes worked, you see a log of timestamp. Refreshing the page should add more timestamps to the log.
Now deploy this local change to Heroku.
Almost every deploy to Heroku follows this same pattern. First, add the modified files to the local Git repository:
$ git add .
Now commit the changes to the repository:
$ git commit -m "Added db route"
[main ce8457f] Added db route
3 files changed, 47 insertions(+)
Now deploy as before:
$ git push heroku main
...
Finally, check that everything is working:
$ heroku open db
Provision a Logging Add-on
Beyond databases, add-ons provide many additional services for your application. In this step, you provision a free add-on to store your app’s logs.
By default, Heroku stores 1500 lines of logs from your application, but the full log stream is available as a service. Several add-on providers have logging services that provide things such as log persistence, search, and email and SMS alerts.
In this step, you provision one of these logging add-ons, Papertrail.
Provision the Papertrail logging add-on:
$ heroku addons:create papertrail
Creating papertrail on warm-everglades-94026... free
Provisioning has been successful
Created papertrail-rigid-56097
Run heroku addons:docs papertrail to view documentation.
The add-on is now deployed and configured for your app. You can list add-ons for your app with this command:
$ heroku addons
Add-on Plan Price Max Price State
──────────────────────────────────────────────────────────────────────────────────────────────
heroku-postgresql (postgresql-flat-32291) essential-0 ~$0.007/hour $5/month created
└─ as DATABASE
papertrail (papertrail-rigid-56097) choklad free free created
└─ as PAPERTRAIL
The table above shows add-ons and the attachments to the current app (warm-everglades-94026) or other apps.
To see this particular add-on in action, visit your application’s Heroku URL a few times. Each visit generates more log messages, which get routed to the Papertrail add-on. Visit the Papertrail console to see the log messages:
$ heroku addons:open papertrail
Your browser opens up a Papertrail web console, showing the latest log events. The interface lets you search and set up alerts.
Define Config Vars
Heroku lets you externalize configuration, storing data such as encryption keys or external resource addresses in config vars.
At runtime, config vars are exposed as environment variables to the application.
Your application is already reading one config var, the $PORT config var.
$PORT is automatically set by Heroku on web dynos.
Let’s explore how to use user-set config vars in your Go application.
Modify main.go and add a repeatHandler function that returns
Hello From Go! the number of times specified by the value of the REPEAT
environment variable.
At the end of main.go add:
func repeatHandler(r int) gin.HandlerFunc {
return func(c *gin.Context) {
var buffer bytes.Buffer
for i := 0; i < r; i++ {
buffer.WriteString("Hello from Go!\n")
}
c.String(http.StatusOK, buffer.String())
}
}
Next, let’s read the $REPEAT environment variable.
In file main.go, on line 27 add:
tStr := os.Getenv("REPEAT")
repeat, err := strconv.Atoi(tStr)
if err != nil {
log.Printf("Error converting $REPEAT to an int: %q - Using default\n", err)
repeat = 5
}
Now register the handler function with the router.
In file main.go, on line 37 add:
router.GET("/repeat", repeatHandler(repeat))
Now run goimports to automatically bring in the standard library packages
that are now in use:
$ goimports -w main.go
The updated main.go will look like this:
package main
import (
"bytes"
"database/sql"
"fmt"
"log"
"net/http"
"os"
"strconv"
"time"
"github.com/gin-gonic/gin"
_ "github.com/heroku/x/hmetrics/onload"
_ "github.com/lib/pq"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
log.Fatal("$PORT must be set")
}
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("Error opening database: %q", err)
}
tStr := os.Getenv("REPEAT")
repeat, err := strconv.Atoi(tStr)
if err != nil {
log.Printf("Error converting $REPEAT to an int: %q - Using default\n", err)
repeat = 5
}
router := gin.New()
router.Use(gin.Logger())
router.GET("/repeat", repeatHandler(repeat))
router.GET("/db", dbFunc(db))
router.LoadHTMLGlob("templates/*.tmpl.html")
router.Static("/static", "static")
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl.html", nil)
})
router.Run(":" + port)
}
func dbFunc(db *sql.DB) gin.HandlerFunc {
return func(c *gin.Context) {
if _, err := db.Exec("CREATE TABLE IF NOT EXISTS ticks (tick timestamp)"); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error creating database table: %q", err))
return
}
if _, err := db.Exec("INSERT INTO ticks VALUES (now())"); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error incrementing tick: %q", err))
return
}
rows, err := db.Query("SELECT tick FROM ticks")
if err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error reading ticks: %q", err))
return
}
defer rows.Close()
for rows.Next() {
var tick time.Time
if err := rows.Scan(&tick); err != nil {
c.String(http.StatusInternalServerError,
fmt.Sprintf("Error scanning ticks: %q", err))
return
}
c.String(http.StatusOK, fmt.Sprintf("Read from DB: %s\n", tick.String()))
}
}
}
func repeatHandler(r int) gin.HandlerFunc {
return func(c *gin.Context) {
var buffer bytes.Buffer
for i := 0; i < r; i++ {
buffer.WriteString("Hello from Go!\n")
}
c.String(http.StatusOK, buffer.String())
}
}
The heroku local command automatically sets up the environment based on the
contents of the .env file in your local directory. In the top level directory
of your sample project, there’s already a .env file that contains:
REPEAT=10
Recompile the app and run it:
$ go install -v .
github.com/heroku/go-getting-started
$ heroku local --port=5006
When you access the /repeat route on the app at http://localhost:5006/repeat
you’ll see “Hello From Go!” ten times.
To set the config var on Heroku, execute the following:
$ heroku config:set REPEAT=10
Setting REPEAT and restarting warm-everglades-94026... done, v5
REPEAT: 10
View the app’s config vars using heroku config:
$ heroku config | grep REPEAT
REPEAT: 10
Deploy the changes to heroku using what you learned, and try it out by visiting
the /repeat handler of your application:
$ git add .
$ git commit -m "Added configurable repeat"
$ git push heroku main
$ heroku open repeat
Delete Your App
Remove the app from your account. We only charge you for the resources you used.
This action permanently deletes your application and any add-ons attached to it.
$ heroku apps:destroy
You can confirm that your app is gone with this command:
$ heroku apps --all
Next Steps
You now know how to configure and deploy a Go app, view logs, and start a console.
To learn more, see: