Skip to main content

Build a Basic App

Let's see how to create and run a basic app that uses MongoDB and Nginx, entirely from Monk.

This tutorial requires you to have Monk installed locally, which you can do just like this.

What we're building#

We're preparing a small application for deployment. It's a simple guestbook written in Node.js, where we store entries in MongoDB and use a basic Nginx reverse proxy in front of the app to secure it.

We will prepare a Monk template for a small system composed of these three elements. The code of the app itself is not the focus of this tutorial and will be provided.


An artist's rendering of the system we're building

The app#

Our App is just a simple piece of code that requires a Mongo database and exposes an HTTP endpoint by means of the Express framework.

Let's grab the source from GitHub:

git clone https://github.com/oaknode/tutorial.git
note

Oaknode is the repo of the company behind Monk. We're migrating to Monk's Github in steps.

Preparing the container#

Monk runs containers so our App needs to be containerized before we can start handling it. We will use Docker in this tutorial as it is the easiest way to obtain a container image from the App's source code.

Open the folder containing the source and inspect the Dockerfile that we have prepared:

Dockerfile

FROM node:alpine
WORKDIR /usr/src/app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 8080CMD ["node", "index.js"]

This is a pretty standard Node.js Dockerfile without any bells and whistles and it's good enough for our small App. Let's build and publish the image:

docker build -t yourname/tutorial:latest .docker push yourname/tutorial:latest

This will give us the container image for our App. Your local docker instance now knows about the yourname/tutorial:latest image, and in the next step we will wrap it in a Monk template.

note

We only use Docker in this tutorial to build the image so that Monk becomes aware of our app by caching its container image. Monk will run and orchestrate containers spawned from any OCI-compliant container image; Docker is one way of obtaining those.

Preparing a Monk manifest#

Let's start writing a template that will describe where to get the app, how to configure it and how to run it in the right context. Start a new file called app.yaml and put the following contents there:

app.yaml
namespace: /yourname
app:    defines: runnable    version: 0.0.1    containers:        defines: containers        app:            image: yourname/tutorial:latest
note

You can look at the tutorial.yaml file in the App's repo to see the full example at any time.

We have provided the simplest description of the container image to be run and Monk is already capable of starting it. The namespace field tells Monk where to put the description of app - it will be under yourname/app. The defines field is important as it tells Monk how to interpret parts of the YAML tree - all the names are free form so this is the way to "type" the YAML.

You could run your new template with:

monk load app.yamlmonk run yourname/app
note

To do this, you should have the cluster already set-up. Also, monk stop yourname/app to stop the template.

However, the template is not complete. Much like with docker-compose, in order to prepare a good runtime environment, we must understand what are the App's requirements.

By inspecting the index.js file we can see that the app requires three environment variables to be set before it runs:

index.js
//...const PORT = process.env.PORT;const DB_HOST = process.env.DB_HOST;const DB_PORT = process.env.DB_PORT;//...

This has to be reflected in our template by telling the container to use the right environment variables and also bringing them out into the Monk namespace. This will allow us to alter the variables later when composing the system.

First, let's define the variables in our runnable. Add the following variables section below the containers section:

app.yaml
namespace: /yourname
app:    # ...    variables:        defines: variables        port:            type: int            value: 8080        db-host:            type: string        db-port:            type: int            value: 27017

This tells Monk that we have a set of values associated with our runnable for the app. Now let's pass the values to the container by adding the following environment section to app/containers/app:

app.yaml
app:    containers:        app:            # ... image            environment:                - <- `PORT=${port}`                - <- `DB_HOST=${db-host}`                - <- `DB_PORT=${db-port}`

Here we see a small example of Monk's powerful language. The YAML values starting with <- will be calculated at runtime. In this case, we just interpolate environment variables from the variables we have defined in the namespace.

note

The syntax for string interpolation in MonkScript is inspired by JavaScript's string template literals. Learn more about Arrow scripts.

Your app.yaml manifest should now look like this:

app.yaml

namespace: /yourname
app:    defines: runnable    version: 0.0.1    containers:        defines: containers        app:            image: yourname/tutorial:latest            ports:                - <- `${port}:${port}`            environment:                - <- `PORT=${port}`                - <- `DB_HOST=${db-host}`                - <- `DB_PORT=${db-port}`    variables:        defines: variables        port:            type: int            value: 8080        db-host:            type: string            value: localhost        db-port:            type: int            value: 27017

Now run the following to make Monk aware of your new template:

monk load app.yaml

And that's it! It's very easy to wrap any containerized application into a Monk template. You could publish this template now for everyone to run with a simple monk run yourname/app or use it in composition with other templates.

