Skip to content

Instantly share code, notes, and snippets.

@vik-y
Last active August 22, 2017 15:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vik-y/cab3d8ba22c73fc3aa1ca565d72f5c43 to your computer and use it in GitHub Desktop.
Save vik-y/cab3d8ba22c73fc3aa1ca565d72f5c43 to your computer and use it in GitHub Desktop.
Documentation for contianer apps in Amahi.

Amahi Documentation

Important Terms

Before going further in this document, understanding the following terms might be useful:

Problem Statement

Amahi supports one click install for different kind of apps. To add any app to amahi they have to be packaged properly. Each app might require a completely different environment to run and it might not be possible to provide that environment to every app because their can be dependnecy conflicts. For example some app might require php5 and some might require php7. We need a mechanism to address this concern such that we can add any app to Amahi irrespective of the technology stack that it might require or without version conflicts.

The idea

The idea is to run apps as containers. This gives a lot of flexibility as we can use any app using any stack.

Initial work what and why? https://docs.google.com/document/d/1Pe4A3my6lrKTlvQ3WeHiB1SVugzcBqpUpPwgFfuPEKA/edit?usp=sharing

Initially I tried managing the containers using rails app itself but then slowly as I went along configuration management became a problem. I found out that each container required a different kind of configuration and it was not possible to write a single configuration and tweak it for different apps. To overcome this problem I shifted to docker-compose for running and managing containers. More info on it can be found at the link below.

Shifting to docker-compose : why? https://docs.google.com/document/d/1wEK5qNKQ-dWmlaYw6EeYZvc_XBkoqadQj3n3kv1HlFw/edit?usp=sharing

In docker-compose we can put all the configuration related to an app in a yml file and put that in the install script. For each app we can have a different yml file based on the requirements.

This documentation covers the installation using docker-compose.

Architecture Overview

Architecture Overview

The idea of implementation is very similar to what is showing in the image above. Each app will run as a container. Each container will expose their own port/ports as can be seen as exposed port in the diagram above. We can map a host system port (mapped port in the diagram) to the exposed port in the container. And then using reverse proxy we can connect different subdomains to different container apps.

This has been explained in detail in the Design Document.

How the app installation works?

This section presents an overview of how installation works.

Assuming we have official container for an app available we can easily integrate them to amahi. If the official image is not available then we might have to build one of our own like I did for osticket and coppermine.

Building Images

Building images can be tricky and does require some knowledge of the apps as well (For example which php libraries to install, etc). There's a well defined procedure for building images for node and rails apps as well.

Building images can be tricky and the image size is a very major issue. To reduce the image size I would suggest the readers to look up the following articles:

Install Script

Once the image is available we can write an install script. You can see below a sample install script for gitlab.

cat > docker-compose.yml << 'EOF'
gitlab-container:
  image: 'gitlab/gitlab-ce:latest'
  container_name: "APP_IDENTIFIER"
  restart: unless-stopped
  hostname: 'APP_HOSTNAME'
  environment:
    GITLAB_OMNIBUS_CONFIG: |
      external_url 'http://APP_HOSTNAME:HOST_PORT'
      gitlab_rails['gitlab_shell_ssh_port'] = 2224
  ports:
    - 'HOST_PORT:HOST_PORT'
    - '2224:22'
  volumes:
    - './srv/gitlab/config:/etc/gitlab'
    - './srv/gitlab/logs:/var/log/gitlab'
    - './srv/gitlab/data:/var/opt/gitlab'
EOF
docker-compose up -d

This script creates a docker-compose.yml file and then runs docker-compose up -d command which essentially creates and runs the container.

The yml file can have different parameters. For that we might have to refer to docker and docker-compose documentation. restart : unless-stopped is used to handle failovers of containers. If a container crashes for some reaosn then it will restart automatically.

Understanding the script

Container script for each app will have these parameters for sure:

