šŸ“š Amm0's Manual for "Custom" /app containers (7.22beta+)

Since there is no documentation on "custom" /app introduced in 7.22beta3, I let AI initially create one, since it offered. Basically gave Claude all YAML examples from builtin /app to store out what might be possible for a "custom" /app.

I have "manually" edited it a bunch since original. Still not 100% (but neither is feature), but should capture all the known YAML attributes, and analyze how /app YAML differs from docker-compose since I wanted that info. Along with some "tips" on to create a "Custom /app".

Content is broken up into three posts since it exceeds 32K limited of a forum post

Temporary Manual for RouterOS "Custom /app" Containers

Custom /app Prerequisites

The new /app feature is partially documented in MikroTik's help.mikrotik.com docs. This document assumes some familiarity with RouterOS containers, and /app has been fully configured.

To use /app, you must:

  • Have MikroTik with ARM64 or X86 CPU (or CHR), with some disk storage.

    Neither Flash/NAND nor RouterOS system disk can be used as disk, but even cheap USB flash drive can work for /app - but critically some disk is needed.

  • container.npk has been installed (or /system/package enable container performed)
  • Device allows containers features via /system/device-mode update container=yes and power-cycled
  • /app/setup has been run, include setting a disk, router-ip (used in NAT rules), and bridge set to the default (or desired) LAN.

For more help on RouterOS /container generally, see:

Deployment via /app/add

The MikroTik RouterOS YAML format is specifically designed to work with the /app/add command via the yaml= attribute. This imports the YAML definition and creates the necessary RouterOS configuration for a container, or group of containers (a la docker-compose).

RouterOS's /app YAML format "borrows" from docker-compose syntax — but RouterOS YAML is not full compatible. Differences between compose and RouterOS app YAML are discussed in a later section.

Adding an Application

RouterOS "custom" /app containers can loaded inline using YAML. Basic example of adding a app from RouterOS CLI:

/app add disabled=no yaml="
name: cligames
descr: Amm0's BSD games for RouterOS
page: https://github.com/tikoci/cligames
category: games
icon: https://wiki.pine64.org/images/b/bc/BSD_Unix_icon.png
default-credentials: joshua:(none)
services:
  cligames:
    image: ghcr.io/tikoci/cligames:latest
    environment:
      TERM: screen
    ports:
      - 2323:23/tcp:telnet
    restart: unless-stopped
"

/app/add Parameters

While most app definition happens in YAML. Some options can be provided via the RouterOS CLI in the /app/add. But some app configuration is only available via YAML.

Exception: currently pvid can only be set via attributes, not YAML. Although the YAML syntax may just unknown, not missing.

auto-update      -- Enable/disable automatic container updates
disabled         -- Disable the application without removing it
environment      -- Override environment variables from YAML
extra-mounts     -- Add additional volume mounts beyond YAML definition
firewall-redirects -- Override port mappings from YAML
hw-device-access -- Grant access to hardware devices
network          -- Override network configuration
pvid             -- VLAN ID for the network
required-mounts  -- Override required volume mounts
use-https        -- Enable HTTPS reverse proxy (see below)
yaml             -- The YAML configuration content

Any /app/add attributes will override or extend YAML settings. i.e. If conflict between YAML definition and /app attributes, RouterOS attributes win.

The operation of network, use-https, and auto-update are discussed below in context of YAML, but works same if provided via CLI attributes.

Most of the custom app action happens in the YAML definition (using yaml= provided to /app/add), discussed in later in the document. But first some basics on how /app works.

/app/add Operations

When you run /app/add command, RouterOS will:

  1. Creates a new entry in /app, that be enabled or disabled at any time. Custom app are marked with the c flag in /app/print or UI.

  2. /app/add attributes and YAML file are parsed and various readonly /app attributes are initialized.

    The readonly cmds= is also calculated based on app config. cmds is what's used for any "dynamic configuration" of RouterOS needed for containers operations. The cmds are run both when an app is enabled, and at each boot if auto-update=yes_ Side note: Somehow, cmds are removed when an app is disabled, even without any corresponding "cleanup" commands & odd since cmds use RouterOS add commands.

  3. File paths are calculated, based the /app/settings disk= value, and top-level app name (based on YAML's name:, not the /app/get ... name), along with service container(s) or mount(s) name. In YAML, you never specify the disk, instead YAML always assumes a relative path.

    • root-dir is calculated using:

      <disk>/app/<app-name>/<service-name>_root
      
    • Any /app-defined Mounts are calculated and created in:

      <disk>/app/<app-name>/<mount-name>
      
    • RouterOS may use additional directories like .backup internally, but also under the <disk>/app/<app-name> file path.

  4. Networking is automatically setup. This is most variable and complex part of /app to understand. Each /app gets a single VETH named veth-app-<app-name>. And all services share one VETH. i.e. For a multi-container app, a single VETH is shared by all containers created in the /app definition. Some considerations:

    • Custom apps that use the internal (e.g. no network: specified in YAML), are added to /app-created bridge named internal, and any ports: defined become dst-nat rules and mapped automatically to the /app/settings router-ip (or assumed-router-ip if router-ip is not set)

    • Also, with the internal (or default) bridge is automatically created as part of /app/setup, and added to the LAN interface list. And a src-nat with masquerade is used on the internal bridge, so all container IPs are NATed to rest of network. e.g. thus it is a requirement that /ip/firewall/filter allows the interface list LAN access, in order for containers to work using the default internal bridge.

    • If a bridged mode is specified in YAML via:

      networks:
        default:
          name: lan
          external: true
      

      or /app/add network=lan, the VETH is added to the lan-bridge set in /app/settings. Additionally, and importantly, the VETH is set with dhcp=yes, so container IP address is obtained dynamically by a VETH-added dynamic /ip/dhcp-client. Currently, there is no option to use a static IP with "external"/lan-bridge bridge.

      VETH interface only recently added support for "DHCP" as option, which /app uses automatically for external bridging. Previously VETH was always configured using static IPs, so this may be confusing to some. But does require the external container bridge to use a DHCP server.

    • Each /app may be configured with pvid, and used if bridge is set with vlan-filtering=yes. This is not used when internal bridge is configured, since it not VLAN-aware. The pvid option does not appear in YAML, so must be use directly set on the /app.

    • In all cases, variables like [containerIP] (and others) are populated based on selected networking mode. These variables may be used any environment= or YAML environment: and will be automatically replaced when used with the calculated value (as shown in /app/print detail where name=<app-name>).

  5. When an app is enabled - either later (via /app/enable myapp), or when adding custom app definition via /app/add disabled=no ... - any containers defined will be created in /container, with any mounts and environment variables defined in /app copied directly to /container/add with any [variables] and file paths resolved.

    Previous /container required the use of /container/env and /container/mount, however newer RouterOS versions allow these to be defined in a /container/add, which /app takes advantage.

  6. RouterOS will start the process to obtain the image (and it's layers), and start the /container. RouterOS GUI's will show the progress of enabling a /app container in red above the App. Any errors (like DNS failures) will be shown, and RouterOS will continue to try to download and start any configuration containers. The same information can be view using /app print follow-only where !disabled

  7. If auto-update=yes, then the /container, /interface/veth, and other dynamically created configuration is periodically checked and updated to match what is defined in /app. Also, a /container/repull may happen after any boot.

    Some dynamic configuration performed by /app may be actually be editable, but critically these edit may be overridden when auto-update runs. e.g. you can disable DHCP on a bridged VETH, but it will be reverted when /app checks configuration.

  8. To remove a custom app, it must first be disabled and cleaned up:

    /app/disable myapp
    /app/cleanup myapp
    

    and then remove the custom app definition from the catalog:

    /app/remove myapp
    

    There may be a bug in 7.22beta where the cleanup and/or removal may take a reboot to take effect.

Further Configuration using /app/set

While /app/add allow a new custom app to be added to the /app catalog. Further customization is available via /app/set after a new app is added.

Some items that may require customization outside of the /app/add

  • pvid and network - Likely most common "customization", with network overriding any YAML definition. Currently, pvid is not even settable via YAML, so must be done via /app/set. For example, if a "management network" uses a seperate VLAN from main LAN, then pvid= option may be used to force an /app to use the management VLAN (instead of an untagged bridged).
  • hw-device-access - Some container apps may use hardware. But the specific hardware may be specific to the router. For example, some IoT containers may use Bluetooth, but the specific radio may internal on some router, but an external USB Bluetooth dongles.
  • auto-update - The default is currently =no, but enabling this option should be considered so future security-related fixes are automatically applied.

Designing a Custom /app

Overview of Networking Options

Network Schemes

There are two main network schemes known, network=default and network=lan.

It may also be possible to define new custom /app/network using YAML but this has not been tested yet, so not described.

1. default - uses /app-created internal bridge and NAT

In most cases, this is what you want for a custom /app. So, if no root networks: section is specified in YAML, or /app's network=default, then RouterOS will automatically:

  • Create (if needed) an isolated bridge named internal with subnet 172.18.0.0/24
  • Create VETH interface for the app, shared by all services, and assigned the next available IP in 172.18.0.0/24 (start at .2 and going up)
  • Uses the internal bridge's IP address (172.18.0.1) as gateway
  • Sets up NAT rules in /ip/firewall/nat for port forwarding based on ports: mappings from internal bridge to the router-ip (or assumed-router-ip if none) in /app/settings.
  • The internal bridge is automatically a member of the LAN group in /interface/list, and the internal bridge is src-nat via masquerade.
2. lan - ethernet bridging using lan-bridge in /app/settings

When network: in provides in YAML, or network=lan is used in /app/add then:

  • Containers attach directly to existing RouterOS bridge (often bridge)
  • Containers get IP via DHCP from the bridge's network, using VETH's dhcp=yes option
  • No NAT port forwarding - direct network access
  • Default bridge is configured in /app/settings set lan-bridge=

Note: There is seemingly no option to not use DHCP on VETH when /app using a LAN bridge.

Communication between "services" i.e. multiple containers within a single /app

Like docker-compose, it possible define multiple containers with a custom /app. Often these related containers need to communicate among themselves. e.g. a webserver container may need a database container.

It is important to understand that all services share a single VETH, so inter-service network communication can use localhost (127.0.0.1). This is the normal mechanism builtin /app use. So in any YAML environment variables (like DB connection strings) or file-based configs: can use localhost and port, to refer to another container exposed network service.

It also possible to use [placeholder] variables, like [ContainerIP] instead of localhost. But this may cause the traffic to flow via the firewall, and be subject to those rules. Depending on /app needs, this may be desirable in some cases to better control access.

Using VLAN with /app

Each app has an attribute pvid=, the can be set when using an "external" bridge to control the VLAN ID used on the associated bridge when vlan-filtering=yes is used on linked bridge. More testing is needed to understand fully usage.

Currently pvid does not appear to be controllable in the YAML.

Updating container files using YAML configs

This is a useful feature for Custom /app's since you can overwrite/create files in the containers root image's file system or potentially mounts. File contents are defined.

Any file on the file system can be written, so it does not necessarily have to be a "config" file.

The link between a file's contents and it's placement is down in two parts. In YAML, there is both a service configs and root configs key used. For example,

name: myapp
services:
  mycontainer:
    image: somerepo.io/myappimage:latest
    configs:
      - source: 'app_config'
        target: '/etc/app/config'
        mode: 0644
configs:
  'app_config':
      content: |
        path /html
        port 80
        

Understanding File Path Mapping

Another concept specific to RouterOS's /app support is how file paths are created. Importantly, all file paths are based on disk in /app/settings.. So in YAML or /app set never use a full RouterOS path - relative paths should also be used, nor use a starting / in a RouterOS file destination/source. Basically do not think about the disk when designing a Custom /app, as RouterOS will prefix the needed path.

RouterOS has conventions on paths are created:

  • Container root-dir is build using <disk>/apps/<app-name>/<service-name>_root. For multi-service /app, there will be multiple directory, one for each service listed in YAML's services: dictionary.
  • Mounts (in YAML, volumes:) are created using similar scheme under <disk>/apps/<app-name>, specifically:
    • data:/data:rw will use <disk>/apps/<app-name>/data on RouterOS file system
    • any paths used in a mount are flattened using a hyphen -, so a mount like etc/config:/config:rw will be at <disk>/apps/<app-name>/etc-config. e.g. using mymount/dir/subdir on left-side of mount will result in mymount-dir-subdir under the <disk>/apps/<app-name>.
    • RouterOS may use additional directories like .backup internally, but also under the <disk>/app/<app-name> file path

About /app's "Variables to Use In Environment"

RouterOS automatically replaces the following [placeholder] variables at deployment time in /container's environment variables. The following known [placeholder] variables are available to custom container apps, and shown in the /app configuration.

Discussed later, but these are similar to docker-compose's ${VAR} variables.

Known [placeholder] Variables

  • [accessIP] - The IP address where the application is accessible (typically the router's IP or container IP depending on network mode)

    • Used in: APP_URL, PUBLIC_URL, NEXTCLOUD_TRUSTED_DOMAINS, ORIGIN, CSRF_TRUSTED_ORIGINS
  • [accessPort] - The host port mapped for the primary web interface (first port in ports: list)

    • Used in: APP_URL, CMD_DOMAIN, FRONTEND_URL, MASTER_URL
  • [accessProto] - Protocol (http or https) based on the use-https setting

    • Used in: APP_URL, SYMFONY__ENV__DOMAIN_NAME, SEARXNG_BASE_URL
  • [containerIP] - The assigned IP address of the container

    • Available for use but not commonly seen in examples
  • [containerInterface] - The generated VETH interface name (e.g., veth-app-myapp)

    • Available for use but not commonly seen in examples
  • [routerIP] - The RouterOS device IP address

    • Available for use but not commonly seen in examples
Example [placeholder] Variable Usage in YAML
...
services:
  mycontainer:
    ...
    environment:
      APP_URL: "[accessProto]://[accessIP]:[accessPort]"
      NEXTCLOUD_TRUSTED_DOMAINS: "[accessIP]"
      PUBLIC_URL: "http://[accessIP]:8016"
      CSRF_TRUSTED_ORIGINS: "http://[accessIP]:8016,http://[accessIP]:8015"
      ORIGIN: "http://[accessIP]:8015"
      SYMFONY__ENV__DOMAIN_NAME: "[accessProto]://[accessIP]:[accessPort]"
      CMD_DOMAIN: "[accessIP]:[accessPort]"
      SEARXNG_BASE_URL: "[accessProto]://[accessIP]:[accessPort]/"
      MASTER_URL: "[accessProto]://[accessIP]:[accessPort]/smokeping/"
      JELLYFIN_PublishedServerUrl: "[accessProto]://[accessIP]"

About "UI URL" (admin-url) in App catalog

Some containers may have some web-based admin UI, and RouterOS /app will use it:

  • In WinBox and WebFig, /app will show a clickable link to open a container's admin UI in the default web browser.
  • If /app/settings has show-in-webfig=yes, then any admin UI will appear on WebFig's Login screen, along with any icon: configured in a custom app's YAML.

When deployed with use-https=no and the application gets IP 192.168.74.139 with port 8088, the admin-url is calculated internally using the [placeholder] variables, so

[accessProto] → http
[accessIP] → 192.168.88.139
[accessPort] → 8088

becomes "UI URL" shown in WinBox/WebFig, like http://192.168.88.139:8088

When defining an app, the ports fields with the label of web is used to determine the accessPort used in the UI URL (admin-url) show. Without a port having label web, no UI URL will be shown.
Additionally, if use-https=yes, but does not work due to error (like failure to get certificates or CHR without /ip/cloud), no UI URL will be shown.

Enabling secure HTTPS using reverse-proxy

Automatically via use-https=yes and /ip/cloud's DDNS

When use-https=yes is set on a /app, RouterOS will automatically:

  • Creates a hidden reverse proxy rule in /ip/reverse-proxy
  • Redirects HTTP requests to HTTPS based on SNI, in form <app-name>.<device-hash>.routingtheworld.com
  • Automatically uses a Let's Encrypt certificates from /ip/cloud
  • Applies to the url-path shown in an enabled /app

The behavior should be automatic based on the default ddns-enabled=auto in /ip/cloud, and MikroTik's default firewall. If you have issues, use /certificate/enable-ssl-certificate which needs to succeed for the /app's automatic SSL certificate support to work.

Custom DNS domains using dns-names and /certificate/enable-ssl-certificate

More placeholder since any steps needs to be tested & support for "custom DNS domain" is not currently a feature of /app so it's manual process after adding a custom /app.

The default for use-https=yes is to use the routingtheworld.com domain. However, you may want to use your own DNS domain, and add DNS CNAME record to your DNS zone pointing to the /ip/cloud DDNS name.

This is not really possible automatically via /app/add today. But you can use /certificate/enable-ssl-certificate with a dns-names= with a list of all possible /app DNS names you want. And the certificate used in /ip/reverse-proxy SNI entry can point to the generated Let's Encrypt certificate, and set to the /app container's IP address and port. Wildcards are not supported in enable-ssl-certificate

Manually use custom certificates

It also possible to use custom certificates, however you must set use-https=no, and then manually add any SNI in /ip/reverse-proxy to the /app container's IP address and http port. This is more complex topic, outside of building a custom /app. See :writing_hand: Example: Creating self-signed certificates.

Important The [ContainerIP] is not a variable available to the reverse-proxy, so script is likely needed today to keep /ip/reverse-proxy in sync with current container IP use in /app. This is important since the container IP can change if /app enable/disable are used since /app aggressively keeps IP addresses sequentially.

Best Practices

AI thought "we" needed this section...but consider the "Best Practices" as TBD. i.e. "Best Practices" can only come with time.

  1. Credentials: Always change default passwords in production
  2. Port Labels: Use descriptive labels for clarity (web, api, database)
  3. Restart Policy: Use unless-stopped for most services
  4. Environment Variables: Group related variables logically
  5. Security: Avoid exposing unnecessary ports externally

/app Command Summary

After adding an application, you can manage it through /app:

# List all applications
/app/print

# "Tail" applications during operations
/app/print follow-only where !disabled

# Show specific application
/app/print detail where name=myapp

# If enabled, container will have additional details:
/container/print detail where name=app-myapp

# Enable/disable an application
/app/enable myapp 
/app/disable myapp

# Start/stop an application once "enabled"
/container/start app-myapp
/container/stop app-myapp

# Remove an custom application
/app/cleanup myapp
/app/remove myapp

Note that app-myapp is used in /container commands, while myapp is used in /app, since the app- prefix is added to any created /app containers.

5 Likes

YAML Schema Documentation

TIP To enable schema validation, including completion, in VSCode (and other editors), see:

šŸ“š Amm0's Manual for "Custom" /app containers (7.22beta+) - #4 by Amm0

Overview

This schema defines the structure for deploying containerized applications on MikroTik RouterOS. An /app's YAML content (yaml=) represents a single application deployment "template". All necessary "services" (containers), overloaded config files, allowed ports, networking mode, and other custom /app's default configuration can be defined using YAML. Note RouterOS attributes on a /app will override any YAML definition.

Complete YAML Example

Specific attributes are discussed later. But the following examples show all, or perhaps most, YAML options:

# Root-level metadata properties
name: comprehensive-app
descr: A comprehensive example demonstrating all available YAML schema options for MikroTik RouterOS container deployments
page: https://example.com/comprehensive-app
category: productivity
default-credentials: admin:SuperSecurePass123
icon: https://example.com/assets/comprehensive-app-logo.png
url-path: /dashboard
auto-update: false

# Service definitions
services:
  # Database - demonstrates environment (object), volumes, user, shm_size, stop_grace_period
  database:
    image: docker.io/postgres:16-alpine
    container_name: app-db
    hostname: database
    user: "999:999"
    environment:
      POSTGRES_DB: comprehensive
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: DatabasePass456
    volumes:
      - database/data:/var/lib/postgresql/data
      - database/backups:/backups
    restart: unless-stopped
    stop_grace_period: 30s
    shm_size: "256mb"

  # Application - demonstrates all port variations, depends_on, configs with mode
  app:
    image: ghcr.io/example/comprehensive-app:latest
    container_name: app-server
    hostname: appserver
    ports:
      - 8080:80:web
      - 8443:443/tcp:web-https
      - 9090:9090/udp:metrics
      - 5432:5432:database-proxy
    environment:
      DB_HOST: 127.0.0.1
      DB_NAME: comprehensive
      APP_URL: "[accessProto]://[accessIP]:[accessPort]"
      TRUSTED_DOMAINS: "[accessIP]"
    volumes:
      - app/data:/var/app/data
      - app/logs:/var/app/logs
    depends_on:
      - database
      - worker
    configs:
      - source: 'app_config'
        target: '/etc/app/config.yaml'
        mode: 0644
      - source: 'init_script'
        target: '/docker-entrypoint.sh'
        mode: 0755
    restart: unless-stopped

  # Worker - demonstrates environment (array) and command (array)
  worker:
    image: ghcr.io/example/comprehensive-app:latest
    container_name: app-worker
    environment:
      - WORKER_MODE=true
      - PUID=1000
      - PGID=1000
    command:
      - php
      - artisan
      - queue:work
      - --verbose
    volumes:
      - app/data:/var/app/data
    restart: unless-stopped

  # Proxy - demonstrates command (string) and security_opt
  proxy:
    image: docker.io/nginx:alpine
    container_name: app-proxy
    security_opt:
      - seccomp:unconfined
      - no-new-privileges:true
    command: nginx -g 'daemon off;'
    ports:
      - 80:80/tcp:http
    configs:
      - source: 'nginx_config'
        target: '/etc/nginx/nginx.conf'
    restart: unless-stopped

# Configuration file definitions
configs:
  'app_config':
    content: |
      server:
        host: 0.0.0.0
        port: 80
      database:
        driver: postgresql
        pool_size: 10

  'init_script':
    content: |
      #!/bin/bash
      echo "Initializing application..."
      php artisan migrate --force

  'nginx_config':
    content: |
      events {
          worker_connections 1024;
      }
      http {
          server {
              listen 80;
              location / {
                  proxy_pass http://127.0.0.1:8080;
              }
          }
      }

# Network configuration
networks:
  default:
    name: lan
    external: true

Root-Level Properties

name (string)

Description: Unique identifier for the application

Format: Lowercase, hyphenated (e.g., my-app, code-server)

Example: "nextcloud", "grafana"

descr (string)

Description: Brief description of the application's purpose

  • Length: One sentence, typically 50-150 characters

Example: "Self-hosted productivity platform for file sharing"

category (string)

Description: Application category for organization

  • Valid Values:
    • productivity
    • media
    • media-management
    • networking
    • development
    • communication
    • storage
    • file-management
    • monitoring
    • search
    • database
    • automation
    • security
    • home-automation
    • ai
    • video
    • radio
    • messaging

services (object)

Description: Dictionary of container services to deploy

  • Structure: Key-value pairs where keys are service names
  • See: Services Section for details

page (string - URL)

Description: Official website or project homepage

Format: Valid URL (http/https)

Example: "https://nextcloud.com"

default-credentials (string)

Description: Default login credentials for the application and shown in /app UI for end-user to know how to login the first time. It a free-form string value.

Example Usages:

  • "username:password" - Standard credentials
  • "web setup" - Setup wizard required
  • "<no_username>:password" - Password-only login
  • "admin:<check logs for password>" - Password in container logs
  • "file /path/to/password" - Password stored in file
  • Empty/not specified - No authentication or varies

icon (string - URL)

Description: URL to application icon/logo

Format: Valid image URL

Example: "https://example.com/logo.png"

url-path (string)

Description: Additional path to append to base URL for accessing the application. See About "Admin URL" for details.

Format: Path starting with /

Example: "/admin", "/web"

auto-update (boolean)

Description: Whether to automatically update the container

  • Default: true (implied)

Example: false

networks (object)

Description: Custom network configuration

configs (object)

Description: Configuration file definitions for inline content

Services Configuration

Each service entry under services can contain the following properties:

image (string) REQUIRED

Description: OCI ("docker") image from a registry to use as container

Format: [registry/]image[:tag]

While registry is optional, it highly recommend to be explict about the registry to be used. The default registry used when one is not provided is based on /container/config. This may be lscr.io or docker.io depending on version, or may have already been customized by the user to ghcr.io. As a result, to ensure consistency and avoid errors, prepand any container image with the registry.

Example:

  • "docker.io/postgres:16"
  • "lscr.io/linuxserver/jellyfin"
  • "ghcr.io/owner/repo:latest"

container_name (string)

Description: Explicit container name

Example: "gitlab"

hostname (string)

Description: Container hostname

Example: "database"

ports (array of strings)

Description: Port mappings

Format: "host:container[/protocol][:label]", with

  • protocol: Optional /tcp or /udp (defaults to TCP)

  • label: Optional descriptive label for /app UI, and as comment= in dynamic configuration done by /app

A special label web is used on the port that exposes the app's "web admin" if one. Otherwise, the label is just informational. See About UI URL in /app catalog

Example:

  • "8080:80:web" - Host port 8080 to container port 80, labeled "web"
  • "8443:443/tcp:web-https" - Explicit TCP protocol with label
  • "9090:9090/udp:metrics" - UDP protocol with label
  • "3000:3000" - Minimal format without label
  • "53:53/udp:dns" - DNS on UDP without label
  • "5432:5432:database" - Database with label, TCP implied

environment (object or array)

Description: Environment variables

Format: Key-value pairs (object) or array of strings

Example:

environment:
  POSTGRES_DB: database
  POSTGRES_USER: user
  POSTGRES_PASSWORD: password

or

environment:
  - PUID=1000
  - PGID=1000

volumes (array of strings)

Description: Volume mounts

Format: "source:destination[:options]" with source being either

  • Named volume: volumename:/path
  • Prefix notation: app/data:/data (creates app-data volume under default <disk>/apps/<appname>/)

Example:

  • "config:/config"
  • "media/movies:/data/media/movies"
  • "media/audio:/data/audio"
  • "postgres-data:/var/lib/postgresql/data"

command (string or array)

Description: Override container image's default start command (e.g. becomes /container's cmd=)

Example:

command: server /data --console-address ":9001"

or

command:
  - solr-precreate
  - gettingstarted

which will concertante the array to a space-separated string used cmd=.

restart (string)

Description: Restart policy

Valid Values:

  • "unless-stopped" (most common)
  • "always"
  • "no"
  • "on-failure"

depends_on (array of strings)

Description: Service dependencies

Example: ["database", "redis"]

user (string)

Description: User ID or username to run as

Format: "uid:gid" or "username"

Example: "0:0", "1000:1000"

shm_size (string)

Description: Shared memory size

Format: Number with unit (mb, gb)

Example: "256m", "1gb"

security_opt (array of strings)

Description: Security options

Example: ["seccomp:unconfined"]

stop_grace_period (string)

Description: Time to wait before forcefully stopping

Format: Number with unit (s, m)

Example: "30s", "15s"

configs (array of objects)

Description: Reference to configuration files

Properties:

  • source (string): Config name from top-level configs
  • target (string): Path inside container
  • mode (octal): File permissions (optional)

Example:

configs:
  - source: 'nginx_conf'
    target: '/etc/nginx/conf.d/default.conf'
  - source: 'init_script'
    target: '/custom-cont-init.d/init.sh'
    mode: 0755

See Updating Container Files in "Designing a Custom /app" for more details

configs Configuration

While each service has a configs: dictionary, the also a "top-level" configs: (described here). The "two 'configs'" in YAML work in tandem, so the top-level configs: provides the source:

Description: Reference to configuration files

  • Properties:
    • '<file-name>' (object) - file name to create in root layer when /app is enabled. Destination path (and final file name) are determined by usage in a service: container object's configs:.
      • content (string) - under the '<file-name>', is the file contents to place into container, representated a string.

Example:

Inline configuration file can be defined using YAML's | continuation syntax:

configs:
  'config_name':
    content: |
      # File content here
      # Can be multiline

Note: YAML continuations requires the file contents to be aligned using two space indents below content:, but when used will be placed at column 1 in actual file.

It also possible to use a plain YAML string with optional \n's as alternative syntax. However RouterOS interpolatioin may be triggered depending on how YAML is provided to /app/add.

See Updating Container Files in "Designing a Custom /app" for an better overview.

networks Configuration

Properties:

  • network_id (object), with elements
    • name (string): Network/bridge name
    • external (boolean): Whether network already exists in RouterOS

The Overview of Networking Options in "Designing an /app" has more details on networking in /app.

Default internal bridge with dst-nat

To use the default internal bridge with NAT, do not set any top-levels networks:. Any ports defined will become dst-nat rules from the router-ip (set in `/app/settings).

# network:

Use the configured "lan-bridge" for direct network access

If you want to a Custom /app to use the configured lan-bridge in /app/settings, the following YAML can be used:

Example:

networks:
  default:
    name: lan           # Must match an existing RouterOS bridge, or be `lan` for default LAN bridge
    external: true      # Use existing network

Advanced network options

This section is bit unknown. Builtin /app use name: lan as the syntax to refer to the lan-bridge configured in /app/setup originally. If another name: is used, that may refer to an existing bridge interface created manually, but this unknown.

However, /app/network/print does show other bridges as e for external, but referencing them by name or the network key does not seem to cause them to be used.

Use /app/set to control network outside of YAML

It is possible to set additional settings directly in a /app configuration. For example, an alternative bridge may be used, and/or VLAN ID (pvid) may be set. These options are not available, AFAIK, in YAML.

2 Likes

Key Differences from Docker Compose

Format Differences

1. File Structure

MikroTik RouterOS Format:

name: myapp
descr: My application
category: productivity
services:
  web:
    image: nginx
  • No version field support in /app
  • Additional metadata fields for RouterOS UI and /app
  • Deployed via /app/add yaml=myapp.yaml

Docker Compose does allow a version field.

2. Metadata Fields

MikroTik Format has additional metadata for the RouterOS container UI and /app/add:

name: myapp                    # Required application identifier
descr: Application description # Required description
page: https://example.com      # Homepage URL
category: productivity         # Application category
default-credentials: user:pass # Default login info
icon: https://url/icon.png     # Icon URL
url-path: /admin              # URL path suffix
auto-update: false            # Auto-update flag

Docker Compose has no equivalent metadata fields, only a version field is allowed.

3. Port Mapping Format

MikroTik Format:

ports:
  - 8080:80:web                # host:container:label
  - 8443:443/tcp:web-https     # host:container/protocol:label
  - 9090:9090/udp:metrics      # UDP with label
  - 3000:3000                  # Minimal (no protocol, no label)
  - 53:53/udp:dns              # Protocol without label also valid
  • Format: host:container[/protocol][:label]
  • Protocol (/tcp or /udp) is optional, defaults to TCP
  • Label is optional, but recommended, as it is used in various comment= in dynamically created config
  • Protocol comes before label when both are used

Docker Compose uses host:container format with no label support, but does allow protocol.

4. Volume Naming Convention

MikroTik Format:

volumes:
  - app/data:/data          
  - postgres/db:/var/lib    
  - config:/config          
  • Subdirectories allowed, but are "flattened" on RouterOS file system. So prefix/subdir → automatically becomes prefix-subdir
  • Designed for automatic namespacing – based on disk from /app/settings with all apps organized by name under app.
  • No bind mounts supported (no host directory paths, AFAIK)

Docker Compose requires explicit volume declarations in a top-level volumes: section and supports bind mounts to local directories.

5. Service Communication

MikroTik Format:

services:
  app:
    environment:
      DB_HOST: [accessIP]    
      REDIS_HOST: 172.18.0.1
  database:
    image: postgres
  redis:
    image: redis
  • All services communicate via IP address or DNS**, typically localhost.
  • Services share the same network namespace, since all services share a single VETH
  • No DNS-based service discovery by name
  • RouterOS [placeholder] variables can be used, which indirectly contain some /app related IP/DNS addresses (like VETH address or gateway, etc).

Docker Compose uses service names for DNS-based communication (e.g., DB_HOST: database), with each service in its own network namespace by default.

6. Configuration Files (Configs)

MikroTik Format:

services:
  web:
    configs:
      - source: 'nginx_conf'
        target: '/etc/nginx/nginx.conf'
        mode: 0755

configs:
  'nginx_conf':
    content: |
      # Inline content here
      server {
        listen 80;
      }
  • Inline content defined directly in YAML
  • Uses content: field with multiline strings
  • Config names typically in quotes
  • No external file references

Docker Compose references external files on disk with file: or pre-existing configs with external:, but doesn't support inline content.

7. Special [placeholder] Variables

MikroTik Format:

environment:
  APP_URL: "[accessProto]://[accessIP]:[accessPort]"
  TRUSTED_DOMAINS: "[accessIP]"
  ORIGIN: "[accessProto]://[accessIP]:[accessPort]"
  • Special runtime placeholders replaced by RouterOS:
    • [accessIP] - Host/router IP address
    • [accessPort] - The mapped host port
    • [accessProto] - Protocol (http/https)
  • Values determined at deployment time

Docker Compose has no special runtime placeholders, only environment variable substitution with ${VAR} syntax using values known before deployment.

8. Network Configuration

MikroTik Format - Default (Internal Network):

# No networks: section needed - uses internal network by default
services:
  app:
    ...
    ports:
      - 8080:80:web

and ends there without any networks:

  • Uses an isolated internal bridge (172.18.0.0/24) shared by all containers, and created as part of /app/setup
  • NAT port forwarding for exposed ports will automatically be created in /ip/firewall/nat
  • App containres communicate upstream via internal bridge, since all app VETH are created on the bridge
  • Services within a /app all use same VETH, so can communicate via localhost/127.0.0.1 along with port.
  • No networks: sections needed

MikroTik Format - External Network:

networks:
  default:
    name: lan          # References existing RouterOS bridge
    external: true
  • Attaches to existing RouterOS bridge
  • Direct network access, no NAT
  • Default LAN bridge (lan-bridge) configured in /app/settings

Docker Compose allows services on multiple networks with rich driver options and full isolation. While RouterOS, all services share a VETH per /app and there is no networks: at the service level (AFAIK).

Although RouterOS /app may support multiple network in future. See output of /app/network/print when using something other on lan in the networks section.

9. Unsupported Docker Compose Features

Based on Claude LLM analysis of all builtin /app YAML configuration shown in RouterOS 7.22beta5. Some items are marked ":thinking:" based on human review, since LLM may be bit too strong to say "unsupported" when different but supported /app concept may apply.

Build & Development:

  • :cross_mark: version field
  • :cross_mark: build context (only pre-built images)
  • :cross_mark: .env file references
  • :thinking: Variable substitution (${VAR})
  • :cross_mark: Multiple compose files / override files
  • :cross_mark: extends or service inheritance
  • :cross_mark: profiles

Storage:

  • :thinking: Bind mounts / host directory paths
  • :cross_mark: tmpfs mounts
  • :cross_mark: Volume driver options
  • :cross_mark: External volume references (beyond basic external flag)

Networking:

  • :cross_mark: Per-service network assignment
  • :cross_mark: Network aliases
  • :cross_mark: extra_hosts
  • :cross_mark: Custom DNS servers
  • :cross_mark: Network driver options beyond basic types
  • :thinking: IPv6 configuration
  • :cross_mark: links (deprecated in Compose anyway)

Security & Secrets:

  • :cross_mark: secrets (use configs instead for all file injection)
  • :thinking: cap_add / cap_drop
  • :thinking: privileged mode
  • :thinking: devices mapping
  • :cross_mark: ulimits

Orchestration:

  • :cross_mark: deploy section (replicas, resources, placement, update config)
  • :cross_mark: healthcheck definitions
  • :cross_mark: Swarm mode features
  • :thinking: Resource limits (memory, CPU)
  • :cross_mark: Placement constraints

Other:

  • :thinking: entrypoint override
  • :cross_mark: working_dir
  • :cross_mark: stdin_open / tty
  • :thinking: labels (metadata)
  • :cross_mark: logging driver configuration
  • :cross_mark: sysctls
  • :thinking: isolation mode

10. Simplified Comparison Example

MikroTik Format:

name: wordpress-site
descr: WordPress blog platform
category: productivity
default-credentials: web setup
page: https://wordpress.org

services:
  db:
    image: docker.io/mariadb:10.6
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: wordpress
    volumes:
      - wordpress/db:/var/lib/mysql
    restart: unless-stopped

  wordpress:
    image: docker.io/wordpress:latest
    ports:
      - 8080:80:web
    environment:
      WORDPRESS_DB_HOST: [accessIP]
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wordpress/html:/var/www/html
    depends_on:
      - db
    restart: unless-stopped

networks:
  default:
    name: lan
    external: true

Key MikroTik-specific elements in this example:

  • Metadata fields (name, descr, category, etc.) for RouterOS UI
  • Port label (8080:80:web) for clarity in UI and RouterOS config
  • Service communication via IP address, which can be RouterOS special [variables], but not service names
  • Volume prefix notation (wordpress/db, wordpress/html)
  • External network reference to RouterOS bridge

Summary of Key Distinctions

Feature MikroTik Format Docker Compose
Deployment /app/add yaml=app.yaml docker compose up
Metadata Extensive (name, descr, category, icon, credentials) Minimal (version only)
Port Format 8080:80/tcp:web (with labels) "8080:80" or long syntax
Port Labels Supported and encouraged Not supported
Volume Naming Relative paths appended to <disk>/apps/<app-name>/ Explicit declarations
Bind Mounts Not supported Supported
Service Communication IP via /app variables Service name DNS
Config Files Inline content: supported External files only
Secrets Not supported (use configs) Supported
Placeholders [accessIP], [accessPort], [accessProto] ${VAR} substitution
Networks Shared namespace, simplified Full isolation, complex
Build Support No (images only) Yes
Resource Limits Not available CPU/memory limits available
Target Platform MikroTik RouterOS Docker Engine / Swarm

The MikroTik format is a streamlined, opinionated subset of Docker Compose, designed for:

  • RouterOS container runtime
  • Application catalog/marketplace presentation
  • Simplified networking (shared namespace)
  • Router appliance deployment constraints
  • User-friendly configuration with enhanced metadata

edits:
  • version is not required
  • minor text cleanup around relative paths
  • added callout that multiple networks may be possible
  • other tweaks and various provisos added
  • clarified web on as port label is how admin-url display is triggered
  • fix required attributes sections, less is required than initally thought
  • linked configs and networks YAML doc to first post
  • fix some links from 32K breakup (likely more still)
2 Likes

BETA Schema validation for /app YAML format

UPDATED to support validation with YAML needed for being an "App Store". This is simple, since custom apps downloaded by a app-store-urls in /app/settings are just an array of /app YAML.

However, for tools like VSCode there is a different schema file to use, the instructions below have been updated to include both a single "Custom /app" and the "/app Store" YAML with multiple. For automatic schema detection of a /app Store YAML file, use a file matching .tikappstore.yaml (with the "single app" form ending in .tikapp.yaml)

RouterOS 7.22beta introduced "custom /app" support that uses YAML to define a new /app in the RouterOS container catalog. RouterOS /app uses its own YAML format. While similar to docker-compose, it not the same. In order to provide validation of MikroTik-specific /app YAML, my TIKOCI project repo has an experimental "JSON" schema to enable validation of /app YAML format in editors like VSCode. _Since custom /app are still in beta, TIKOCI /app YAML schema has not published to https://www.schemastore.org yet - which allow tools like VSCode to automatically use to do YAML validation. The schema is in JSON, but editors will use for YAML too.

Download the "beta" RouterOS /app YAML schema - https://tikoci.github.io/restraml/routeros-app-yaml-schema.latest.json

The schema can be used in a various editors that support YAML validation. However only VSCode has been tested

Adding /app YAML validation to VSCode

To use the schema to check /app YAML in VSCode:

  1. VSCode's YAML extension need to be installed, which adds basic YAML support and schema validation.
  2. You'll need to add the /app YAML schema to VSCode "User Settings" via JSON. To access the settings, use shift + command + P, then search for "Open User Settings (JSON)", and select it.
  3. In the JSON file appears, add the following text within the outer { } block:
    {
      "yaml.schemas": {
         "https://tikoci.github.io/restraml/routeros-app-yaml-schema.latest.json": [
             "tikapp.yaml",
             "tikapp.yml",
             "*.tikapp.yaml",
             "*.tikapp.yml",
         ],
         "https://tikoci.github.io/restraml/routeros-app-yaml-store-schema.latest.json": [
             "tikappstore.yaml",
             "tikappstore.yml",
             "*.tikappstore.yaml",
             "*.tikappstore.yml",
         ]
       },
       ...
    }
    
  4. Create a new file ending in .tikapp (or any of the endings used in settings.json above), and any errors will be highlight with red underline, and listed in the "Problems" view.

Here is what happens with "bad /app YAML" in VSCode, with the routeros-app-yaml-schema.latest.json schema used:

And if you "hover" over an error, some help will automatically appear:

PRO TIP
If you add a header to the "/app YAML" like this:

# yaml-language-server: $schema=https://tikoci.github.io/restraml/routeros-app-yaml-schema.latest.json
name: amm0-note
descr: Use a YAML comment at top of file to load a schema
services:
 example:
   image: docker.io/library/alpine

This will cause the VSCode and YAML extension to load the current /app YAML schema without modifying VSCode setting to map extensions. With this as the first line in a .yaml/.yml file:

#  yaml-language-server: $schema=https://tikoci.github.io/restraml/routeros-app-yaml-schema.latest.json

All that's is needed is YAML extension installed, which will look for "first line" to load the /app YAML schema.

For a combined /app Store YAML file available for download via app-store-urls, use the following header instead:

# yaml-language-server: $schema=https://tikoci.github.io/restraml/routeros-app-yaml-store-schema.latest.json

Note that -store is added to the single file form

1 Like

Placeholder for some example

If you have any question, or spot errors in the "temporary manual", feel free to reply below.

Just a microtic detail from someone who is more comfortable with docker-compose than with RouterOS : the version field is no longer required.

Apart from that, huge congrats for that job!

1 Like

NEW app-store-urls for HTTP/HTTPS download of /app YAMLs

In 7.22beta6, custom /app can be downloaded via URL. See post below for details and example:

Updated schemas to now support /app Store YAML

A 2nd "JSON schema" (which is also used for YAML) was added to handle the new app-store-urls format. The "App Store" format is just an array of YAML defined here, so the update schemes just enable validation when same "Custom /app YAML" is used as element of a list (instead of the top-level, as would be used in /app/add yaml=). The "/app Store YAML" really just references the orginal JSON scheme with individual /app YAML

See YAML schema for /app for VSCode above for usage. The same files likely work in other editors with JSON schemes and YAML support (like NeoVim), but I didn't try or test so YMMV.

YAML changes?

If anyone notices new YAML elements in 7.22beta6, please comment below. So far the beta5 and beta6 format looks same. At least from builtin's /app's YAML usage – which is what forms the basis of this "manual"

Geez, Im not even brave enough to try capsman and then you throw this YAMls stuff at us, and all the time I was thinking what does a Barca footie player have to do with RoS?? :wink:

Better than guy with hockey mask and bloody machete. :slight_smile: