My pizza service analogy works well as an example as it covers showing pages, posting forms, and reading changed data. And I have over time written several howtos that use the pizza service analogy, the Play framework, and hosting with various cloud providers:
You do need Java JDK v8 installed. Scala however can be installed but the build tool below will download a Scala SDK instance so not neccessary. You can use OpenJDK or Oracle's own JDK version.
For macOS go to Oracle's JDK page, choose the latest JDK8 version, accept the license, download and run the dmg file.
For Ubuntu users install Oracle JDK8 via apt-get:
sudo add-apt-repository ppa:webupd8team/java;
sudo apt-get update;
sudo apt-get install oracle-java8-installer;
sudo apt-get install oracle-java8-set-default
For macOS use Homebrew:
brew cask install java
On Ubuntu v14.04 & v12.04, you need to add a PPA, for v14.10 and later its in the standard repositories.
sudo add-apt-repository ppa:openjdk-r/ppa;
sudo apt-get update;
Then for all Ubuntu versions:
sudo apt-get install openjdk-8-jdk
Then also on Ubuntu run and choose JDK 8 for both of these:
sudo update-alternatives --config java
sudo update-alternatives --config javac
Download and install Activator, Lightbend's (né Typesafe) extension of SBT, the near default Scala build tool. Activator includes a UI which you do not need, but does include the handy new command to scaffold Play and Akka applications.
Activator comes in two flavours: full and mini. Full includes common dependencies for Play and Akka. Full will save you a little time initially, but soon the dependencies versions will move along and you have to download them all like mini via SBT's use of Ivy anyway.
On macOS:
brew install typesafe-activator
On Linux download and extract the Zip:
wget
https://downloads.typesafe.com/typesafe-activator/1.3.12/typesafe-activator-1.3.12-minimal.zip;
unzip typesafe-activator-1.3.12-minimal.zip;
sudo mv activator-1.3.12-minimal /opt/activator
then add /opt/activator/bin to your $PATH environment variable.
I recommend also installing SBT, as 99% of the time you don't need the extra cruft of Activator. There are other Scala build tools such as Maven and Gradle, but there is little reason to use those with Scala and again you will painfully swim against the current of 99% of projects and companies that defaults to SBT.
On macOS:
brew install sbt
Or on Ubuntu/debian install SBT via apt-get.
echo "deb https://dl.bintray.com/sbt/debian /" | \
sudo tee -a /etc/apt/sources.list.d/sbt.list;
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 \
--recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823;
sudo apt-get update;
sudo apt-get install sbt
Since v2.4 Play has used dependency injection, which is the most significant difference in my pizza ordering application compared to my previous iterations. Play's choice of run time DI with Guice goes against my strong preference for compile time DI, but it is futile to swim against the current.
Create the new application:
activator new pizzeria
Choose option 6, the play-scala option.
You now have a basic play application folder structure inside pizzeria:
Since we used the mini flavour of Activator we need to download all the dependencies.
Launch Activator or SBT (and make some tea):
sbt
Compile and test the code (drink the tea and fetch some biscuits)
;compile;test
Then run the application (and make some more tea)
run
Thankfully you will not need to download the internet from now on, as your local ~/.ivy2 folder is now enormous.
Since we haven't changed anything this should all work, and the application will now be available on the default port of 9000.
curl localhost:9000
Press ctrl+d to stop. Then exit SBT:
exit
You now have a working scaffold. Note a shortcut to run the app:
sbt run
On macOS you can install Docker via Brew, which I used to do. But the "new" Docker for Mac is a better choice. Download and run the dmg file. Launch the Docker app via Launchpad.
For Linux it depends on the distrobution, for Ubuntu 16.04:
sudo apt-get install apt-transport-https ca-certificates;
sudo apt-key adv \
--keyserver hkp://ha.pool.sks-keyservers.net:80 \
--recv-keys 58118E89F3A912897C070ADBF76221572C52609D;
echo "deb https://apt.dockerproject.org/repo ubuntu-xenial main" | \
sudo tee /etc/apt/sources.list.d/docker.list;
sudo apt-get update;
sudo apt-get install linux-image-extra-$(uname -r) \
linux-image-extra-virtual;
sudo apt-get install docker-engine;
sudo service docker start
You have a few options of how you build your images. Mostly due to relatively slowness of downloading all Ivy dependencies from scratch on every build.
You can build the jar file outside of Docker. Then just add the jar or target/stage folder to the Dockerfile. This is result in very fast Docker image builds, but adds another step that is just as slow before it.
For that you either build manually in the project folder via sbt package or sbt stage (for Play apps).
Or via a build/continuous integration server that uploads it to a Maven/Ivy repository.
Or you build it inside Docker.
Either on every build, this is the least complicated method but also very, very slow. This is the method we will use in this document.
Or you depend on a base image with all the dependencies preload in ~/.ivy2. Similar to my play-framework-base. You then extend your Dockerfile from this base image. Note the versions of the dependencies in the base and your project must match for it to be of use.
Create a Dockerfile in the root folder. This will use my my play-framework image, so the default dependencies for Play is already part of the image but not any other dependencies.
FROM flurdy/play-framework:2.5.10-alpine
MAINTAINER Ivar Abrahamsen <@flurdy>
COPY conf /etc/app/
ADD . /opt/build/
WORKDIR /opt/build
RUN /opt/activator/bin/activator clean stage && \
rm -f target/universal/stage/bin/*.bat && \
mv target/universal/stage/bin/* target/universal/stage/bin/app && \
mv target/universal /opt/app && \
ln -s /opt/app/stage/logs /var/log/app && \
rm -rf /opt/build && \
rm -rf /root/.ivy2
WORKDIR /opt/app
ADD . /opt/build/
ENTRYPOINT ["/opt/app/stage/bin/app"]
EXPOSE 9000
Build the image and name it pizzeria:
docker build -t pizzeria .
This will take a while. You will need to get more than a cup of tea.
Run the new image and map the port to 9020.
docker run -ti --rm -p 9000:9020 pizzeria:latest
In another terminal window:
curl localhost:9020
Lets create the actual pizzeria service. We will create a normal landing page that in our case shows the pizza menu and order form. We will have a POST endpoint to send our order to, and a confirmation page to confirm our pizza order has been received.
Though first lets remove some parts we are not using in this application. (of course in your future applications you may use these, so be aware of them)
rm -f app/controllers/AsyncController.scala;
rm -f app/controllers/CountController.scala;
rm -rf app/filters app/services;
rm -f app/Filters.scala app/Module.scala;
rm -rf bin libexec;
rm -rf test
Note: There is an example pizzeria project on my Github.
In the project's build.sbt file lets remove some dependencies we won't need in this example (cache, ws, scalatest), keeping jdbc as we will need that later. And also add a hack with devnull to support docker restarts.
vi build.sbt
name := """pizzeria"""
version := "1.0-SNAPSHOT"
lazy val root = (project in file(".")).enablePlugins(PlayScala)
scalaVersion := "2.11.8"
libraryDependencies ++= Seq(
jdbc
)
javaOptions in Universal ++= Seq(
"-Dpidfile.path=/dev/null"
)
Lets edit the routes file. We will remove the references to Async, Home and CountControllers. And add a PizzaController. Open conf/routes in your editor of choice.
vi conf/routes
# Routes
# ~~~~
GET /
controllers.PizzaController.showMenu
GET /assets/*file
controllers.Assets.versioned(path="/public", file: Asset)
You can cheat a little by just renaming HomeController to PizzaController, then editing it.
mv -f app/controllers/HomeController.scala \
app/controllers/PizzaController.scala;
vi app/controllers/PizzaController.scala
Lets rename the class, and then rename the index function to showMenu, and return the pizzamenu template.
package controllers
import javax.inject._
import play.api._
import play.api.mvc._
@Singleton
class PizzaController @Inject() () extends Controller {
def showMenu = Action {
Ok(views.html.pizzamenu)
}
}
Again we can cheat by renaming the index template, and removing some cruft.
mv -f app/views/index.scala.html app/views/pizzamenu.scala.html;
vi app/views/pizzamenu.scala.html
@()
@main("Pizza Menu") {
<h1>Pizza Menu</h1>
}
Running the app, and testing it in another terminal or browser should display the "Pizza Menu" headline.
sbt run
curl localhost:9000
Ok that is nice, but there is still no actual pizzas shown! Lets create the domain object:
mkdir app/models;
touch app/models/pizza.scala;
vi app/models/pizza.scala
package models
case class Pizza(name: String)
And a repository lookup, this could query a database, or use some connector/adapter to talk to some middleware or backend services.
mkdir app/repositories;
touch app/repositories/PizzaRepository.scala;
vi app/repositories/PizzaRepository.scala
package repositories
import com.google.inject.ImplementedBy
import javax.inject.Inject
import play.api.libs.concurrent.Execution.Implicits._
import scala.concurrent.Future
import models._
@ImplementedBy(classOf[DefaultPizzaRepository])
trait PizzaRepository {
def findPizzas() = Future {
List( Pizza("Hawaii"), Pizza("Pepperoni") )
}
}
class DefaultPizzaRepository @Inject() () extends PizzaRepository
The ImplementedBy and Inject is the additional cruft required by the Guice runtime dependency injection. Lets inject the repository into the pizza controller:
vi app/controllers/PizzaController.scala
package controllers
import javax.inject._
import play.api._
import play.api.libs.concurrent.Execution.Implicits._
import play.api.mvc._
import repositories._
@Singleton
class PizzaController @Inject() (val pizzaRepository: PizzaRepository)
extends Controller {
def showMenu = Action.async {
pizzaRepository.findPizzas().map{ pizzas =>
Ok(views.html.pizzamenu(pizzas))
}
}
}
You will notice the .async postfix to the Action. This means this will return a future of result, so not blocking, and is because the call to the repository returns a future as well.
We need to modify the pizza menu view as we now pass in a list of pizzas.
vi app/views/pizzamenu.scala.html
@(pizzas: List[Pizza])
@main("Pizza Menu") {
<h1>Pizza Menu</h1>
<ul>
@for(pizza <- pizzas){
Pizza: @pizza.name
}
</ul>
}
This should now list the pizzas in the application:
sbt run
curl localhost:9000
Now lets order the pizza!
First lets modify the menu view.
vi app/views/pizzamenu.scala.html
@(pizzas: List[Pizza])
@import helper._
@main("Pizza Menu") {
<h1>Pizza Menu</h1>
<ul>
@for(pizza <- pizzas){
@form(action=routes.controllers.PizzaController.orderPizza){
Pizza: @pizza.name
<button type="submit">order</button>
<input type="hidden" name="pizza.name" value="@pizza.name"/>
}
}
</ul>
}
Then reflect the new orderPizza call in the routes file:
vi conf/routes
# Routes
# ~~~~
GET /
controllers.PizzaController.showMenu
POST /order
controllers.PizzaController.orderPizza
GET /assets/*file
controllers.Assets.versioned(path="/public", file: Asset)
And create the orderPizza function in the pizza controller to simple return a success page.
vi app/controllers/PizzaController.scala
.....
def orderPizza = Action {
Ok
}
.....
You can test the order button now, it should respond in a blank success page.
When ready lets create a pizza order domain object in our models file:
vi app/models/pizza.scala
case class PizzaOrder(id: Option[Long], pizza: Pizza)
And in the controller a form mapping transformer.
vi app/controllers/PizzaController.scala
package controllers
import javax.inject._
import play.api._
import play.api.data._
import play.api.data.Forms._
import play.api.libs.concurrent.Execution.Implicits._
import play.api.mvc._
import scala.concurrent.Future
import models._
import repositories._
@Singleton
class PizzaController @Inject() (val pizzaRepository: PizzaRepository)
extends Controller {
val orderForm = Form( mapping (
"id" -> ignored(None: Option[Long]),
"pizza" -> mapping (
"name" -> nonEmptyText
)(Pizza.apply)(Pizza.unapply)
)(PizzaOrder.apply)(PizzaOrder.unapply)
)
.....
}
Then we extend the orderPizza method to bind the request to this mapping.
.....
def orderPizza = Action { implicit request =>
orderForm.bindFromRequest.fold(
errors => BadRequest,
order => Created
)
}
.....
This can now be tested via a browser by clicking on the order buttons on the menu, or directly via curl.
This still doesn't really do anything, it just returns blank pages as we havent specified a template. It only returns the expected http error code. We could also include the erroneous form in the BadRequest response but we are keeping it simple for now.
curl --data "{\"wrongfield\"=\"wrongdata\"}" \
-H "Content-Type: application/json" \
-v http://localhost:9000/order
This should not work as wrong field name, and will return amongst others:
< HTTP/1.1 400 Bad Request
But this should order a pizza:
curl --data "{\"pizza.name\":\"Hawaii\"}" \
-H "Content-Type: application/json" \
-v http://localhost:9000/order
< HTTP/1.1 201 Created
Ok lets show a more usefull pizza ordered confirmation page, than just a blank page with http status code.
Lets add a confirmation page route, which includes an order id in its path.
vi conf/routes
# Routes
# ~~~~
GET /
controllers.PizzaController.showMenu
POST /order
controllers.PizzaController.orderPizza
GET /order/:orderId
controllers.PizzaController.showConfirmation(orderId: Long)
GET /assets/*file
controllers.Assets.versioned(path="/public", file: Asset)
Lets add the showConfirmation method to PizzaController. It will try to find the order, and either display a confirmation page with it, or a blank page with a 404 not found http status code if there was no orders found with the supplied id.
vi app/controllers/PizzaController.scala
.....
def showConfirmation(orderId: Long) = Action.async {
pizzaRepository.findPizzaOrder(orderId).map{
case Some(order) => Ok(views.html.confirmation(order))
case _
=> NotFound
}
}
.....
We will need to create the findPizzaOrder method in the pizza repository, and hard code its response for now:
vi app/repositories/PizzaRepository.scala
.....
def findPizzaOrder(orderId: Long) = Future {
Some(PizzaOrder(Some(orderId), Pizza("Hawaii")))
}
.....
And the view
cp app/views/pizzamenu.scala.html \
app/views/confirmation.scala.html;
vi app/views/confirmation.scala.html
@(pizzaOrder: PizzaOrder)
@main("Pizza order confirmation") {
<h1>Pizza ordered!</h1>
<p>
You have ordered a <em>@pizzaOrder.pizza.name</em> pizza!
</p>
}
You can test this via:
curl localhost:9000/order/123
Lets add a correct response form the order post we created before, and a call to the repositry to simulate storing the order. Edit the orderPizza in the pizza controller:
vi app/controllers/PizzaController.scala
.....
def orderPizza = Action.async { implicit request =>
orderForm.bindFromRequest.fold(
errors =>
Future.successful( BadRequest ),
order =>
{
pizzaRepository.addPizzaOrder(order) map {
case PizzaOrder(Some(orderId), _) =>
Redirect(routes.PizzaController.showConfirmation(orderId))
case _ => InternalServerError("Could not add pizza order")
}
}
)
}
.....
And then add the addPizzaOrder to the pizza repository:
vi app/repositories/PizzaRepository.scala
.....
def addPizzaOrder(pizzaOrder: PizzaOrder): Future[PizzaOrder] = Future {
pizzaOrder.copy(id = Some(123L)
}
.....
Test the order pizza call:
curl --data "{\"pizza.name\":\"Hawaii\"}" \
-H "Content-Type: application/json" \
-v http://localhost:9000/order
Which response should now include a redirect to the confirmation page:
.....
< HTTP/1.1 303 See Other
< Location: /order/123
.....
The hard coded responses in PizzaRepository is not satisfactory. Lets add some persistant database support.
For this we will use the H2 in-memory database. Which is ideal unit tests and when developing locally.
Then perhaps a docker based PostgreSQL database when developing and testing a larger stack via Docker Compose and in staging environments. See my Docker machine & compose pizzeria howto for an example.
When in production it is recommend using proper database instance(s) outside Docker. For example clustered PostgreSQL, Amazon RDS, Google Bigtable, Redis on Heroku, Cassandra on Mesos etc. I am on the fence on this one, I understand why due to Docker's ephemeral nature and historic issue with data persistence, but I do run many databases in containers, especially less ciritical system...
For persistance library this Pizzeria uses Anorm. Anorm is very thin wrapper around normal SQL made by the original Play developers themselves. A more common framework is Slick, and is the default for Play these days. Slick is functional relational mapper, and often involves more compexity that is needed by too many layers of abstractions.
In the project's build.sbt file we will add some dependencies we will need, and actually use the jdbc alias we kept earlier.
vi build.sbt
.....
libraryDependencies ++= Seq(
jdbc,
evolutions,
"com.h2database" % "h2" % "1.4.192",
"com.typesafe.play" %% "anorm" % "2.5.0"
)
.....
Play's default main configuration file has evolved into a huge blob of configurations. But lets remove 99% which is just cruft, add some jdbc properties, and end up with a config file like this:
vi conf/application.conf
## Secret key
play.crypto.secret = "changemechangemechangemechangeme"
play.crypto.secret=${?APPLICATION_SECRET}
## Evolutions
play.evolutions.db.default.autoApply = true
## JDBC Datasource
db.default {
driver = org.h2.Driver
url = "jdbc:h2:mem:play;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=FALSE"
username = sa
password = ""
logSql = true
}
Above we have enabled evolutions which lets us create and evolve our database schema and data. Our first evolution file creates the initial schema:
mkdir -p conf/evolutions/default;
touch conf/evolutions/default/1.sql;
vi conf/evolutions/default/1.sql
# Pizzeria schema
# --- !Ups
CREATE TABLE pizza (
id serial NOT NULL,
name varchar(128) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE pizza_order (
id serial NOT NULL,
pizza_id int NOT NULL REFERENCES pizza (id),
order_date TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
);
# --- !Downs
DROP TABLE pizza_order;
DROP TABLE pizza;
Our PizzaRepository can now be modified to use an actual database.
vi app/repositories/PizzaRepository.scala
package repositories
import anorm._
import anorm.SqlParser._
import com.google.inject.ImplementedBy
import javax.inject.Inject
import play.api.db._
import play.api.libs.concurrent.Execution.Implicits._
import scala.concurrent.Future
import models._
@ImplementedBy(classOf[DefaultPizzaRepository])
trait PizzaRepository {
def dbApi: DBApi
private lazy val db: Database = dbApi.database("default")
.....
}
class DefaultPizzaRepository @Inject() (val dbApi: DBApi) extends PizzaRepository
Modify the existing trait's methods, starting with findPizzas:
.....
val pizzaParser: RowParser[Pizza] = Macro.namedParser[Pizza]
def findPizzas(): Future[List[Pizza]] = Future {
db.withConnection { implicit connection =>
SQL"""
select name from pizza
order by name
"""
.as( pizzaParser.* )
}
}
.....
And a much more comples addPizzaOrder that uses internal functions (mostly as we have not exposed the database id in the Pizza domain model):
.....
def addPizzaOrder(pizzaOrder: PizzaOrder): Future[PizzaOrder] = {
def findPizzaId(pizzaName: String) = Future {
db.withConnection { implicit connection =>
SQL"""
select id from pizza
where name = $pizzaName
"""
.as( scalar[Long].single )
}
}
def addOrder(pizzaId: Int) = Future {
db.withConnection { implicit connection =>
SQL"""
insert into pizza_order(pizza_id)
values ($pizzaId)
"""
.executeInsert()
}
}
for {
pizzaId <- findPizzaId(pizzaOrder.pizza.name)
orderId <- addOrder(pizzaId)
} yield pizzaOrder.copy( id = orderId )
}
.....
And finally findPizzaOrder:
.....
def findPizzaOrder(orderId: Long): Future[Option[PizzaOrder]] = Future {
db.withConnection { implicit connection =>
SQL"""
select p.id as pizza_id, p.name as pizza_name
from pizza_order o
inner join pizza p on p.id = o.pizza_id
where o.id = $orderId
"""
.as( ( get[Long]("pizza_id") ~
get[String]("pizza_name")
).singleOpt )
.map{ case pizzaId ~ pizzaName =>
PizzaOrder( Some(orderId), Pizza(pizzaName) )
}
}
}
.....
Lets add the default pizzas to the menu:
touch conf/evolutions/default/2.sql;
vi conf/evolutions/default/2.sql
# Pizzeria Menu
# --- !Ups
INSERT INTO pizza (name) values
('Margherita'),
('Hawaii'),
('Quattro Stagioni'),
('Pepperoni');
# --- !Downs
DELETE FROM pizza;
We can now test the full flow with persisted data.
sbt run
curl localhost:9000
This should show the extended menu.
curl --data "{\"pizza.name\":\"Margherita\"}" \
-H "Content-Type: application/json" \
-v localhost:9000/order/code
This should show a redirect to confimation page. And if we GET that it should show we ordered a Margherita.
curl localhost:9000/order/1
Build a Docker image of our finished Pizzeria service. And tag it.
docker build -t pizzeria .;
docker tag pizzeria:latest pizzeria:1.0
Docker Cloud is a handy way to manage your public docker containers. It enables you to easily build and host your images. It lets you orchestrate and deploy your container stacks. And monitor and scale as appropriate.
Docker Cloud does not actually host your actual running containers. Hosting is still done with normal IAAS cloud providers. Such as Amazon AWS, etc. Docker Cloud instead manages the containers on these instances for you. It can directly create instances for you on some of the IAAS providers, or you can connect an existing node to Docker Cloud by installing an agent service. You can also spread your containers horizontally across nodes on different IAAS providers.
There is a reasonable free tier that lets your experiment and run quite a few containers for free. (As I am grand-fathered in as a user of Docker Cloud's predecessor, Tutum so my free tier is slightly more generous). Combine this with the free tiers at many IAAS providers you can experiment for quite some time totally free.
We need to store the Docker image of our Pizzeria service, so we need a Docker registry There are many providers, Quay.io, Amazon EC2 Container Registry, Google Platform Container Registry, etc. You can even host a registry yourself inside a container. But for our pizzeria we will use the Docker Hub.
Go to Docker Hub, log in with your Docker id or sign up. Create a new repository for the pizzeria service.
Lets push up our local Docker image to the public one you just created.
docker push yourUsername/pizzeria:latest
As part of the excellent Docker Cloud's own intro to itself we will start by integrating your IAAS / cloud service provider. At the time of writing, December 2016, Docker Cloud supports out of the box AWS, Digital Ocean, Microsoft Azure, SoftLayer and Packet.net. It does not yet support Google Cloud directly.
DigitalOcean is the easiest to integrate with. Amazon AWS I would guess is the most popular as many will already have a presence there. I prefer Google Cloud as provider, via the Bring Your Own Node feature.
Create a node or two. Keep in mind the pizzeria service is a JVM based image so will be memory hungry, and for now compute shy. So the t2.nano and t2.micro instances on AWS will be too small where as a $20 instance on DigitalOcean or a t2.small instance on AWS is a good start, and R4 instances better suited later on. For example I mostly use 6.5GB with 1 cpu custom instances on Google Cloud to host 4-5 containers per node.
Note unless you are within the free tier on your IAAS, the node choices will cost you money.
Create a service, which is basically your application. There are lots of options you dont need to worry about right now but essentialy:
Once the service is configured you press the Create & Deploy button.
If that works, then you should be forwarded to the final screen where a summary of the service is shown. Note the Endpoints section, where you shold have a Service and a Container endpoints. The service endpoint should look something like:
tcp://pizzeria.12abcd34.svc.dockerapp.io:9010
The service endpoint is clickable, but change the protocol from tcp to http.
Note: If you plan to add SSL to your apps by following my Let's Encrypt with Nginx, then this service endpoint is the one you need to proxy to.
Stacks are not need in this pizzeria example. But if you were to separate the service into e.g. a back-end, front-end and database, like my docker compose based example, then Stacks is where you define the separate services within Docker Cloud.
Spray and its successor Akka HTTP are alternatives to Play. It is a good choice for a service without UI such as the backend in my docker compose example.
Heroku is a PAAS. And is the smoothest and quiest way to deploy a pizzeria service to a cloud/hosting provider. I love Heroku, and run many applications with it, but it is not perfect. They do support a Docker build flow, but mostly you do not use Docker with Heroku. Instead you push your code (via Git remote) and use their build flow to generate and host their own containers.
The Container Engine is an extensive Docker container service. It is a commercial offering that uses Kubernetes clusters.
At a lower level Docker Machine lets you create instances in cloud providers. You can then manually deploy your containers to it. You can also scale horizontally with Docker Swarm.
A recently annouced product you can also deploy containers directly to AWS with Docker for AWS.
Please fork and send a pull request for to correct any typos, or useful additions.
Buy a t-shirt if you found this guide useful. Hire me for short term advice or long term consultancy.
Otherwise contact me. Especially for things factually incorrect. Apologies for procrastinated replies.