container-name: "APP_IDENTIFIER"
restart : unless-stopped
ports:
  - 'HOST_PORT:xyz' # Not required for apps which are not webapp

# xyz = any port inside the container
# HOST_PORT is port on the host machine. Two different containers cannot have same value for 
# HOST_PORT but can have the same value for xyz (Just to clear up the confusion about 
# HOST_PORT)

Most of the configuration related to the container will be written in the docker-compose.yml file but some data we have to extract during run time like the HOST_PORT using which will reverse proxy to the container. HOST_PORT is 35000+app_id and has to be derived during app installation. Similarly APP_IDENTIFIER and APP_HOSTNAME are derived during runtime. The code below is used to put that data into install script during runtime.

#app/models/app.rb Line 322 to 326
install_script = installer.install_script
install_script = install_script.gsub(/HOST_PORT/, (BASE_PORT+self.id).to_s)
install_script = install_script.gsub(/WEBAPP_PATH/, webapp_path)
install_script = install_script.gsub(/APP_IDENTIFIER/, identifier)
install_script = install_script.gsub(/APP_HOSTNAME/, app_host)

Sample uninstall script

docker-compose stop
docker-compose rm -f
# Not removing the image. Just stopping the container. 

For most containers the above uninstallation script will work fine. This stops the running container and removes it. Please note that this doesn't delete any of the volumes attached (persistent storage. Please refer to docker documentatio for more details regarding volumes) with the container so if you add a volume during installation (as we have done in the gitlab example above) then we have to remove them here during uninstallation. For example if we were to remove gitlab completely along with all files that were added by gitlab container then the uninstall script would look something like this:

docker-compose stop
docker-compose rm -f
rm -rf srv # Removing the srv folder which holds the persistent files for gitlab container

This behaviour might not be intended for all applications. Right now I haven't removed static files for any apps that I have added.

Reverse Proxy

Why is it needed?

Each app will be running insidea container and they will attach to some port/ports on the host machine. For example, imagine if you are running an app like gitlab in container. Gitlab might require different ports for different things. Inside the container different services can run on any port as containers run in an isolated environment and have their own network stack. But ultimately the services running on these containers have to be reachable from the outside world. To make that possible we have to bind a port inside the container with a host port.

Gitlab App - Inside Container - Web Server Running on port 80 We have to bind this with a port on host machine (our HDA). Let's assume we bind it to port 35001. Now typing amahi.net:35001/ in the browser will open gitlab website.

OSticket app - Inside Container - Web Server Running on port 9000 Let's bind this to port 35002 on host machine. Now typing amahi.net:35002/ in the browser will open osticket website.

As we can see that each of these apps web interface is accessible on different ports but we want to achieve a behaviour like:

osticket.amahi.net -> opens osticket which is running on amahi.net:35002/ gitlab.amahi.net -> opens gitlab which is running on amahi.net:35001/

But the url end point should be the same, i.e osticket.amahi.net and gitlab.amahi.net . To attain this behaviour we have to use Reverse Proxy.

For reverse proxy I have added a new app-container.conf file which can be seen below. The APP_PORT is changed during runtime.

<VirtualHost *:80>

	ServerName HDA_APP_NAME
	ServerAlias HDA_APP_NAME.HDA_DOMAIN

	APP_ALIASES

	APP_CUSTOM_OPTIONS

	ProxyPreserveHost On


    ProxyPass / http://localhost:APP_PORT/
    ProxyPassReverse / http://localhost:APP_PORT/

	ErrorLog  APP_ROOT_DIR/logs/error_log
   	CustomLog APP_ROOT_DIR/logs/access_log combined env=!dontlog
</VirtualHost>

APP_PORT part is derived from the app id. After installation the app will have some id in the database base. The APP_PORT will be 35000+app_id

