Overview

Goals and plan

This guide demonstrates how to develop new modules for Add-On Mart and prepare them for deployment using the CLI tool `addon-mart`. With this tool, you can efficiently manage development and debugging in a local deployment environment.

By separating configuration into a module schema (defining options) and storing the actual values in separate files, you can easily configure and manage the application. Built-in support for templates, file mounting, and Docker commands allows you to configure, build, and run the application locally with a single command.

This document walks you through pre-built service components, which follow a typical microservices architecture: frontend, backend, and database. Our example is a portal application that provides access to pages and features for authorized users, including login and self-registration pages.

The demo application serves as a tutorial foundation and can be expanded and enhanced as needed. The frontend is built on Angular (using TypeScript and an HTTP web server), while the backend is based on FastAPI (using Python and an ASGI web server). Each service component is Dockerized and can be deployed on any platform with any orchestrator. However, the main goal is to prepare the application for placement in our marketplace and get it ready for deployment in a test environment.

The first sections guide you through launching a single frontend container with Dockerfile, explaining the structure, passing parameters through environment variables, file organization, and the launch command. The next sections assist in creating the final set of configuration files needed to launch the complete application.

For those looking to use the solution as is and skip intermediate steps, you can download the final file set from the last step.

Component diagram

NameDescription
FrontendThe frontend is the part of a web application that interacts directly with the user. It is responsible for displaying data and facilitating user interactions, providing a convenient interface and visual representation of the application's functions. The primary purpose of the frontend is to create an intuitive and engaging user experience.
BackendThe backend is the part of a web application that operates on the server and processes data. It is responsible for storing, processing, and transmitting data between the server and the frontend, providing the application’s logic and functionality. The primary purpose of the backend is to support and manage the server-side operations, ensuring reliability and security.
MongoDBMongoDB is a NoSQL database used for storing and managing data in an unstructured format, such as JSON documents, for instance, list of customers
Mongo Express

Mongo Express is a web-based interface for managing MongoDB databases. It allows users to view, add, edit, and delete documents, as well as run queries on data through a user-friendly graphical interface. The primary purpose of Mongo Express is to simplify working with MongoDB by providing an intuitive way to interact with the database.

This solution is used only for the development environment to test the application and provide convenient access to the data in the database. In a production environment, this service should not be deployed for security reasons.

Frontend

Pre-authorizationAuthorized access

Login page

Use login name `admin` and password `admin` for authorization by default.

Home page

Register page

Users page

It is used for managing the users (List/Add/Edit/Delete). 

Backend

FastAPI provides built-in visual tools for displaying the OpenAPI schema, making it easier to interact with and understand the API. These tools are available by default on the backend, streamlining the development process and improving accessibility for developers.

The current API description based on OpenAPI v3 JSON:

openapi.json

Link to openapi.json for downloading and link to https://editor.swagger.io/ where you should see the visual

