Skip to main content
Version: Developer

Building Custom Images

Overview

Custom workspaces let you deliver the exact software, configuration, and desktop experience your users need. This guide builds a custom Docker image from a Kasm core image, then progresses from a minimal customization to installed software, automatic startup, branding, single-application mode, and a private registry. The result is a robust, repeatable image that you can register as a workspace and update on a schedule with no user downtime.

This build process is the preferred method for creating sustainable, highly automated images. For a quick alternative that captures the state of an active session, see create-image-from-session. To keep deployed images current automatically, see Image Maintenance Process.

Custom images import from an existing Default or Core Docker Image published by the Kasm Technologies team. The Core Docker images contain the minimal configurations necessary for the Docker images to work within the platform. The kasmweb/core-ubuntu-jammy image is the preferred core image and is based on Ubuntu 22.04 LTS. Most programs that install on Ubuntu can install inside this image. The exceptions are programs delivered as containers themselves, such as Snaps and some FlatPaks.

A Git repository with several example Dockerfiles that demonstrate how to build full desktop or single-application images is available on GitHub.

Prerequisites

Before you begin, confirm the following:

  • Administrator access to the Kasm Workspaces deployment, with permission to register workspaces.
  • Working knowledge of Docker and shell scripting to add custom software and configurations.
  • A server with the Kasm Agent installed, or a separate build host with Docker. You can run these steps on the server that runs the Agent.
  • The base image tag that matches your deployed Kasm version. Base custom images on a rolling tag so the image receives regular program and security updates, for example kasmweb/core-ubuntu-jammy:[[release]]-rolling-daily or kasmweb/core-ubuntu-jammy:[[release]]-rolling-weekly.
Base on a rolling tag

The Kasm team publishes new editions of the core image at every release cycle. The Kasm team also maintains rolling updates for all published images. Base custom images on a rolling tag and run a scheduled build so the custom image receives regular program and security updates. Build your image on the appropriate tag for your deployed Kasm version, for example kasmweb/core-ubuntu-jammy:[[release]]-rolling-daily or kasmweb/core-ubuntu-jammy:[[release]]-rolling-weekly.

FROM kasmweb/core-ubuntu-jammy:1.19.0
note

You can also base a custom image on any of the default images published. See Default Docker Images for a list.

tip

If you build directly on the Kasm Workspaces server or Agent, disable Automatically Prune Images for the applicable Agent. See Agent Settings for more details.

Solution approach

This guide progresses through the following phases:

  1. Build and register a baseline image.
  2. Install software into the image.
  3. Launch an application automatically on session start.
  4. Brand the desktop background.
  5. Build a single-application workspace.
  6. Push the image to a container registry.
  7. Present a custom launch form.

Detailed steps

Build and register a baseline image

The baseline Dockerfile has a predefined section where customizations are added. Do not modify the statements before and after that section. This example adds a single customization. It creates a file named hello.txt on the desktop.

  1. Create a file named Dockerfile with the following contents.


    FROM kasmweb/core-ubuntu-jammy:1.19.0
    USER root

    ENV HOME /home/kasm-default-profile
    ENV STARTUPDIR /dockerstartup
    ENV INST_SCRIPTS $STARTUPDIR/install
    WORKDIR $HOME

    ######### Customize Container Here ###########


    RUN touch $HOME/Desktop/hello.txt


    ######### End Customizations ###########

    RUN chown 1000:0 $HOME
    RUN $STARTUPDIR/set_user_permission.sh $HOME

    ENV HOME /home/kasm-user
    WORKDIR $HOME
    RUN mkdir -p $HOME && chown -R 1000:0 $HOME

    USER 1000

  2. Build the image.

    sudo docker build -t sublime-text:example -f Dockerfile .
  3. Log into the Kasm UI as an administrator and register a new workspace. Select the Workspaces panel and click Add Workspace. You can also use the arrow menu to clone the configuration of an existing workspace.

    tip

    Kasm Workspaces does not assume the tag latest as many other Docker tools do. Docker images from older versions may not be compatible with newer versions, which reduces the benefit of a latest tag. The Kasm team recommends a tag that reflects the version of the core image used as a base. Kasm Workspaces expects an explicit tag in the workspace configuration for Docker Image.

  4. When you finish editing the details of the workspace, click Save to create the new workspace.

    Register New Image

    Register New Image
  5. From the Kasm Dashboard, click the Sublime Text image.

    New Image Available

    New Image Available
  6. A new pop-up appears in the UI with a LAUNCH SESSION button. Click it to launch the Kasm session.

    Launch Session

    Launch Session
  7. A new session is created with the hello.txt file you created.

    Running the custom Image

    Running the custom Image