Please note that we can run any kind of app in container. It might be a headless app and it might be a webapp. In case of web applications we need to define an external port (mapped port on host - Refer to Architecture Overview section) through which the app will be bind. Then to reach that app we have to reverse proxy. "APP_PORT" is essentially that. For apps which don't require a web interface we might not use this file at all.

How to add a new app?

Taking example of Hydra

See the usage as mentioned by the maintainer:

docker create --name=hydra \
-v <path to data>:/config \
-v <nzb download>:/downloads \
-e PGID=<gid> -e PUID=<uid> \
-e TZ=<timezone> \
-p 5075:5075 linuxserver/hydra

Convert the above to a docker-compose file. Ignore the -e PGID=<gid> -e PUID=<uid>, even though it's relevant, it is out of the scope of this discussion.

hydra-container:
  image: 'docker.io/linuxserver/hydra'
  container_name: "hydra"
  restart: unless-stopped
  ports:
    - '5075:5075'
  volumes:
    - './config:/config'
    - './downloads:/downloads'
    - '/etc/localtime:/etc/localtime:ro'


# Understanding the volume mounts:
# ./config:/config -> As seen in the docker create commnad the -v command mentions the volumes. 
# Path to data that we are providing is a relative path. Every installed app has a path in which the
# install script runs. So "config" and "downloads" folder will be created there in that path.

# /etc/localtime:/etc/localtime:ro -> This is to make sure that the container uses the same time as used 
# by the host system. To avoid this mount we can also use 
# environment:
# - TZ=<timezone>
# in the docker compose file 

NOTE: Please note that adding apps might require knowledge about docker and docker-compose and discussing those is out of the scope of this documentation though the links mentioned below might be useful.

  • Container and Layers
  • Docker Compose
  • Restart Policies. Apps run as containers which are managed by docker. If docker daemon is shut down or stopped then the app will also stop. If the container crashes for some reason then it has to be restarted. Using restart policies we can manage this.

Now once we are done with making a docker-compose file we can test it on our local system to see if it is working properly or not. Once that's done, we can go ahead and add this image to amahi.org

For adding to amahi.org some modifications have to be done. The final changes can be seen below. Notice the APP_IDENTIFIER and HOST_PORT (For more info on this refer to "Understandin the script" section)

hydra-container:
  image: 'docker.io/linuxserver/hydra'
  container_name: "APP_IDENTIFIER"
  restart: unless-stopped
  ports:
    - 'HOST_PORT:5075'
  volumes:
    - './config:/config'
    - './downloads:/downloads'
    - '/etc/localtime:/etc/localtime:ro'

The final install and uninstall scripts to be added on amahi.org will be

Install Script

cat > docker-compose.yml << 'EOF'
hydra-container:
  image: 'docker.io/linuxserver/hydra'
  container_name: "APP_IDENTIFIER"
  restart: unless-stopped
  ports:
    - 'HOST_PORT:5075'
  volumes:
    - './config:/config'
    - './downloads:/downloads'
    - '/etc/localtime:/etc/localtime:ro'
EOF
docker-compose up -d

Uninstall Script

docker-compose stop
docker-comose rm -f
rm -rf config # Use this if you want all files to be removed after uninstall
rm -rf downloads  # Use this if you want all files to be removed after uninstall

NOTE

You should be careful about the following:

Future Work

Cleanup - too many images.

Presently when a containerised app is uninstalled the container is stopped and removed but the image used to run the container stays (Note the difference between container and image). I am not removing the image since images are generally large and what if users decides to reinstall the app? This has to be thought of more. Adding the feature of deleting image after installation is essentially 1 line of code. Going forward we might have to come up with a mechanism for clearing images which haven't been used for a long time.

Reducing Download Size

We can't build and maintain containers for all apps. Vendors provide official containers but some of them are huge in size. To fix this problem we can reduce the download to that of a normal installation by using an apt-cache, gem server or npm server on hda itself.

The idea is following: Instead of we building images on our server we can push the Dockerfile to the client and the client can build the docker image. While building the image they will download the required packages, gem files, node dependencies or whatever. If we have a mechanism to cache this download so that all the subsequent builds can use this data then we can save a lot of Internet usage. One possible way of doing this was running apt-cache, gem server and npm server on the client itself.

Updating apps

With containers updates can be really easy. We can support single click update of apps. It's just a matter of supporting them now.

Support for advance configuration.

With containers we can limit the cpu/memory/disk usage of each app. We just have to modify the docker-compose.yml file for the app for these changes without doing any modification to the source code. If required, this can be used.

Collecting Logs

It's a new feature, we will need to collect a lot of metrics from the users to understand how this feature is working and how it can be improved. Some of those features include

  • The CPU info of systems which are running amahi.
  • RAM and Storage information.
  • Logs of containerised apps to debug errors.
@FransM
Copy link

FransM commented Aug 17, 2017

My comments:
Please start with a problem statement. What is your assignment/goal
Next have a section called introduction or overview that describes the stucture of your document.

Architecture: what you describe here is useful info but not architecture. Describe how containers fit into the amahi framework.
The goal is not to describe docker, but to describe how docker images fit into the amahi structure.

How the app installation works section:
I'm missing a section on terminology. An image is that a docker image? You could give examples.
Also a section like this "then we might have to build one of our own like I did for osticket and coppermine": this does not help the reader. Explain how to do this (or provide a pointer to already existing info).

The script and the section understanding the script: this starts good. It may be better to use an actual example (e.g. your osticket image)
Also where you explain about ports may be a better location for the image that you have in architecture overview.
Also you explain the first few lines but then you deviate into the installer script.
Suggest to explain the rest of the script as well.
Installation then could be a completely different section.

reverse proxy: please explain why a reverse proxy is needed.
You also mention app-php5.conf but php5 appears here by magic. Please explain more about why and when this is needed.

Goal of the documentation should be that someone else is able to create a new container and add it to amahi.. You may reference to other documentation (e.g. on container creation) but please detail amahi specifics. We would also want to add non php5 containers, so please detail what things are generic and what things are for the php5 container.

Second goal of the documentation would be design documentation. How does it work internally. This is the knowledge that would be needed to maintain this.

As such the hydra example is a nice start.

Wrt this "NOTE: Please note that adding apps might require knowledge about docker and docker-compose and discussing those is out of the scope of this documentation."
I understand that but it would be helpful to provide pointers where to find this info.

TOWRITE: these are good sections to add. I'm not sure there are too many images. If the image helps to understand things, just keep it.

@vik-y
Copy link
Author

vik-y commented Aug 18, 2017

TOWRITE: these are good sections to add. I'm not sure there are too many images. If the image helps to understand things, just keep it.

  • Write about the assignment/goal.
  • Change the architecture diagaram. Make something relevant to amahi design.
  • Pointer to building images.
  • Do changes as recommended for installation section
  • Give example of how to build osticket or coppermine images
  • Explain why reverse proxy is needed
  • Explain about app-php5.conf
  • Add pointers to cove
  • Design Documentation

@vik-y
Copy link
Author

vik-y commented Aug 18, 2017

UPDATE:
Added some pointers to official documentation. Working on making this more informative. Need some more time. Maybe 12 more hours. Busy with college work right now.

@vik-y
Copy link
Author

vik-y commented Aug 19, 2017

UPDATE:
Did recommended changes for installation section.
Explained why reverse proxy is needed.
Resolved the app-php5.conf issue. Renamed that in the main codebase to app-container.conf. That makes things more clear.

Started working on design documentation on a separate gist.

@vik-y
Copy link
Author

vik-y commented Aug 21, 2017

UPDATE:

Finished future work section!

@vik-y
Copy link
Author

vik-y commented Aug 22, 2017

UPDATE:

Shifting the architecture diagram to design document. Changing the diagram.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment