Documentation
Contributing
Adding a new app

Adding a new app

💡
In order to proceed you should be familiar working with the Terminal and Git.

In this guide, we will show you how to add a new app to the Runtipi App Store. The process is pretty simple and should take you less than 20 minutes.

Prerequisites

  • The app you want to add is free and open-source (or at least source available with a permissive license for personal use)
  • The app you want to add has an official and maintained docker image

Let's add the app

Fork the repository

In order to open a pull request you need to fork the repository. Visit the official App Store repository (opens in a new tab) and start by clicking the "Fork" button in the upper right corner of the page. This will create a new repository with your name and an identical structure to the original repository.

Clone the repository locally

On your computer clone the repository you just forked.

git clone https://<your-github-username>/runtipi/runtipi-appstore

Create a new branch for your app

Navigate to the repoisitory you just cloned.

cd runtipi-appstore

Create a new branch for your app.

git checkout -b app/<app-name>

Create the app files

Each app requires at least these files:

  • A docker-compose.yml file to run your app
  • A config.json file to configure your app
  • A description in markdown format
  • A logo in jpg format 512x512 pixels)

Inside the repository open the apps folder and create a new folder for your app. The name should be the same as the app name without spaces or capital letters.

The config.json file

Create a new config.json file inside the newly created folder:

{
  "name": "My super app",
  "id": "my-super-app",
  "available": true,
  "short_desc": "Its an awesome app!",
  "author": "me",
  "port": 8685,
  "categories": ["development", "ai"],
  "description": "This app does x thing and y thing.",
  "tipi_version": 1,
  "version": "v1.5.7",
  "source": "https://github.com/me/my-awesome-app",
  "exposable": true,
  "supported_architectures": ["arm64", "amd64"],
  "created_at": 1724134338800,
  "updated_at": 1724134338800
}
💡

This config.json file contains all the minimum required options. Check out the reference table for all available options.

Available categories : utilities, network, media, development, automation, social, utilities, photography, security, featured, books, data, music, finance, ai. If you want to add a new category, please open a new issue.

When choosing a port, please make sure that it is not in use already. You can check by using the search feature in your editor (for example the search icon on Visual Studio Code). You can also visit the App Store repository (opens in a new tab) and look the README.md file for the list of used ports for each app.

Sometimes an app is requiring more info to run it such as passwords or username. You can leverage the form_fields property in the config.json file to ask such information. Let's take for example Nextcloud. The image requires a username and password. We can simply add two fields in the config.json that will be presented to the user before installing.

{
  "form_fields": [
    {
      "type": "text",
      "label": "Username",
      "max": 50,
      "min": 3,
      "required": true,
      "env_variable": "NEXTCLOUD_ADMIN_USER"
    },
    {
      "type": "password",
      "label": "Password",
      "max": 50,
      "min": 3,
      "required": true,
      "env_variable": "NEXTCLOUD_ADMIN_PASSWORD"
    }
  ]
}

You can choose between different types of fields. The app will automatically validate the user input before submitting.

TypeDescriptionExample value
textJust a string of textusername
passwordWill be hidden on typingpassword
emailAn email addresstest@example.org
numberAny number123
fqdnFully qualified domain nameexample.org
ipAny valid ipv4 address192.168.2.100
fqdnipCombination between ip and fqdn192.168.2.100 or example.org
randomGenerate a random value for the user2m3ffc0923rk93df9023f9
booleanA checkboxtrue or false

Here is also a table of what a form_field object requires:

NameDescriptionExample valueRequired
typeThe type of the form field to use, see above.textyes
labelThe label to show the user.Nextcloud Usernameyes
hintA small hint to show the user.The username you want to use for nextcloudfalse
placeholderA placeholder for the form fielduserfalse
defaultAdd a default value, used only if the required option is set to falseuserfalse
regexUse a reggex pattern to verify the user input^[0-9]+.[0-9]+.[0-9]+$false
pattern_errorThe error to show when the regex pattern validation failsInvalid usernamefalse
minThe minium length for a text or password input5false
maxThe maximum length for a text or password input100false
requiredWheter the form field is required or nottrueyes
env_variableThe name of the variable you'll use in your docker-compose.yml file.NEXTCLOUD_USERNAMEyes
optionsOptions for multiple selectSee bellowfalse
encodingUsed only in addintion to random type. Specify the random value encoding. Can be base64 or hexbase64false
💡

When using the field type random for a password or secret, the min value will be used to determine the length of the string. If the min value is omitted the default length is 32 characters.

Here is how to use the form fields in your docker compose file:

services:
  nextcloud:
    container_name: nextcloud
    image: nextcloud:23.0.3-apache
    restart: unless-stopped
    ports:
      - ${APP_PORT}:80
    volumes:
      - ${APP_DATA_DIR}/data/nextcloud:/var/www/html
    environment:
      - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
      - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}

NEXTCLOUD_ADMIN_USER and NEXTCLOUD_ADMIN_PASSWORD are coming from the user inputs example above.

Now let's take a look at the options option in the form fields, this will be rendered as dropdown in the UI. You can use them like this:

{
  "label": "Select your favorite fruits",
  "type": "text",
  "required": true,
  "options": [
    { "label": "Apple", "value": "apple" },
    { "label": "Banana", "value": "banana" },
    { "label": "Orange", "value": "orange" }
  ],
  "env_variable": "fruits"
}

The docker-compose.yml file

In the same folder, create a docker-compose.yml file with your app config.

version: "3.9"
services:
  my-app: # Should be exact same name as "id" field in config.json
    container_name: my-app # Should be exact same name as "id" field in config.json
    image: my-app:1.0.0 # Try to avoid "latest" tag. As it may break configs in the future.
    restart: unless-stopped # Do not change this
    environment:
      - TZ=${TZ} # Can use any env variable. See "environment variables" section in the docs
    volumes:
      - ${APP_DATA_DIR}/data/config:/config # Always start the path with ${APP_DATA_DIR}/data/. This will put all data inside app-data/my-app/data
      - ${APP_DATA_DIR}/data/projects:/projects
    ports:
      - ${APP_PORT}:8443
    restart: unless-stopped
    networks:
      - tipi_main_network # That should not be changed
    labels: # Use your editors search and replace feature to replace all instances of "myapp" with your app name in the traefik labels
      # Main
      traefik.enable: true
      traefik.http.middlewares.myapp-web-redirect.redirectscheme.scheme: https
      traefik.http.services.myapp.loadbalancer.server.port: 8443 # Should be the same as the app internal port so for this example 9443
      # Web
      traefik.http.routers.myapp-insecure.rule: Host(`${APP_DOMAIN}`)
      traefik.http.routers.myapp-insecure.entrypoints: web
      traefik.http.routers.myapp-insecure.service: myapp
      traefik.http.routers.myapp-insecure.middlewares: myapp-web-redirect
      # Websecure
      traefik.http.routers.myapp.rule: Host(`${APP_DOMAIN}`)
      traefik.http.routers.myapp.entrypoints: websecure
      traefik.http.routers.myapp.service: myapp
      traefik.http.routers.myapp.tls.certresolver: myresolver
      # Local domain
      traefik.http.routers.myapp-local-insecure.rule: Host(`myapp.${LOCAL_DOMAIN}`)
      traefik.http.routers.myapp-local-insecure.entrypoints: web
      traefik.http.routers.myapp-local-insecure.service: myapp
      traefik.http.routers.myapp-local-insecure.middlewares: myapp-web-redirect
      # Local domain secure
      traefik.http.routers.myapp-local.rule: Host(`myapp.${LOCAL_DOMAIN}`)
      traefik.http.routers.myapp-local.entrypoints: websecure
      traefik.http.routers.myapp-local.service: myapp
      traefik.http.routers.myapp-local.tls: true

If your app requires different docker-compose files for different architectures, you can use the following naming convention:

docker-compose.arm64.yml

If the user is running on an arm64 architecture, this file will be used instead of the default docker-compose.yml.

The docker-compose.json file (optional)

If you would like your app to have all our advanced features (like the ability to use only a reverse proxy or disable the app's port), you can check our Dynamic Compose (opens in a new tab) section to add the new compose file. In the future we are planning to mainstream this new dynamic compose. You would help us a lot by including this file since it will make the later migration faster.

The metadata/description.md file

Here is located the long description of the app in markdown (html is supported too), usually a copy of the readme provided by the app's author. Make sure that when using images they are coming from github or an image service since tipi doesn't support loading images from local storage in the description.

The metadata/logo.jpg file

That's a simple logo of the app in .jpg format. In order for the app to look good make sure to use an image of 512x512 pixels and that it of course looks good in the dashboard. Additionally if you can it would be nice to try to avoid the white-background-logo format since it makes the app look bad in the appstore.

The data folder

If your app requires default files or configuration, you can easily provide those by creating a data folder beside the app config.

        • config.json
        • docker-compose.yml
          • anything.conf
          • config.ini.template
          • description.md
          • logo.jpg
  • Anything placed under data will be copied over app-data/<app-id>/data before installation. Then you can mount those files inside your compose file.

    my-app:
      container_name: my-app
      restart: unless-stopped
      volumes:
        - ${APP_DATA_DIR}/data:/var/lib/config # Will mount the folder with `anything.conf` inside

    If one of your files requires some dynamic values, you can use a template file. Let's take the example of a config.ini file that requires the APP_PORT variable.

    [my-app]
    port = {{APP_PORT}}

    If you name your file config.ini.template, it will automatically replace the variables with the values defined in the environment variables.

    For example if you put the previous example in data/config.ini.template, it will be copied over to app-data/<app-id>/data/config.ini with the APP_PORT variable replaced.

    Commit your changes

    Once you're done, you can commit your changes to the repository.

    git add .
    git commit -m "Add my-app"
    git push origin master

    Test your app in your own instance

    Before submitting your app, you can test it in your own instance. You can do so by adding your app repository to the appsRepoUrl property in the settings.json file. Visit the Using your own appstore repository section to learn more.

    Submit a Pull Request

    You can now submit a pull request to the repository.

    • On GitHub, visit your repository and click on the "Pull requests" tab.
    • Click on "New pull request" and fill in the title and description.
    • Choose your branch and target the main repository at runtipi/runtipi-appstore.