Install software into the image

Now do something more useful. Update the Dockerfile to install Sublime Text.

  1. Update the Dockerfile with the following contents.

    FROM kasmweb/core-ubuntu-jammy:1.19.0
    USER root

    ENV HOME /home/kasm-default-profile
    ENV STARTUPDIR /dockerstartup
    ENV INST_SCRIPTS $STARTUPDIR/install
    WORKDIR $HOME

    ######### Customize Container Here ###########

    RUN wget -qO - https://download.sublimetext.com/sublimehq-pub.gpg | apt-key add - \\
    && apt-get update \\
    && apt-get install -y apt-transport-https \\
    && echo "deb https://download.sublimetext.com/ apt/stable/" | tee /etc/apt/sources.list.d/sublime-text.list \\
    && apt-get update \\
    && apt-get install sublime-text \\
    && cp /usr/share/applications/sublime_text.desktop $HOME/Desktop/ \\
    && chmod +x $HOME/Desktop/sublime_text.desktop \\
    && chown 1000:1000 $HOME/Desktop/sublime_text.desktop


    ######### End Customizations ###########

    RUN chown 1000:0 $HOME
    RUN $STARTUPDIR/set_user_permission.sh $HOME

    ENV HOME /home/kasm-user
    WORKDIR $HOME
    RUN mkdir -p $HOME && chown -R 1000:0 $HOME

    USER 1000

    Create a desktop icon

    Creating desktop icons is a common need. This example shows how applications, when installed, often place a .desktop file in /usr/share/applications. This file can often be copied without modification to the desktop $HOME/Desktop/. Mark the file as executable and change the ownership to user and group 1000.

    Reference:

  2. Rebuild the image, create a new session, and verify that Sublime Text is installed and an icon is present on the desktop.

    Sublime Text is Installed

    Sublime Text is Installed

Launch an application automatically on session start

Use the custom_startup.sh interface point to launch Sublime Text when the session starts. Create the script and mark it executable. This script runs in the standard user (1000) context when the session starts. Use the built-in command /usr/bin/desktop_ready to ensure Sublime Text starts after the Kasm desktop environment.

  1. Update the Dockerfile with the following contents.

    FROM kasmweb/core-ubuntu-jammy:1.19.0
    USER root

    ENV HOME /home/kasm-default-profile
    ENV STARTUPDIR /dockerstartup
    ENV INST_SCRIPTS $STARTUPDIR/install
    WORKDIR $HOME

    ######### Customize Container Here ###########

    RUN wget -qO - https://download.sublimetext.com/sublimehq-pub.gpg | apt-key add - \\
    && apt-get update \\
    && apt-get install -y apt-transport-https \\
    && echo "deb https://download.sublimetext.com/ apt/stable/" | tee /etc/apt/sources.list.d/sublime-text.list \\
    && apt-get update \\
    && apt-get install sublime-text \\
    && cp /usr/share/applications/sublime_text.desktop $HOME/Desktop/ \\
    && chmod +x $HOME/Desktop/sublime_text.desktop \\
    && chown 1000:1000 $HOME/Desktop/sublime_text.desktop

    RUN echo "/usr/bin/desktop_ready && /opt/sublime_text/sublime_text &" > $STARTUPDIR/custom_startup.sh \\
    && chmod +x $STARTUPDIR/custom_startup.sh


    ######### End Customizations ###########

    RUN chown 1000:0 $HOME
    RUN $STARTUPDIR/set_user_permission.sh $HOME

    ENV HOME /home/kasm-user
    WORKDIR $HOME
    RUN mkdir -p $HOME && chown -R 1000:0 $HOME

    USER 1000

  2. Rebuild the image and create a new session. Sublime Text starts automatically.

    Sublime Text Started Automatically

    Sublime Text Started Automatically

Brand the desktop background

Change the background by overwriting the /usr/share/backgrounds/bg_default.png file.

  1. Update the Dockerfile with the following contents.

    FROM kasmweb/core-ubuntu-jammy:1.19.0
    USER root

    ENV HOME /home/kasm-default-profile
    ENV STARTUPDIR /dockerstartup
    ENV INST_SCRIPTS $STARTUPDIR/install
    WORKDIR $HOME

    ######### Customize Container Here ###########

    RUN wget -qO - https://download.sublimetext.com/sublimehq-pub.gpg | apt-key add - \\
    && apt-get update \\
    && apt-get install -y apt-transport-https \\
    && echo "deb https://download.sublimetext.com/ apt/stable/" | tee /etc/apt/sources.list.d/sublime-text.list \\
    && apt-get update \\
    && apt-get install sublime-text \\
    && cp /usr/share/applications/sublime_text.desktop $HOME/Desktop/ \\
    && chmod +x $HOME/Desktop/sublime_text.desktop \\
    && chown 1000:1000 $HOME/Desktop/sublime_text.desktop

    RUN echo "/usr/bin/desktop_ready && /opt/sublime_text/sublime_text &" > $STARTUPDIR/custom_startup.sh \\
    && chmod +x $STARTUPDIR/custom_startup.sh

    RUN wget https://cdn.hipwallpaper.com/i/92/9/0Ts6mr.png -O /usr/share/backgrounds/bg_default.png

    ######### End Customizations ###########

    RUN chown 1000:0 $HOME
    RUN $STARTUPDIR/set_user_permission.sh $HOME

    ENV HOME /home/kasm-user
    WORKDIR $HOME
    RUN mkdir -p $HOME && chown -R 1000:0 $HOME

    USER 1000

  2. Rebuild the image and create a new session. Sublime Text starts automatically.

    Custom Desktop Background

    Custom Desktop Background

Build a single-application workspace

You can configure a container-based workspace to launch in single-application mode using XFCE. When a single-app workspace launches, it opens a specific application automatically on session start, for example Firefox, and only that application is usable. The traditional desktop interface is disabled, which prevents users from interacting with panels and menus or launching additional applications. XFCE configurations make this possible.

Several popular applications are already available as single-app workspaces in the official Workspaces registry. Examples include Firefox, Chrome, Brave, Slack, Telegram, and Zoom.

  1. Start by writing a custom Dockerfile for your single-app workspace. Use the kasmweb/core-ubuntu-jammy image as the base image when creating a single-app workspace. If you use a different image, you must manually load the image with the single-app XFCE config files available on the GitHub repository.

    FROM kasmweb/core-ubuntu-jammy:1.19.0-rolling-weekly
    USER root

    ENV HOME /home/kasm-default-profile
    ENV STARTUPDIR /dockerstartup
    ENV INST_SCRIPTS $STARTUPDIR/install
    WORKDIR $HOME

    ######### Customize Container Here ###########


    ######### End Customizations ###########

    RUN chown 1000:0 $HOME
    RUN $STARTUPDIR/set_user_permission.sh $HOME

    ENV HOME /home/kasm-user
    WORKDIR $HOME
    RUN mkdir -p $HOME && chown -R 1000:0 $HOME

    USER 1000
  2. Set the single-app XFCE config files as your default XFCE config and remove the xfce4-panel.

    FROM kasmweb/core-ubuntu-jammy:1.19.0-rolling-weekly
    USER root

    ENV HOME /home/kasm-default-profile
    ENV STARTUPDIR /dockerstartup
    ENV INST_SCRIPTS $STARTUPDIR/install
    WORKDIR $HOME

    ######### Customize Container Here ###########

    ## --> Update the desktop environment to be optimized for a single application
    RUN cp $HOME/.config/xfce4/xfconf/single-application-xfce-perchannel-xml/* $HOME/.config/xfce4/xfconf/xfce-perchannel-xml/

    ## --> Optionally, set a background image
    RUN cp /usr/share/backgrounds/bg_kasm.png /usr/share/backgrounds/bg_default.png

    ## --> Remove the xfce4-panel
    RUN apt-get remove -y xfce4-panel

    ######### End Customizations ###########

    RUN chown 1000:0 $HOME
    RUN $STARTUPDIR/set_user_permission.sh $HOME

    ENV HOME /home/kasm-user
    WORKDIR $HOME
    RUN mkdir -p $HOME && chown -R 1000:0 $HOME

    USER 1000
  3. Write installation logic as a bash script to install the custom application of your choice. As an example, to install Discord, write an installation bash script like the following and name it install_discord.sh. For another example, see this Chromium install script.

    #!/usr/bin/env bash
    set -ex

    # Install Discord from deb
    apt-get update
    curl -L -o discord.deb "https://discord.com/api/download?platform=linux&format=deb"
    apt-get install -y ./discord.deb
    rm discord.deb

    # Default config values
    mkdir -p $HOME/.config/discord/
    echo '{"SKIP_HOST_UPDATE": true}' > $HOME/.config/discord/settings.json

    # Desktop file setup
    sed -i "s@Exec=/usr/share/discord/Discord@Exec=/usr/share/discord/Discord --no-sandbox@g" /usr/share/applications/discord.desktop
    cp /usr/share/applications/discord.desktop $HOME/Desktop/
    chmod +x $HOME/Desktop/discord.desktop

    # Cleanup
    if [ -z ${SKIP_CLEAN+x} ]; then
    apt-get autoclean
    rm -rf \
    /var/lib/apt/lists/* \
    /var/tmp/* \
    /tmp/*
    fi

    # Cleanup for app layer
    chown -R 1000:0 $HOME
    find /usr/share/ -name "icon-theme.cache" -exec rm -f {} \;

  4. Write a custom_startup.sh script that automatically launches your custom application on workspace startup. For another example, see this Chromium startup script.

    #!/usr/bin/env bash
    set -ex
    START_COMMAND="/usr/share/discord/Discord"
    PGREP="Discord"
    export MAXIMIZE="true"
    export MAXIMIZE_NAME="Discord"
    MAXIMIZE_SCRIPT=$STARTUPDIR/maximize_window.sh
    DEFAULT_ARGS="--no-sandbox"
    ARGS=${APP_ARGS:-$DEFAULT_ARGS}

    options=$(getopt -o gau: -l go,assign,url: -n "$0" -- "$@") || exit
    eval set -- "$options"

    while [[ $1 != -- ]]; do
    case $1 in
    -g|--go) GO='true'; shift 1;;
    -a|--assign) ASSIGN='true'; shift 1;;
    -u|--url) OPT_URL=$2; shift 2;;
    *) echo "bad option: $1" >&2; exit 1;;
    esac
    done
    shift

    # Process non-option arguments.
    for arg; do
    echo "arg! $arg"
    done

    FORCE=$2

    kasm_exec() {
    if [ -n "$OPT_URL" ] ; then
    URL=$OPT_URL
    elif [ -n "$1" ] ; then
    URL=$1
    fi

    # Since we are execing into a container that already has the browser running from startup,
    # when we don't have a URL to open we want to do nothing. Otherwise a second browser instance would open.
    if [ -n "$URL" ] ; then
    /usr/bin/filter_ready
    /usr/bin/desktop_ready
    bash ${MAXIMIZE_SCRIPT} &
    $START_COMMAND $ARGS $OPT_URL
    else
    echo "No URL specified for exec command. Doing nothing."
    fi
    }

    kasm_startup() {
    if [ -n "$KASM_URL" ] ; then
    URL=$KASM_URL
    elif [ -z "$URL" ] ; then
    URL=$LAUNCH_URL
    fi

    if [ -z "$DISABLE_CUSTOM_STARTUP" ] || [ -n "$FORCE" ] ; then

    echo "Entering process startup loop"
    set +x
    while true
    do
    if ! pgrep -x $PGREP > /dev/null
    then
    /usr/bin/filter_ready
    /usr/bin/desktop_ready
    set +e
    bash ${MAXIMIZE_SCRIPT} &
    $START_COMMAND $ARGS $URL &
    set -e
    fi
    sleep 1
    done
    set -x

    fi

    }

    if [ -n "$GO" ] || [ -n "$ASSIGN" ] ; then
    kasm_exec
    else
    kasm_startup
    fi

    In the custom_startup.sh script above, the START_COMMAND contains the command that starts the application. The PGREP value contains the name of the application process, which can be a partial name of the process. The startup script uses this value to check the application and re-open it if it crashes or the user closes it. Set MAXIMIZE to true to maximize the application automatically. This requires that you also configure the MAXIMIZE_WINDOW name to match the window name of your application.

  5. Copy both the install script and the custom startup script to your image.

    FROM kasmweb/core-ubuntu-jammy:1.19.0-rolling-weekly
    USER root

    ENV HOME /home/kasm-default-profile
    ENV STARTUPDIR /dockerstartup
    ENV INST_SCRIPTS $STARTUPDIR/install
    WORKDIR $HOME

    ######### Customize Container Here ###########

    ## Update the desktop environment to be optimized for a single application
    RUN cp $HOME/.config/xfce4/xfconf/single-application-xfce-perchannel-xml/* $HOME/.config/xfce4/xfconf/xfce-perchannel-xml/

    ## Optionally, set a background image
    RUN cp /usr/share/backgrounds/bg_kasm.png /usr/share/backgrounds/bg_default.png

    ## Remove the xfce4-panel
    RUN apt-get remove -y xfce4-panel

    ## --> Copy installation script
    COPY ./install_discord.sh $INST_SCRIPTS/discord/
    ## --> Run installation script
    RUN bash $INST_SCRIPTS/discord/install_discord.sh && rm -rf $INST_SCRIPTS/discord/


    ## --> Copy custom_startup.sh script to the startup directory inside the image
    COPY ./custom_startup.sh $STARTUPDIR/custom_startup.sh
    ## --> Make it executable
    RUN chmod +x $STARTUPDIR/custom_startup.sh
    ## --> Set permissions
    RUN chmod 755 $STARTUPDIR/custom_startup.sh


    ######### End Customizations ###########

    RUN chown 1000:0 $HOME
    RUN $STARTUPDIR/set_user_permission.sh $HOME

    ENV HOME /home/kasm-user
    WORKDIR $HOME
    RUN mkdir -p $HOME && chown -R 1000:0 $HOME

    USER 1000

    Kasm executes the script $STARTUPDIR/custom_startup.sh automatically on workspace startup, which launches the application.

  6. Build the image.

    sudo docker build -t my_custom_singleapp_image -f Dockerfile .

Push the image to a container registry

To simplify image management, use a Docker container registry. This example uses a container registry provided by GitLab.

  1. Log in to the Docker container registry. You are prompted for a username and password. GitLab provides the ability to create Personal Access Tokens with permissions limited to read and write permissions to the registry.

    sudo docker login registry.gitlab.com
  2. Build the image, using the registry location as part of the image name.

    sudo docker build -t registry.gitlab.com/my-company/my-project/sublime-text:example -f Dockerfile .
  3. Push the image to the registry.

    sudo docker push registry.gitlab.com/my-company/my-project/sublime-text:example
  4. Register the image in Kasm by creating a new workspace that points to the Docker image. Enter the GitLab username in the Docker Registry Username field and the GitLab password or access token in the Docker Registry Password field.

    Using a Docker Container Registry

    Using a Docker Container Registry

Present a custom launch form

Administrators can present custom forms to users when a workspace launches. A custom form is helpful when creating advanced turnkey solutions for a given workspace. In the following example, a form element gets an authentication key for a Tailscale VPN connection. The workspace container then uses this key to establish the connection.

Launch Config Form

Launch Config Form

See Workspace Launch Forms for more details.

Understanding kasm-default-profile

In the Dockerfile template used above, the pre-customization steps set the HOME environment variable to /home/kasm-default-profile.

ENV HOME /home/kasm-default-profile

The post-customization steps set the HOME directory to /home/kasm-user, which is the profile directory used when the container starts.

ENV HOME /home/kasm-user

This process supports the Persistent Profiles feature. When a container does not use persistent profiles, or the first time a user creates a session when persistent profiles are enabled, the /home/kasm-user directory is empty. When the container starts, if the path /home/kasm-user is empty, the container copies the default profile from /home/kasm-default-profile to /home/kasm-user. When persistent profiles are enabled, the /home/kasm-user directory is not empty for the user's next session.

The copy script resides within the core images and can be adjusted if necessary. View it in the workspaces-core-images GitHub repository.

General Docker images

Kasm Workspaces is intended primarily for UI streaming containers. However, Workspaces can also orchestrate containers using any image. Images that are not based on one of the Kasm-maintained core images are incompatible with some features:

  • Web Filtering
  • URL Categorization
  • Connecting to the container through the UI

By default, Workspaces applies a restart policy of 'unless-stopped' to containers, which means the container restarts automatically unless it is manually stopped. This may or may not be desired. To launch containers that perform a task and exit, override the default restart policy.

Workspaces also runs every container as USER 1000 unless overridden.

In the Workspaces Admin UI, navigate to Workspaces, edit the desired Workspace, and in the Docker Run Config Override (JSON) field, set the restart policy to 'on-failure' and optionally the container user as shown here.

{"restart_policy":{"Name":"on-failure","MaximumRetryCount":5}, "user": "root"}

With this policy applied, when a user creates an instance of this image, it is removed automatically when the container finishes running.

Routing image traffic through a VPN

To route an image's traffic through a sidecar VPN container, see VPN Sidecar Containers.

Common troubleshooting steps

  • A newly built image is pruned before it can launch. When you build directly on the Kasm server or Agent, disable Automatically Prune Images for that Agent. See Agent Settings.
  • The workspace fails to start or the image is reported as not found. Kasm requires an explicit tag in the workspace Docker Image field. Kasm does not assume the latest tag. Specify a tag that reflects the version of the core image used as a base.
  • A desktop icon does not appear or does not launch. Confirm that the .desktop file is copied to $HOME/Desktop/, marked executable, and owned by user and group 1000.
  • The application does not start automatically. Confirm that custom_startup.sh is placed in $STARTUPDIR, is executable, and calls /usr/bin/desktop_ready. For single-app workspaces, verify that START_COMMAND and PGREP match the installed application.
  • Pushing to the registry fails with an authentication error. Confirm that you are logged in with docker login and that the GitLab access token has read and write permissions to the registry.
  • A custom-built image is missing core features such as web filtering or UI connection. Base the image on a Kasm core image. Images that are not based on a core image are incompatible with some platform features.