After starting the application, API documentation will be available via Swagger (http://localhost:5500/docs) and Redoc (http://localhost:5500/redoc).

Swager UIRedoc UI

Mongo Express

Installing addon-mart tool

Follow this link and installation instructions to have addon-mart tool installed. After this, configure it to work locally with command:

$ addon-mart set-context local

You can find more information here.

Introduction to the configuration schema

By defining a separate configuration schema, you store the settings and options outside of the application code. The schema includes metadata that specifies validation types, mandatory fields, and default values, helping ensure correct configurations. 

The image below shows an example of the configuration schema on the left, while on the right is the Add-On Mart web interface for configuration, which is dynamically generated from the schema into an interactive form. 

This metadata also allows the application to automatically generate a dynamic HTML form based on the configuration schema, enabling users to input values through a clear and user-friendly interface.

For more information on the structure and fields of the module schema, please refer to the detailed documentation or online with schema-generated UI

Launching a single-container

The initial pack of the Frontend application code you can find here. Clone it to your local computer to continue with this tutorial

Building a schema for a frontend application

By examining the src/assets/env.template.js  listing, we can determine the environment variables that can be configured.

src/assets/env.template.js
(function (window) {
    window['env'] = window['env'] || {};

    window['env']['apiUrl'] = '${API_URL}';
})(this);

This example shows where the variable is used for an Angular app. If you are using your own app, the set of variables will already be known to you.

So, we have just one environment variable. Let's create addonmart-tool/config_schema.json file based on an online schema-generated UI tool or manually

addonmart-tool/config_schema.json
{
  "groups": [
    {
      "name": "Frontend",
      "provider": "APP_OWNER",
      "settings": [
        {
          "id": "api_url",
          "label": "API URL",
          "type": "text",
          "mandatory": true,
          "info": "URL for the API endpoint that the frontend will interact with.",
          "placeholder": "https://api.example.com",
          "default": null
        }
      ]
    }
  ]
}

The definition of the field names and description can be found in the documentation.

The value of the id field should be:

  • required;
  • unique for all schema;
  • in the snake_case style as to the Naming policy.

Create a file with the value of the variables

Let's create addonmart-tool/config_values.yaml file with just one api_url option

addonmart-tool/config_values.yaml
options:
  api_url: http://localhost:5500

Pay attention to following the Naming policy. Please note that this is a temporary value for local testing. In production or staging mode, this value will be retrieved from the configuration server.

Naming policy

  • Use the snake_case style for the option name in the config schema file ('id' field)
  • Use the snake_case style for the config values file
  • The tool generates environment variables in the UPPERCASE_WITH_UNDERSCORES style
  • Use the camelCase style as a placeholder name in template files

First launch

Overview of the current file structure of the frontend folder (just 2 levels):

$ tree -L 2
.
├── addonmart-tool
│   ├── config_schema.json
│   └── config_values.yaml
├── angular.json
├── Dockerfile
├── nginx.conf
├── package.json
├── package-lock.json
├── scripts
│   └── init_container.sh
├── src
│   ├── app
│   ├── assets
│   ├── environments
│   ├── favicon.ico
│   ├── index.html
│   ├── main.ts
│   └── styles.css
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json

Use run command to launch the frontend application 

$ addon-mart run \
  --configuration-schema ./addonmart-tool/config_schema.json \
  --local-values         ./addonmart-tool/config_values.yaml \
  --build                ./addonmart-tool/build \
  --app-dir              ./ \
  --app-name             aom-web-app:1.0.1 \
  --port                 80:80
INFO: > validating configuration values...
INFO: reading ./addonmart-tool/config_schema.json file...
INFO: reading ./addonmart-tool/config_values.yaml file...
INFO: creating ./addonmart-tool/build/config_values.env file...
SUCCESS: configuration values are valid.
INFO: building "aom-web-app:1.0.1" image using Dockerfile...
{"stream":"Step 1/12 : FROM node:18-slim AS build"}
{"stream":"\n"}
...
{"stream":" ---\u003e Using cache\n"}
{"stream":" ---\u003e eed0c7a31967\n"}
{"stream":"Step 12/12 : CMD [\"sh\", \"init_container.sh\"]"}
{"stream":"\n"}
{"stream":" ---\u003e Using cache\n"}
{"stream":" ---\u003e 1a805c528dab\n"}
{"aux":{"ID":"sha256:1a805c528dab5c7541c7e12a2d1b54ad741073ce0289017c72225087c55ac4cd"}}
{"stream":"Successfully built 1a805c528dab\n"}
{"stream":"Successfully tagged aom-web-app:1.0.1\n"}
INFO: creating "aom-web-app" container...
INFO: starting "aom-web-app" container...
SUCCESS: "aom-web-app" container has been successfully started.
INFO: please use "docker logs -f aom-web-app" to see logs.
INFO: please use "docker container stop aom-web-app" to stop the container.

Use help to get the description for each argument

$ addon-mart run -h
NAME:
   addon-mart run - Allows to build and start a dockerized application with provided configuration

USAGE:
   addon-mart run [command options] [arguments...]

OPTIONS:
   --configuration-schema value  Path to a file with application configuration-schema. Note: this file should be in JSON (.json) format (required)
   --local-values value          Path to a file with configuration values. Note: this file should be in YAML (.yaml) format (default: "config_values.yaml")
   --remote-values value         Combination of instance ID and access token for connection with configuration server. Format: <instance_id>:<access_token>
   --build value                 Path to a directory for processed configuration values files and templates (default: "build/")
   --files-mapping value         Path to the file with mapping of external files. Note: this file should be in YAML (.yaml) format
   --app-name value              Name of the app (it is used to name an image and container)
   --port value                  Port mapping for app container. Format <host_port>:<container_port>
   --app-dir value               Path to a directory with source code of the app. Note: this directory should contain Dockerfile file
   --compose-config value        Name of compose configuration file
   --templates-mapping value     Path to the file with mapping templates. Note: this file should be in YAML (.yaml) format
   --templates value             Path to a directory with templates
   --help, -h                    show help

Check the running web application on the localhost: http://localhost/

Check the status of the image, container

$ docker images aom-web-app
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
aom-web-app   1.0.1     1a805c528dab   27 hours ago   12.3MB

$ docker ps 
CONTAINER ID   IMAGE               COMMAND                  CREATED              STATUS              PORTS                NAMES
9223239c787b   aom-web-app:1.0.1   "/docker-entrypoint.…"   About a minute ago   Up About a minute   0.0.0.0:80->80/tcp   aom-web-app

Let's check the environment variables passed to the running container

$ docker exec aom-web-app env | sort
API_URL=http://localhost:5500
HOME=/root
HOSTNAME=9223239c787b
NGINX_VERSION=1.25.4
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PKG_RELEASE=1

So, the environment variable API_URL  and its value is passed successfully

Checking logs

Use docker logs command to check the container log ("-f" argument helps to follow new messages in the realtime)

$ docker logs -f aom-web-app
2024/11/07 15:18:02 [notice] 8#8: using the "epoll" event method
2024/11/07 15:18:02 [notice] 8#8: nginx/1.25.4
2024/11/07 15:18:02 [notice] 8#8: built by gcc 12.2.1 20220924 (Alpine 12.2.1_git20220924-r10) 
2024/11/07 15:18:02 [notice] 8#8: OS: Linux 6.8.0-48-generic
2024/11/07 15:18:02 [notice] 8#8: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2024/11/07 15:18:02 [notice] 8#8: start worker processes
2024/11/07 15:18:02 [notice] 8#8: start worker process 9
...
2024/11/07 15:18:02 [notice] 8#8: start worker process 19
2024/11/07 15:18:02 [notice] 8#8: start worker process 20
172.17.0.1 - - [07/Nov/2024:15:27:07 +0000] "GET /account/login?returnUrl=%2F HTTP/1.1" 200 675 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" "-"
172.17.0.1 - - [07/Nov/2024:15:27:07 +0000] "GET /assets/env.js HTTP/1.1" 200 128 "http://localhost/account/login?returnUrl=%2F" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" "-"
172.17.0.1 - - [07/Nov/2024:15:27:07 +0000] "GET /favicon.ico HTTP/1.1" 200 15086 "http://localhost/account/login?returnUrl=%2F" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" "-"

Stopping the container

Use docker container stop command to stop the container

$ docker container stop aom-web-app
aom-web-app

Auto-generated build folder

A build folder has been created with the contents that will be used to launch the container. Do not edit the files here manually, they will be automatically overwritten at the next launch.

 $ tree addonmart-tool/
addonmart-tool/
├── build
│   └── config_values.env
├── config_schema.json
└── config_values.yaml

Listing the generated environment variable file

addonmart-tool/build/config_values.env
API_URL="http://localhost:5500" 

Binding of custom static files

You can use file binding as a configuration option to customize or brand the application. For example, this tool allows you to replace the favicon.ico  file used as the browser icon. Let's create a directory "assets" and find and copy any random favicon.ico to replace the icon we have by default.

$ tree addonmart-tool/
addonmart-tool/
├── assets
│   └── favicon.ico
├── build
│   └── config_values.env
├── config_schema.json
├── config_values.yaml
└── files_mapping.yaml

The path to the new favicon file is addonmart-tool/assets/favicon.ico .

Let's create a file with source mapping (absolute path) and target mount point in the container (absolute path)

addonmart-tool/files_mapping.yaml
 mapping:
   - <absolute_path_to_file>/frontend/addonmart-tool/assets/favicon.ico:/usr/share/nginx/html/favicon.ico

Execute the launch command

$ addon-mart run \
  --configuration-schema ./addonmart-tool/config_schema.json \
  --local-values         ./addonmart-tool/config_values.yaml \
  --files-mapping        ./addonmart-tool/files_mapping.yaml \
  --build                ./addonmart-tool/build \
  --app-dir              ./ \
  --app-name             aom-web-app:1.0.1 \
  --port                 80:80

Notice, the new --files-mapping ./addonmart-tool/files_mapping.yaml  argument

The browser may cache the page icon file, so a hard reload of the page may be required.

 

The final version of the Frontend application code is available for download via this link.

Launch the entire application at once

Overview

The initial pack of the application code you can find here

Overview of the current file structure of the application folder (just 3 levels):

$ tree -L 3 -a
.
├── backend
│   ├── app
│   │   └── main.py
│   ├── Dockerfile
│   └── requirements.txt
├── compose.yaml
├── .env.example
├── frontend
│   ├── angular.json
│   ├── Dockerfile
│   ├── nginx.conf
│   ├── package.json
│   ├── package-lock.json
│   ├── scripts
│   │   └── init_container.sh
│   ├── src
│   │   ├── app
│   │   ├── assets
│   │   ├── environments
│   │   ├── favicon.ico
│   │   ├── index.html
│   │   ├── main.ts
│   │   └── styles.css
│   ├── tsconfig.app.json
│   ├── tsconfig.json
│   └── tsconfig.spec.json
└── mongo
    └── init-mongo.js.example

We have separate folders for each component. Let's look Docker Compose file:

compose.yaml
 version: '3.3'

services:
  web:
    build:
      context: './frontend'
    ports:
      - '80:80'
    env_file: '.env.example'
    depends_on:
      - back
    networks:
      - portal-network

  back: 
    build:
      context: './backend'
      target: builder
    ports:
      - '5500:80'
    env_file: '.env.example'
    depends_on:
      - mongo
    networks:
      - portal-network
  
  mongo:
    container_name: mongo
    image: mongo
    ports:
      - 27017:27017
    env_file: '.env.example'
    volumes:
      - mongo_data:/data/db
      - ./mongo/init-mongo.js.example:/docker-entrypoint-initdb.d/init-mongo.js
    networks:
      - portal-network

  mongo-express:
    image: mongo-express
    ports:
      - 8081:8081
    env_file: '.env.example'
    depends_on:
      - mongo
    networks:
      - portal-network

networks:
  portal-network:
    driver: bridge

volumes:
  mongo_data:

You can find the official definition and description in the Docker Compose documentation: https://docs.docker.com/compose/.

Building a schema for the application

Every dockerized application should have a set of parameters that configure it, without which the application will not be able to start or function. This list can be found either in the README file or (as in our case) in the example file. This file specifies which variables will be configured and we can see the data type and examples of their values.

Please review an example of the environment variable file. Here, we can find all the configuration parameters we need to build the application configuration schema.

.env.example
# Frontend
API_URL=http://localhost:5500

# Backend
BACKEND_MONGO_USERNAME=mongo_user
BACKEND_MONGO_PASSWORD=password
BACKEND_MONGO_HOST=mongo
BACKEND_MONGO_PORT=27017
BACKEND_MONGO_DATABASE=portal

# MongoDB
MONGO_INITDB_ROOT_USERNAME=mongo_root
MONGO_INITDB_ROOT_PASSWORD=example
MONGO_INITDB_DATABASE=portal

# Mongo Express
ME_CONFIG_MONGODB_ADMINUSERNAME=mongo_root
ME_CONFIG_MONGODB_ADMINPASSWORD=example
ME_CONFIG_MONGODB_URL=mongodb://mongo_root:example@mongo:27017/
ME_CONFIG_BASICAUTH=false

Let's create addonmart-tool/config_schema.json file with help of an online schema-generated UI tool or manually

addonmart-tool/config_schema.json
{
  "groups":[
    {
      "name":"Frontend",
      "provider":"APP_OWNER",
      "settings":[
        {
          "id":"api_url",
          "label":"API URL",
          "type":"text",
          "mandatory":true,
          "info":"URL for the API endpoint that the frontend will interact with.",
          "placeholder":"https://api.example.com",
          "default":null
        }
      ]
    },
    {
      "name":"Backend",
      "provider":"APP_OWNER",
      "settings":[
        {
          "id":"backend_mongo_username",
          "label":"MongoDB Username",
          "type":"text",
          "mandatory":true,
          "info":"Username for authenticating with MongoDB in the backend.",
          "placeholder":"mongo_user",
          "default":null
        },
        {
          "id":"backend_mongo_password",
          "label":"MongoDB Password",
          "type":"password",
          "mandatory":true,
          "info":"Password for MongoDB backend authentication.",
          "placeholder":"Enter your password",
          "default":null
        },
        {
          "id":"backend_mongo_host",
          "label":"MongoDB Host",
          "type":"text",
          "mandatory":true,
          "info":"Host address for the MongoDB instance used by the backend.",
          "placeholder":"localhost",
          "default":null
        },
        {
          "id":"backend_mongo_port",
          "label":"MongoDB Port",
          "type":"integer",
          "mandatory":true,
          "info":"Port number for MongoDB connection in the backend.",
          "placeholder":"27017",
          "default":null
        },
        {
          "id":"backend_mongo_database",
          "label":"MongoDB Database",
          "type":"text",
          "mandatory":true,
          "info":"Database name for MongoDB in the backend.",
          "placeholder":"portal",
          "default":null
        }
      ]
    },
    {
      "name":"MongoDB",
      "provider":"APP_OWNER",
      "settings":[
        {
          "id":"mongo_initdb_root_username",
          "label":"Root Username",
          "type":"text",
          "mandatory":true,
          "info":"Root username for initializing MongoDB.",
          "placeholder":"mongo_root",
          "default":null
        },
        {
          "id":"mongo_initdb_root_password",
          "label":"Root Password",
          "type":"password",
          "mandatory":true,
          "info":"Password for MongoDB root user during initialization.",
          "placeholder":"Enter root password",
          "default":null
        },
        {
          "id":"mongo_initdb_database",
          "label":"MongoDB Database",
          "type":"text",
          "mandatory":true,
          "info":"Database to be created during MongoDB initialization.",
          "placeholder":"portal",
          "default":null
        },
        {
          "id":"mongo_username",
          "label":"MongoDB Username",
          "type":"text",
          "mandatory":true,
          "info":"Username for MongoDB user access.",
          "placeholder":"mongo_user",
          "default":null
        },
        {
          "id":"mongo_password",
          "label":"MongoDB Password",
          "type":"password",
          "mandatory":true,
          "info":"Password for MongoDB user access.",
          "placeholder":"Enter password",
          "default":null
        }
      ]
    },
    {
      "name":"Mongo Express",
      "provider":"APP_OWNER",
      "settings":[
        {
          "id":"me_config_mongodb_adminusername",
          "label":"Mongo Express Admin Username",
          "type":"text",
          "mandatory":true,
          "info":"Username for MongoDB admin access via Mongo Express.",
          "placeholder":"admin_user",
          "default":null
        },
        {
          "id":"me_config_mongodb_adminpassword",
          "label":"Mongo Express Admin Password",
          "type":"password",
          "mandatory":true,
          "info":"Password for MongoDB admin access via Mongo Express.",
          "placeholder":"Enter admin password",
          "default":null
        },
        {
          "id":"me_config_mongodb_url",
          "label":"MongoDB URL",
          "type":"text",
          "mandatory":true,
          "info":"URL for MongoDB instance accessible by Mongo Express.",
          "placeholder":"mongodb://localhost:27017",
          "default":null
        },
        {
          "id":"me_config_basicauth",
          "label":"Basic Authentication",
          "type":"boolean",
          "mandatory":true,
          "info":"Enable or disable basic authentication for Mongo Express.",
          "placeholder":null,
          "default":null
        }
      ]
    }
  ]
}

The definition of the field name and goal can be found in the documentation.

The value of the id field should be:

  • required;
  • unique for all schema;
  • in the snake_case style as to the Naming policy.

Create a file with the value of the variables

Let's create addonmart-tool/config_values.yaml file with options

addonmart-tool/config_values.yaml
options:
  # Frontend
  api_url: http://localhost:5500

  # Backend
  backend_mongo_username: mongo_user
  backend_mongo_password: password
  backend_mongo_host: mongo
  backend_mongo_port: 27017
  backend_mongo_database: portal

  # MongoDB
  mongo_initdb_root_username: mongo_root
  mongo_initdb_root_password: example
  mongo_initdb_database: portal
  mongo_username: mongo_user
  mongo_password: password

  # Mongo Express
  me_config_mongodb_adminusername: mongo_root
  me_config_mongodb_adminpassword: example
  me_config_mongodb_url: mongodb://mongo_root:example@mongo:27017/
  me_config_basicauth: false

Pay attention to following the Naming policy

Using generated environment variable file

Docker Compose uses config.yaml as its configuration file. This approach does not support passing environment variable files directly in the launch command but requires defining them within the configuration file. 

During the run  command processing, it always generates a final file with a set of all environment variables. The location of this file relative to the build  folder is always static and we can count on it. This file will be used in the compose file. 

Using templates

In addition, a separate configuration file is required for MongoDB initialization and setup that also should be dynamically configured.

To ensure proper configuration, we’ll use template files with placeholders for custom variables. During the template processing, these placeholders will be replaced with their actual values and saved in a new file. Then these generated files will be referenced within the Docker Compose file.

We should use a separate template folder and the file name should end with .template postfix.

Note:

  1. Instead of the default delimiters {{ }} around variables use {@ @}
  2. Variables should be in camelCase style.

Learn more in the documentation 

Create a MongoDB initiation file

Make the changes for mongo/init-mongo.js.example file and save it to addonmart-tool/templates/init-mongo.js.template 

BeforeAfter
mongo/init-mongo.js.example
db = db.getSiblingDB('admin');
db.auth('mongo_root', 'example');

db = db.getSiblingDB('portal');
db.createUser({
  user: 'mongo_user',
  pwd: 'password',
  roles: [
    {
      role: 'readWrite',
      db: 'portal',
    },
  ],
});

... other code without changes ...


addonmart-tool/templates/init-mongo.js.template
db = db.getSiblingDB('admin');
db.auth('{@ meConfigMongodbAdminusername @}', '{@ mongoInitdbRootPassword @}');

db = db.getSiblingDB('{@ mongoInitdbDatabase @}');
db.createUser({
  user: '{@ mongoUsername @}',
  pwd: '{@ mongoPassword @}',
  roles: [
    {
      role: 'readWrite',
      db: '{@ mongoInitdbDatabase @}',
    },
  ],
});

... other code without changes ...

Template processing diagram

Update compose YAML file

It needs changes:

  • env_file link form '.env.example' to './addonmart-tool/build/config_values.env'
  • mongo.volumes link from './mongo/init-mongo.js.example:/docker-entrypoint-initdb.d/init-mongo.js' to './addonmart-tool/build/configs/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js'

Listing the changed compose.yaml file

compose.yaml
version: '3.3'

services:
  web:
    build:
      context: './frontend'
    ports:
      - '80:80'
    env_file: './addonmart-tool/build/config_values.env'
    depends_on:
      - back
    networks:
      - portal-network

  back: 
    build:
      context: './backend'
      target: builder
    ports:
      - '5500:80'
    env_file: './addonmart-tool/build/config_values.env'
    depends_on:
      - mongo
    networks:
      - portal-network
  
  mongo:
    container_name: mongo
    image: mongo
    ports:
      - 27017:27017
    env_file: './addonmart-tool/build/config_values.env'
    volumes:
      - mongo_data:/data/db
      - ./addonmart-tool/build/configs/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js
    networks:
      - portal-network

  mongo-express:
    image: mongo-express
    ports:
      - 8081:8081
    env_file: './addonmart-tool/build/config_values.env'
    depends_on:
      - mongo
    networks:
      - portal-network

networks:
  portal-network:
    driver: bridge

volumes:
  mongo_data:

First launch

Overview of the current file structure of the application folder before running (just 3 levels):

$ tree -L 3
.
├── addonmart-tool
│   ├── config_schema.json
│   ├── config_values.yaml
│   └── templates
│       └── init-mongo.js.template
├── backend
│   ├── app
│   │   └── main.py
│   ├── Dockerfile
│   └── requirements.txt
├── compose.yaml
├── frontend
│   ├── angular.json
│   ├── Dockerfile
│   ├── nginx.conf
│   ├── package.json
│   ├── package-lock.json
│   ├── scripts
│   │   └── init_container.sh
│   ├── src
│   │   ├── app
│   │   ├── assets
│   │   ├── environments
│   │   ├── favicon.ico
│   │   ├── index.html
│   │   ├── main.ts
│   │   └── styles.css
│   ├── tsconfig.app.json
│   ├── tsconfig.json
│   └── tsconfig.spec.json
└── mongo
    └── init-mongo.js.example

We can launch the application with the run command.

$ addon-mart run \
  --configuration-schema ./addonmart-tool/config_schema.json \
  --local-values         ./addonmart-tool/config_values.yaml \
  --templates            ./addonmart-tool/templates \
  --build                ./addonmart-tool/build \
  --compose-config       ./compose.yaml

Pay attention to the new --templates ./addonmart-tool/templates  and  --compose-config ./compose.yaml  argument

Output
INFO: > validating configuration values...
INFO: reading ./addonmart-tool/config_schema.json file...
INFO: reading ./addonmart-tool/config_values.yaml file...
INFO: creating ./addonmart-tool/build/config_values.env file...
SUCCESS: configuration values are valid.
INFO: > starting templates processing...
INFO: processing init-mongo.js.template template...
SUCCESS: all templates have been processed successfuly.
INFO: running the app using compose.yaml...
SUCCESS: the app has been successfully started.
INFO: please use "docker compose --env-file ./addonmart-tool/build/config_values.env --file ./compose.yaml logs" to see logs.
INFO: please use "docker compose --env-file ./addonmart-tool/build/config_values.env --file ./compose.yaml stop" to stop the app.

Checking the status and managing the container

Check the running web application on the localhost: http://localhost/. Try to use the initial credential to log in: login 'admin' with password 'admin'.

If it is OK, you should redirected to the home page with "Hi Admin! You're logged in on the Portal!!" salutation.

API specification should be available on http://localhost:5500/docs , http://localhost:5500/redoc pages. Mongo Express should be also available on http://localhost:8081/.

Recheck the Backend API:

 $ curl -s localhost:5500/health | jq
{
  "status": "OK"
}

Let's look at all running application containers:

 $ docker ps 
CONTAINER ID   IMAGE           COMMAND                  CREATED          STATUS          PORTS                                           NAMES
09d4a59a8152   step_4_web      "/docker-entrypoint.…"   11 minutes ago   Up 11 minutes   0.0.0.0:80->80/tcp, :::80->80/tcp               step_4-web-1
17ed1aaf25e1   mongo-express   "/sbin/tini -- /dock…"   11 minutes ago   Up 11 minutes   0.0.0.0:8081->8081/tcp, :::8081->8081/tcp       step_4-mongo-express-1
84f311e21f7d   step_4_back     "uvicorn main:app --…"   11 minutes ago   Up 11 minutes   0.0.0.0:5500->80/tcp, :::5500->80/tcp           step_4-back-1
6e7ddc5eaefb   mongo           "docker-entrypoint.s…"   11 minutes ago   Up 11 minutes   0.0.0.0:27017->27017/tcp, :::27017->27017/tcp   mongo

Recheck the passed environment variables:

$ docker exec step_4-web-1 env | sort
API_URL=http://localhost:5500
BACKEND_MONGO_DATABASE=portal
BACKEND_MONGO_HOST=mongo
BACKEND_MONGO_PASSWORD=password
BACKEND_MONGO_PORT=27017
BACKEND_MONGO_USERNAME=mongo_user
HOME=/root
HOSTNAME=09d4a59a8152
ME_CONFIG_BASICAUTH=0
ME_CONFIG_MONGODB_ADMINPASSWORD=example
ME_CONFIG_MONGODB_ADMINUSERNAME=mongo_root
ME_CONFIG_MONGODB_URL=mongodb://mongo_root:example@mongo:27017/
MONGO_INITDB_DATABASE=portal
MONGO_INITDB_ROOT_PASSWORD=example
MONGO_INITDB_ROOT_USERNAME=mongo_root
MONGO_PASSWORD=password
MONGO_USERNAME=mongo_user
NGINX_VERSION=1.25.4
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PKG_RELEASE=1

The final version of the application code at this stage is available for download via this link.

Optimization of the schema and variables

Using an auto-generated environment variable file limits us in choosing a name on the configuration schema, due to the names of the variables that need to be generated. As a result, we see excessive duplication of option values, which will always be the same.

Let's try using Templates and significantly improve the configuration. As a result, our configuration schema will be significantly simplified, which will improve its use by the end user.

Templates offer benefits over a basic set of auto-generated environment variable pairs. They can be tailored to any format and combination, eliminating redundant values.

Improved variables file

addonmart-tool/config_values.yaml
options:
  # Frontend
  api_url: http://localhost:5500

  # Backend
  mongo_host: mongo

  # MongoDB
  mongo_root_username: mongo_root
  mongo_root_password: example
  mongo_port: 27017
  mongo_database: portal
  mongo_username: mongo_user
  mongo_password: password

  # Mongo Express
  me_config_basicauth: false

Pay attention, the variable's names are also changed

Improved schema file

addonmart-tool/config_schema.json
{
  "groups":[
    {
      "name":"Frontend",
      "provider":"APP_OWNER",
      "settings":[
        {
          "id":"api_url",
          "label":"API URL",
          "type":"text",
          "mandatory":true,
          "info":"URL for the API endpoint that the frontend will interact with.",
          "placeholder":"https://api.example.com",
          "default":null
        }
      ]
    },
    {
      "name":"Backend",
      "provider":"APP_OWNER",
      "settings":[
        {
          "id":"mongo_host",
          "label":"MongoDB Host",
          "type":"text",
          "mandatory":true,
          "info":"Host address for the MongoDB instance used by the backend.",
          "placeholder":"localhost",
          "default":null
        }
      ]
    },
    {
      "name":"MongoDB",
      "provider":"APP_OWNER",
      "settings":[
        {
          "id":"mongo_root_username",
          "label":"Root Username",
          "type":"text",
          "mandatory":true,
          "info":"Root username for initializing MongoDB.",
          "placeholder":"mongo_root",
          "default":null
        },
        {
          "id":"mongo_root_password",
          "label":"Root Password",
          "type":"password",
          "mandatory":true,
          "info":"Password for MongoDB root user during initialization.",
          "placeholder":"Enter root password",
          "default":null
        },
        {
          "id":"mongo_port",
          "label":"MongoDB Port",
          "type":"integer",
          "mandatory":true,
          "info":"Port number for MongoDB connection in the backend.",
          "placeholder":27017,
          "default":null
        },
        {
          "id":"mongo_database",
          "label":"MongoDB Database",
          "type":"text",
          "mandatory":true,
          "info":"MongoDB application Database.",
          "placeholder":"portal",
          "default":null
        },
        {
          "id":"mongo_username",
          "label":"MongoDB Username",
          "type":"text",
          "mandatory":true,
          "info":"Username for MongoDB user access.",
          "placeholder":"mongo_user",
          "default":null
        },
        {
          "id":"mongo_password",
          "label":"MongoDB Password",
          "type":"password",
          "mandatory":true,
          "info":"Password for MongoDB user access.",
          "placeholder":"Enter password",
          "default":null
        }
      ]
    },
    {
      "name":"Mongo Express",
      "provider":"APP_OWNER",
      "settings":[
        {
          "id":"me_config_basicauth",
          "label":"Basic Authentication",
          "type":"boolean",
          "mandatory":true,
          "info":"Enable or disable basic authentication for Mongo Express.",
          "placeholder":null,
          "default":null
        }
      ]
    }
  ]
}

Create Environment variable template file

To ensure proper configuration, we’ll use template files with placeholders for custom variables. During the template processing, these placeholders will be replaced with their actual values and saved in a new file. These generated files will then be referenced within Docker Compose.

We should use a separate template folder and the file name should end with .template postfix.

Note:

  1. Instead of the default delimiters {{ }} around variables use {@ @}
  2. Variables should be in camelCase style.

Learn more in the documentation 

This environment variable file consists of lines with a name/value pair for each one. We use the UPPERCASE_WITH_UNDERSCORES naming style for environment variable names, but for value placeholders, we use the camelCase naming style.

addonmart-tool/templates/.env.template
# Frontend
API_URL={@ apiUrl @}

# Backend
BACKEND_MONGO_USERNAME={@ mongoUsername @}
BACKEND_MONGO_PASSWORD={@ mongoPassword @}
BACKEND_MONGO_HOST={@ mongoHost @}
BACKEND_MONGO_PORT={@ mongoPort @}
BACKEND_MONGO_DATABASE={@ mongoDatabase @}

# MongoDB
MONGO_INITDB_ROOT_USERNAME={@ mongoRootUsername @}
MONGO_INITDB_ROOT_PASSWORD={@ mongoRootPassword @}
MONGO_INITDB_DATABASE={@ mongoDatabase @}

# Mongo Express
ME_CONFIG_MONGODB_ADMINUSERNAME={@ mongoRootUsername @}
ME_CONFIG_MONGODB_ADMINPASSWORD={@ mongoRootPassword @}
ME_CONFIG_MONGODB_URL=mongodb://{@ mongoRootUsername @}:{@ mongoRootPassword @}@{@ mongoHost @}:{@ mongoPort @}/
ME_CONFIG_BASICAUTH={@ meConfigBasicauth @}

Update compose YAML file

The env_file value should point to the generated env file './addonmart-tool/build/configs/.env'

Listing the changed compose.yaml file

compose.yaml
version: '3.3'

services:
  web:
    build:
      context: './frontend'
    ports:
      - '80:80'
    env_file: './addonmart-tool/build/configs/.env'
    depends_on:
      - back
    networks:
      - portal-network

  back: 
    build:
      context: './backend'
      target: builder
    ports:
      - '5500:80'
    env_file: './addonmart-tool/build/configs/.env'
    depends_on:
      - mongo
    networks:
      - portal-network
  
  mongo:
    container_name: mongo
    image: mongo
    ports:
      - 27017:27017
    env_file: './addonmart-tool/build/configs/.env'
    volumes:
      - mongo_data:/data/db
      - ./addonmart-tool/build/configs/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js
    networks:
      - portal-network

  mongo-express:
    image: mongo-express
    ports:
      - 8081:8081
    env_file: './addonmart-tool/build/configs/.env'
    depends_on:
      - mongo
    networks:
      - portal-network

networks:
  portal-network:
    driver: bridge

volumes:
  mongo_data:

Final launch

Overview of the current file structure of the addonmart-tool  folder before running:

$ tree -a addonmart-tool/
addonmart-tool/
├── config_schema.json
├── config_values.yaml
└── templates
    ├── .env.template
    └── init-mongo.js.template

We can launch the application with the run command.

$ addon-mart run \
  --configuration-schema ./addonmart-tool/config_schema.json \
  --local-values         ./addonmart-tool/config_values.yaml \
  --templates            ./addonmart-tool/templates \
  --build                ./addonmart-tool/build \
  --compose-config       ./compose.yaml

INFO: > validating configuration values...
INFO: reading ./addonmart-tool/config_schema.json file...
INFO: reading ./addonmart-tool/config_values.yaml file...
INFO: creating ./addonmart-tool/build/config_values.env file...
SUCCESS: configuration values are valid.
INFO: > starting templates processing...
INFO: processing .env.template template...
INFO: processing init-mongo.js.template template...
SUCCESS: all templates have been processed successfuly.
INFO: running the app using compose.yaml...
SUCCESS: the app has been successfully started.
INFO: please use "docker compose --env-file ./addonmart-tool/build/config_values.env --file ./compose.yaml logs" to see logs.
INFO: please use "docker compose --env-file ./addonmart-tool/build/config_values.env --file ./compose.yaml stop" to stop the app.

Let's examine the template processing result for the environment variable file:

 $ tree -a addonmart-tool/build/
addonmart-tool/build/
├── configs
│   ├── .env
│   └── init-mongo.js
└── config_values.env
addonmart-tool/build/configs/.env
 # Frontend
API_URL=http://localhost:5500

# Backend
BACKEND_MONGO_USERNAME=mongo_user
BACKEND_MONGO_PASSWORD=password
BACKEND_MONGO_HOST=mongo
BACKEND_MONGO_PORT=27017
BACKEND_MONGO_DATABASE=portal

# MongoDB
MONGO_INITDB_ROOT_USERNAME=mongo_root
MONGO_INITDB_ROOT_PASSWORD=example
MONGO_INITDB_DATABASE=portal

# Mongo Express
ME_CONFIG_MONGODB_ADMINUSERNAME=mongo_root
ME_CONFIG_MONGODB_ADMINPASSWORD=example
ME_CONFIG_MONGODB_URL=mongodb://mongo_root:example@mongo:27017/
ME_CONFIG_BASICAUTH=False

Check the running web application on the localhost: http://localhost/. Try to use the initial credential to log in: login 'admin' with the password 'admin'.

Recheck the Backend API:

 $ curl -s localhost:5500/health | jq
{
  "status": "OK"
}

Application configuration approaches

This section provides an overview of configuration approaches for microservices. Our tutorial application's setup is simplified, with some functions streamlined or omitted, but it’s essential to consider how microservices can be configured.

There are two main approaches for configuring an application:

  • Configuration via Environment Variables
  • Configuration via Configuration Files

Configuration via Environment Variables

Our Frontend and Backend services use environment variables for configuration. During startup, these variables are passed to the container as key-value pairs or as an entire file.

Single-Component Application with Dockerfile

We already used this command previously here for running just the Frontend service:

$ addon-mart run \
  --configuration-schema ./addonmart-tool/config_schema.json \
  --local-values         ./addonmart-tool/config_values.yaml \
  --build                ./addonmart-tool/build \
  --app-dir              ./ \
  --app-name             aom-web-app:1.0.1 \
  --port                 80:80

Under the hood, the config_values.env  file is created in the build directory and then passed to the container in its entirety.

Multi-Component Service with Docker Compose file

For multi-component services, the configuration is managed via a Docker Compose file. This approach allows you to specify the path to the configuration file with env_file instructions in the Docker Compose file. An example can be found in this section.

Configuration via Configuration Files

We already introduced using configuration files in the initial application preparation for MongoDB. So, it’s crucial to adhere to the configuration file's format and syntax, based on the service's documentation or a custom parser for our services.

Templates and their processing are integral to this approach.

These templates can later be used for staging or production deployments.

For instance, the Backend configuration can utilize a configuration file located in the application folder instead of environment variables.

Example of a YAML Configuration File for Backend service:

config.yaml.example
app:
  mongo:
    username: mongo_root
    password: example
    host: mongo
    port: 27017
    database: portal

let's create a template based on it and put the file in the templates folder with '.template' postfix in the filename:

addonmart-tool/templates/config.yaml.template
app:
  mongo:
    username: {@ mongoUsername @}
    password: {@ mongoPassword @}
    host: {@ mongoHost @}
    port: {@ mongoPort @}
    database: {@ mongoDatabase @}

Single-Component Application with Dockerfile

In this case, we also should create templates_mapping.yaml and point local and contained application paths accordingly.

addonmart-tool/templates_mapping.yaml
mapping:
  - template: config.yaml.template
    location: /app/config.yaml

Do not forget to use --templates-mapping arguments with the mapping file in the command

Multi-Component Service with Docker Compose file

For such cases, configuration is handled entirely through the Docker Compose file via volumes instruction

compose.yaml
...
  back:
    ...
    volumes:
      - ./addonmart-tool/build/configs/config.yaml:/app/config.yaml
...

Summary:

Working with the Application

The final version of the application code is available for download via this link.

How to Run the Application

Use the following command:

$ addon-mart run \
  --configuration-schema ./addonmart-tool/config_schema.json \
  --local-values         ./addonmart-tool/config_values.yaml \
  --templates            ./addonmart-tool/templates \
  --build                ./addonmart-tool/build \
  --compose-config       ./compose.yaml

Checking the Application

Frontend: Access the frontend http://localhost/ using the provided credentials (login 'admin', password 'admin').

Backend: Refer to the documentation and example CURL request here (link) to test backend functionality.

API specification should be available on http://localhost:5500/docs , http://localhost:5500/redoc pages. Mongo Express should be also available on http://localhost:8081/.

Recheck the Backend API:

 $ curl -s localhost:5500/health | jq
{
  "status": "OK"
}

Audit Tools

You have access to a complete set of Docker and Docker Compose CLI tools for container management. For example, to view logs

$ docker compose --file ./compose.yaml logs

How to Stop the Application

To stop the application, use stop  command

$ docker compose --file ./compose.yaml stop

How to Remove artifacts

$ docker compose --file ./compose.yaml down

It stops and removes containers, networks, and images created by up.

By default, the only things removed are:

  • Containers for services defined in the Compose file.
  • Networks defined in the networks section of the Compose file
  • The default network, if one is used
$ docker compose --file ./compose.yaml down -v

Option -v, --volumes  is used for removing named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers. Note, the DB data will also be deleted.

Local Development for Marketplace

Applications intended for marketplace deployment should meet certain requirements:

  • Containerized as independent components
  • Configuration schema
  • Add-On Mart deployment files for deploy

Early adoption of the configuration schema can help identify issues sooner and speed up the overall preparation process.

To support the preparation, we have developed tools to streamline this process:

  • Add-On Mart Tool: For efficient management of AddonMart files as well as testing, submitting
  • Configuration Schema UI Generator: For creating application configuration schemas
  • Guides and Documentation: Public space contains manuals and guidelines

Staging and Marketplace Publication

Upon completing this guide, you can move on to the next steps in staging and publication. Details for this phase are available in the documentation for the next stage.