A basic example is much similar to the example we have already. It’s a bit of a pity when we are trying to create content, because Runway is really that simple and does not require a ton of custom configuration or libraries.
Runway’s builder uses cloud native buildpacks and autodetects Golang applications via the e.g. go.mod
or main.go
files in the root of your repository and attempts to build an application by running go mod vendor
and go build
.
An application for Runway is typically one where you receive (HTTP) traffic. So a port is critical. By default, we use 5000
as a default port, so in your case, you have to make sure to respect the environment variable or explicitly override it.
Normally, you would use runway app config set ...
to add configuration to your application’s environment. But the PORT
variable is always set, and you don’t need to do anything here!
Using the (default) PORT
environment variable:
package main
import (
"fmt"
"net/http"
"os"
)
func hello(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "hello from Runway!\n")
}
func main() {
port := ":8484"
if os.Getenv("PORT") != "" {
port = ":" + os.Getenv("PORT")
}
http.HandleFunc("/", hello)
http.ListenAndServe(port, nil)
}
The code is hopefully straightforward — we use the PORT
environment variable that Runway gives us, and default to something else if it is not set.
If you’d like to choose your own port number — tell us what you want, e.g. feeling 1337
— you can also just explicitly set PORT
, and hard-code the same port in your application and remove a couple lines of code:
runway app config set PORT=1337
Runway will now happily route all traffic to your application running on port 1337
!
1-1023
are prohibited, as they require a
privileged (root
) user and we don’t support that.Runway is a platform — we’re the cloud. In the cloud you log to stdout or stderr. These get collected on our platform and then you can run runway app logs
or use the Runway UI to see what’s going on.
A straightforward example to enable logging in Golang shows the following — using the outstanding slog
package. But of course logrus
, zerolog
etc. work as well.
package main
import (
"fmt"
"log/slog"
"net/http"
"os"
)
func hello(w http.ResponseWriter, req *http.Request) {
slog.Info("request", "method", req.Method, "url", req.URL.String())
fmt.Fprintf(w, "hello from Runway!\n")
}
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
slog.SetDefault(logger)
port := ":8484"
if os.Getenv("PORT") != "" {
port = ":" + os.Getenv("PORT")
}
slog.Info("will use", "port", port)
http.HandleFunc("/", hello)
err := http.ListenAndServe(port, nil)
if err != nil {
slog.Error("can't listen", "error", err)
}
}
Project setup is a bit of a controversial topic in the Golang ecosystem — but we’re not judging either way.
If you for example follow the cmd
pattern and your application’s entrypoint is not in the root of your repository, then you can set a variable to tell our builder what to look for.
$ tree
.
├── cmd
│ └── server
│ └── main.go
└── go.mod
runway app config set BP_GO_TARGETS=./cmd/server
[build]
[[build.env]]
name = "BP_GO_TARGETS"
value = "./cmd/server"
project.toml
file the root of your repository.If you build multiple targets, please check out the guide on monorepos and multi-process. Otherwise: the first target in BP_GO_TARGETS
becomes the default process and is started by Runway.
Another alternative to the cmd
pattern is to use Golang’s flag package or a library such as cobra or (our favorite) urfave/cli.
This allows you to stack all features into one binary and you would invoke it with myapp --server
or binary cli
etc.
A nice benefit of including everything in one binary, is of course a slightly faster build - YMMV! But you should prioritize structure and readable code over everything else.
Check out the guide on faster builds to see a complete example.