This tutorial will go over the steps needed in order to get a Yesod application deployed to Heroku, connecting to a database, and ensuring all requests are forced to use the SSL protocol.
We are going to assume two things in this tutorial:
- You have used Heroku before to deploy web applications.
- You have a Yesod application created with the
yesod-postgrestemplate.
The first thing we are going to want to do is to add a buildpack for Haskell. I have had a good experience with the heroku-buildpback-stack.
You can add a buildpack via Heroku's web UI, or on the command line:
heroku buildpacks:set https://github.com/mfine/heroku-buildpack-stackThe port that a Heroku application must bind to is set
dynamically by Heroku,
but can be accessed via the PORT environmental variable.
Yesod already has a mechanism for retrieving the port to bind to from the
environment, but it looks for the YESOD_PORT environmental variable.
What we need to do, then is to set YESOD_PORT equal to the value of PORT
before the application is started. All we have to do is add a .profile file to
the root of our Yesod project:
# .profile
export YESOD_PORT=$PORT
Adding a database happens in two steps.
First, we need to add the "addon" to our application:
heroku addons:create heroku-postgresql:hobby-dev
Next, we need to instruct Yesod to connect to the database server that Heroku has provisioned for us.
If you run heroku config, you will see all the environmental variables in our
environment. We are only interested in DATABASE_URL, which will be a Postgres
connection string in the following format:
postgres://<user>:<pass>@<host>:<port>/<db>.
If you take a look at ./config/settings.yml, you can see that Yesod looks for
specific environmental variables. Let's set these on Heroku so that Yesod will
pick them up:
heroku config:set YESOD_PGUSER=<user> YESOD_PGPASS=<pass> YESOD_PGHOST=<host> YESOD_PGDATABASE=<database>
A common requirement in production applications is to force all connections to an application to be made via SSL / the HTTPS protocol.
While some of that can be done at the DNS level, it is the responsibility of the application layer to do that.
Luckily, Yesod is built on WAI, and WAI provides the forceSSL that will do exactly that.
We can modify makeApplication function, and simply include the middleware.
(Make sure to import forceSSL).
-- src/Application.hs
makeApplication :: App -> IO Application
makeApplication foundation = do
logWare <- makeLogWare foundation
-- Create the WAI application and apply middlewares
appPlain <- toWaiAppPlain foundation
return $ forceSSL $ logWare $ defaultMiddlewaresNoLogging appPlainHowever, what if we only wanted to force SSL in production? Well, notice how
makeApplication is called with a value of type App, and App is created in
the makeFoundation functions. We can add a new field to App, initialize that
field in makeFoundation, and then only use forceSSL if that field's value is
Production.
-- src/Foundation.hs
data Environment = Production | Development deriving (Read)
data App = App
{ appSettings :: AppSettings
, appStatic :: Static -- ^ Settings for static file serving.
, appConnPool :: ConnectionPool -- ^ Database connection pool.
, appHttpManager :: Manager
, appLogger :: Logger
, environment :: Environment -- ^ This is our new field
}
-- src/Application.hs
-- In the makeFoundation, we want to make sure that our environment field is
-- initialized
makeFoundation :: AppSettings -> IO App
makeFoundation appSettings = do
--
menv <- readMay <$> getEnv "YESOD_ENVIRONMENT"
let environment = maybe Development id menv -- default to Development
--
-- Finally, we switch forceSSL on or off based on the value of the environmen
-- field of our App value.
makeApplication :: App -> IO Application
makeApplication foundation = do
logWare <- makeLogWare foundation
let sslWare = makeSSLWare foundation
-- Create the WAI application and apply middlewares
appPlain <- toWaiAppPlain foundation
return $ sslWare $ logWare $ defaultMiddlewaresNoLogging appPlain
makeSSLWare :: App -> Middleware
makeSSLWare App{..} = case environment of
Production -> forceSSL
Development -> idNow, we need to make sure that whenever we start our application we have the
YESOD_ENVIRONMENT set to either Production or Development.
Note: If the above works for your sensibilities, don even bother reading the below.
There are a few different ways that we could have gone about conditionally
turning on forceSSL.
Since makeApplication is already an IO action, we could have read the
environment within makeApplication. I like to keep the IO closer to the top
layers of the system, so I didn't choose this route.
Another option is to add a field to the AppSettings data type, and use the
dev variable that is set to True or False depending on how the system was
started. If we went this route, we'd have to change another part of the system
so that the application would be started in such a way that dev would be defined
as true when I deployed to my staging server in Heroku.
Finally, we could have used the settings.yml instead, since we are already
using an environmental variable named YESOD_. I think this is a legitimate
route, but I didn't think about it until later. I'll update this guide if I ever
get around to it.
Point is, the suggested route is "good enough".