Runway
Runway is built and operated in the EU!

Basic

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.

Port

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.

Environment

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:

main.go
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.

Use your own port

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!

Logging

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.

main.go
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)
	}
}

cmd pattern

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 CLI
runway app config set BP_GO_TARGETS=./cmd/server
project.toml
[build]
  [[build.env]]
    name = "BP_GO_TARGETS"
    value = "./cmd/server"

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.

One binary (to rule them all)

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.