usethetypes#7: Configuring the Web Server Port

We’re are gradually building up our app so that we can deploy it to a hosting provider such as Heroku. Providers like Heroku do not guarantee that the app will always be able to listen on the same port. In this short video, we’ll modify our web app so that the web server port can be configured via an environment variable.

Step 1: Grab our web app

Now we’re going to get our little Haskell app’s source code and create a new repo:

cd $HOME/src
mkdir snap-hello-world
cd snap-hello-world
wget -O - | tar xvz --strip-components=1
git init
git add .

Step 2: Check the current behaviour

Let’s run our app to check its current behaviour:

stack build --exec snap-hello-world

We can see that it listens on port 8000 by default. Make a mental note of this.

Step 2: Make the port configurable

Heroku passes the web server port to the executable using the PORT environment variable. We’ll modify our program to read the port from this environment variable or default to port 8000 if this environment variable is not defined.

To do this we’ll use lookupEnv from System.Environment. We can view a function’s type signature from GHCi which we can start up as follows:

stack ghci

From GHCi, you can then view the type signature as follows:

:type System.Environment.lookupEnv

This function evaluates to an I/O action that will return Nothing if the environment variable is not defined. We can use the following trickto view its type signature without running GHCi interactively:

stack ghci <<< ':t System.Environment.lookupEnv'

In future videos, we’ll revisit this and other ways to look up type signatures.

We can then combine lookupEnv with the maybe function to provide a default value:

stack ghci <<< ':t maybe'

This takes a default value and a function to apply to the Maybe value if it is not Nothing. We can string these together to get a port with a default value. We can then use httpServe instead of quickHttpServe to allow us to pass in our configuration created using the setPort function. Let’s start VSCode and open an integrated terminal and update our program:

stack exec ghcid

{-# LANGUAGE OverloadedStrings #-}

module Main (main) where

import Control.Applicative ((<|>))
import Snap.Core (ifTop, route, writeText)
import Snap.Http.Server (httpServe, setPort)
import Snap.Util.FileServe (serveDirectory, serveFile)
import System.Environment (lookupEnv)

main :: IO ()
main = do
    mbPort <- lookupEnv "PORT"
    let port = maybe 8000 read mbPort
        config = setPort port mempty
    httpServe config $
        ifTop (serveFile "views/index.html")
        <|> route
            [ ("/static", serveDirectory "static")

Step 3: Demonstrate the default behaviour

stack build --exec snap-hello-world

Step 4: Demonstrate the PORT environment variable

PORT=8888 stack build --exec snap-hello-world

We can visit this in our browser to confirm that it’s listening on the correct port: http://localhost:8888.