We are going to deploy it ourselves though, so publishing won't be necessary.

3rd party services#

One of the most interesting facts about Monk is that we don't have to work from scratch when it comes to deploying 3rd party services such as Mongo and Nginx. We can simply take them off the shelf and focus on our app.

You'll see in a second. We will use pre-made templates for Mongo and Nginx and simply include them in our system composition. Which means this step... is not really a step after all ๐Ÿ˜Ž

Composing the system#

Now it's time to compose our app's template with the third party services, and make another template out of that. This way, you will be able to run the same composition on any Monk cluster on any cloud.


Template architecture for the system we&#39;re building

If you were to publish your composed template, other people would also be able to run this same setup in seconds or compose it further with their own services. That's what our Publisher program is there for, by the way.

Creating a template of templates#

First, create a new manifest file called system.yaml and add the following contents:

system.yaml
namespace: /yourname
tutorial-app:    defines: runnable    inherits: ./app    variables:        port:            value: 8080
system:    defines: process-group    runnable-list:        - /yourname/tutorial-app        - /yourname/tutorial-mongo        - /yourname/tutorial-nginx

We have already defined the yourname/app runnable inside app.yaml and now we're instantiating it as yourname/tutorial-app. Since app lives in the same namespace, we can just refer to it as ./app. The inherits keyword tells Monk to put the sub-tree from the target path in the new path (here: yourname/tutorial-app) and override it with whatever comes next.

There are two other runnables which we will define in a moment. Also notice that the file uses the same namespace, which means that Monk will put system together with the app in the same namespace tree. This is important because it allows you to keep your definition files in separate repositories but still retain order within your own namespace.

Finally, process-group is like a runnable consisting of other runnables. This allows us to group them and tell Monk that we want these things to be run together in a single cluster.

Adding Mongo#

Great, now let's define the missing yourname/tutorial-mongo runnable in terms of the existing MongoDB template. Add this to the system.yaml file:

system.yaml
namespace: /yourname
# ...
tutorial-mongo:    defines: runnable    inherits: mongodb/latest

And we're all good. We will use MongoDB from the mongodb/latest template that has been published to the shared namespace. That's all we need, since the app is simple and we don't want to change any defaults that the database template comes with.

Let's update tutorial-app so it knows how to find the database. Add the db-host section to the tutorial-app/variables:

system.yaml
namespace: /yourname
tutorial-app:    # ...    variables:        # ...        db-host:            value: <- get-hostname("yourname/tutorial-mongo", "db")

Adding Nginx#

Nginx is next. The process is similar to MongoDB, but we want to get Nginx configured as a reverse proxy, so we'll have to tell it where to connect to:

system.yaml
namespace: /yourname
# ...
tutorial-nginx:    defines: runnable    inherits: nginx/reverse-proxy    variables:        listen-port:            value: 9090        proxy-target-host:            value: <- get-hostname("yourname/tutorial-app", "app")        proxy-target-port:            value: 8080

And that's pretty much it. Just changing those two variables is enough for the Nginx template to generate a config file for itself and behave as a reverse proxy for our purposes.

We could change any of the Nginx settings here or even enable Let's Encrypt, which is already available in the Nginx template, but let's keep it simple for now.

Our app is ready#

In the previous steps we've composed all parts of our app. Your system.yaml file should look like this now:

system.yaml

namespace: /yourname
tutorial-mongo:    defines: runnable    inherits: mongodb/latest
tutorial-app:    defines: runnable    inherits: ./app    variables:        port: 9090        db-host: <- get-hostname("yourname/tutorial-mongo", "database")
tutorial-nginx:    defines: runnable    inherits: nginx/reverse-proxy    variables:        server-name: tutorial-app.moncc.io        listen-port: 8080        proxy-target-host: <- get-hostname("yourname/tutorial-app", "app")        proxy-target-port: 9090
system:    defines: process-group    runnable-list:        - /yourname/tutorial-mongo        - /yourname/tutorial-app        - /yourname/tutorial-nginx

Let's load the system definition with:

monk load system.yaml

You can now run your system locally using the following command:

monk run yourname/system

And everything should be working like a charm. You can now visit http://localhost:9090 to verify that the app is in fact running and functioning correctly.

To stop it, use:

monk stop yourname/system

Conclusion#

This concludes our basic app tutorial. We have learned how to compose 3rd party software with our own containerized service and how to run it all as an ensemble using Monk.

The next step would be to run your app on different clouds. To that end, you can head back to the Creating a cluster and Running templates in a cluster guides to see them with a fresh perspective.

Or better yet, continue to Connecting runnables to see how we can make our services talk to each other.

Rate this page