This post is a long time in coming.  A long time. The goal was to setup ConnectWise Control in a Docker container and use some sort of web proxy with Let's Encrypt to provide SSL encryption over https. There were a lot of new technologies to learn and then there problems, more problems and even more problems on top of that...

...And I solved them all. My hope others do suffer the frustrations I did.

more to come...

...so after I wrote this I proceeded to overwrite my working configuration while trying to do a backup in order to transfer everything over to the new server. Back to the beginning, the third time's the charm.

Step 1: Install Docker

There are plenty of articles on how to install Docker on the Internet. I am using Ubuntu 20.04 LTS has my host system and I used this article from Digital Ocean:

How To Install and Use Docker on Ubuntu 20.04 | DigitalOcean
Docker is an application that simplifies the process of managing application processes in containers. In this tutorial, you’ll install and use Docker Community Edition (CE) on Ubuntu 20.04. You’ll install Docker itself, work with containers and images, and push an image to a Docker Repository.

Step 1.5 Install docker-compose

This article from digitalocean explains how to do this quite well:

How To Install and Use Docker Compose on Ubuntu 20.04 | DigitalOcean
Docker Compose is a tool that allows you to run multi-container application environments based on definitions set in a YAML file. It uses service definitions to build fully customizable environments with multiple containers that can share networks and data volumes. In this guide, we’ll demonstrat…

Step 2: Build ScreenConnect Image

I initially built this ScreenConnect image over a year ago and some of the details are a bit hazy. The original image was based on version 19.4 running on Ubuntu 18.04. Ultimately, I wanted to end up with the latest version of ScreenConnect for Linux running on the latest version of Ubuntu which at the time of writing this article is ScreenConnect 20.2 and Ubuntu 20.04 LTS.

Note: When I refer to ScreenConnect I am referring to the ScreenConnect application. When I refer to screenconnect I am referring to the docker container.

The upgrade process was a source of many problems. The good news is that I figured them out and hopefully this will save you a lot of time.

First I created a directory to hold all the files related to creating the image and container. I called it screenconnect and you can call it whatever works for you.

You will need the following in the screenconnect directory:

  • a Dockerfile for building the image (see below)
  • a subdirectory 'setup' for holding all the ScreenConnect Installers. I needed 19.4, 19.6 and 20.2 but more on that later.
  • supervisord.conf (see below)

Here is the Dockerfile:

FROM ubuntu

ENV SC_INSTALL_FILE ScreenConnect_19.4.25759.7247_Release.tar.gz

MAINTAINER Max Abramowitz max@williammax.com

RUN apt-get update && apt-get -y upgrade
RUN DEBIAN_FRONTEND=noninteractive apt-get -y install mono-runtime ca-certificates-mono supervisor

WORKDIR /tmp
COPY setup/$SC_INSTALL_FILE /tmp

RUN tar -zxvf $SC_INSTALL_FILERUN echo -e "\n\n" | ScreenConnect_*_Install/install.sh

RUN DEBIAN_FRONTEND=noninteractive apt-get -y install libc6-dev libz-dev

WORKDIR /lib/x86_64-linux-gnuRUN mv libc.so libc.so.oldRUN ln -s libc-2.31.so libc.so

EXPOSE 8040 8041

WORKDIR /opt/screenconnect

COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf

Dockerfile Explaination

FROM Ubuntu

Pretty straight forward. Get the latest Ubuntu image to use as our base image.

ENV SC_INSTALL_FILE ScreenConnect_19.4.25759.7247_Release.tar.gz

Create an ENVironment variable SC_INSTALL_FILE. This makes is relatively easy to build new images as needed. So if you want to build a ScreenConnect image based on version 20.2 you would just replace ScreenConnect_19.4.25759.7247_Release.tar.gz with ScreenConnect_20.2.29488.7513_Release.tar.gz or whatever is appropriate for your needs.

MAINTAINER Max Abramowitz <max@williammax.com>

Added a maintainer tag to give myself some credit.

RUN apt-get update && apt-get -y upgrade

Again pretty straight forward update and upgrade ubuntu with apt-get.

RUN DEBIAN_FRONTEND=noninteractive apt-get -y install mono-runtime ca-certificates-mono supervisor

Install mono-runtime ca-certificates-mono and supervisor. DEBIAN_FRONTEND=noninteractive tells apt-get to accept all the default answers for all questions (if any).

WORKDIR /tmp

Set the working directory to /tmp

COPY setup/$SC_INSTALL_FILE /tmp

Copy the installer defined at the top of the file from the setup directory of your host system into the /tmp directory of the image you are building.

RUN tar -zxvf $SC_INSTALL_FILE

Extract the installation files.

RUN echo -e "\n\n" | ScreenConnect_*_Install/install.sh

Send two newline characters to install.sh. This is the equivalent of running the install.sh script and hitting enter/return twice to accept the default values.

RUN DEBIAN_FRONTEND=noninteractive apt-get -y install libc6-dev libz-dev
WORKDIR /lib/x86_64-linux-gnu
RUN mv libc.so libc.so.old
RUN ln -s libc-2.31.so libc.so

This next section of commands is needed for Ubuntu 20.04. It was not necessary in Ubuntu 18.04. If you do not include it the image will build (great!) and ScreenConnect will not run (bad!).

In summary: install libc6-dev and libz-dev, rename libc.so and create a symbolic link.

EXPOSE 8040 8041

Expose port 8040 and 8041

WORKDIR /opt/screenconnect

Set the working directory to /opt/screenconnect

COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf

And finally copy the supervisord.conf from your host system to /etc/supervisor/conf.d/ on your image.

Supervisor

This part of the build I did so long ago I kind of forgot about it. One of the confusing things for me about Docker is that a container is not a virtual machine. Containers do one thing (mostly) and when they are done doing that thing they stop. This is a problem for programs that run as services, because if you try to run them in a Docker container you will find that they run and then terminate which terminates your Docker container. Supervisor is a way to get around this.

supervisord.conf

[supervisord]
nodaemon=true

[program:ScreenConnect]
command=/etc/init.d/screenconnect start

I have forgotten why this works, but it does.

So now that you have all your files in place it is time to build your image:

docker build -t wmc/sc -t wmc/sc:19.4 .

The -t options are tags. I tagged each image with a version number and left the version number out to indicated that this was the latest image when appropriate.

Building the image will take a minute or two.

Step 3: Create a docker-compose.yml file

We are going to do this in two steps. First we are going to create a docker-compose.yml file that will run our ScreenConnect image just all by its lonesome, then once we are sure that this is working we will proxy traffic with Traefik and encrypt our traffic with Let's Encrypt.

version: '3'

services:
  screenconnect:
    container_name: sc
    image: wmc/sc:20.2
    restart: unless-stopped
    volumes:
      - opt:/opt/screenconnect
    ports:
      - 8040:8040
      - 8041:8041

volumes:
   opt:

This is a relatively simple docker-compose.yml file. Creates screenconnect service, with a container named "sc" using the image created in the previous step. Defines a volume to be used by the container and makes ports 8040 and 8041 reachable on the network.

Test that the image is working with:

docker-compose up -d

Tips & Tricks

Monitoring ScreenConnect Startup

ScreenConnect can take a long time to startup. This caused me a huge amount of problems until I was able to properly monitor ScreenConnect's startup process. To do this, first get a shell to the running container:

docker exec -it sc bash

Then, tail -f the screenconnect log file:

tail -f /var/log/screenconnect

You will initially see this:

When ScreenConnect completes it startup process, you will see this:

At this point you should be able to access your running instance of ScreenConnect at http://yourhostname.yourdomain.com:8040.

Migration & Upgrading

At this point you should migrate from an old server and upgrade you screenconnect container.

You cannot copy files from one version of ScreenConnect to another and expect it to work. If you need to migrate from an old version of ScreenConnect start by building an image based on the version of ScreenConnect you are running, migrate your data and settings and then upgrade the container. My recommendation is to validate your work at each step: build the base version and test, migrate data and test, upgrade container and test.

You will need to perform upgrades on the running container (see below). I also recommend upgrading from one stable version to another, don't jump versions. I was upgrading from 19.4 to 20.2 and the when I tried to upgrade from 19.4 to 20.2 screenconnect threw a error related to Mono and did not work. Eventually, I figured out that it was necessary to upgrade to 19.6 first, then 20.2.

Accessing /opt/screenconnect

All the ScreenConnect files live in /opt/screenconnect. On my implementation these files live in /var/lib/docker/volumes/screenconnect_opt/_data. You need to be root to access this directory.

Upgrading ScreenConnect

  1. Download the installation
  2. Copy to /var/lib/docker/volumes/screenconnect_opt/_data
  3. Move out of /opt/screenconnect
  4. Extract files from tar.gz file
  5. Run install.sh

Steps #3-5 need to be done from within the container. Use

docker exec -it sc bash

to get a container shell to do this.

External Accessibility Check

Once your data is migrated and your container upgraded to the latest version Click on Admin -> Status in the ScreenConnect application and check the External Accessibility Check. In my testing both the Web Server Test URL and Relay Test URL failed in version 19.4 and 19.6 and the Relay Test URL failed in version 20.2.

Don't worry about this while you are upgrading. Only once the upgrade process is complete should this be addressed (if needed).

Edit the web.config file in /var/lib/docker/volumes/screenconnect_opt/_data. Add the following to fix the Web Server Test URL:

<add key="WebServerAddressableUri" value="http://host.yourdomain.com:8040" />

And this to fix the Relay Test URL:

<add key="RelayAddressableUri" value="relay://host.yourdomain.com:8041" />

At this point all your data should be migrated, your screenconnect container upgraded to the latest version and any external accessibility issues resolved. Now all you need to do is update the image in the docker-compose.yml file to your latest image.

Pat yourself on the back. You did great.

Step 4: Traefik & Let's Encrypt

ScreenConnect has a back a$$wards way of implementing SSL. I could never figure it out. No matter, because it is possible to secure your screenconnect instance using Traefik and SSL.

What is Traefik?

Traefik is a modern HTTP reverse proxy and load balancer. Its tag line is, "Makes Networking Boring". I found this partly true. Traefik does make setting up reverse proxy easier than other softwares, but getting it to work was a challenge for me. It has great documentation which kind of sucks. It is great, because it lays out what you can do very well. It sucks, because it does not inform you on how to do it. There are multiple ways to configured Traefik: command-line, files and directly in a container.

Complaints aside, it is  better than the alternatives.

Here is the docker-compose.yml file I used to create my Traefik container:

version: '3'

services:
  traefik:
    container_name: traefik
    image: traefik:v2.4
    restart: unless-stopped
    command:
      - "--providers.docker=true"
#      - "--api.insecure=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--certificatesResolvers.le.acme.email=email@yourdomain.com"
      - "--certificatesResolvers.le.acme.storage=acme.json"
      - "--certificatesResolvers.le.acme.tlsChallenge=true"
      - "--certificatesResolvers.le.acme.httpChallenge=true"
      - "--certificatesResolvers.le.acme.httpChallenge.entryPoint=web"

    networks:
      - proxy

    ports:
      - 80:80
      - 443:443
      - "8080:8080"

    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/acme.json:/acme.json

And here is the updated docker-compose.yml I used for my screenconnect container:

version: '3'

services:
  screenconnect:
    container_name: sc
    image: wmc/sc:20.2
    restart: unless-stopped
    volumes:
      - opt:/opt/screenconnect
    ports:
      - 8041:8041
    expose:
      - 8040
    labels:
      - traefik.enable=true
      - traefik.http.routers.sc.rule=Host(`host.yourdomain.com`)
      - traefik.http.services.sc.loadbalancer.server.port=8040
      - traefik.http.routers.sc.entrypoints=websecure
      - traefik.http.routers.sc.tls=true
      - traefik.http.routers.sc.tls.certresolver=le
    networks:
      - proxy

volumes:
   opt:

networks:
  proxy:
    external: true

Let's go through the interesting bits line by line:

- "--providers.docker=true"

Traefik can work with several different container platforms this command says, "work with docker".

- "--api.insecure=true"

Tells Traefik to make its dashboard available. You can access it by going to http://host.yourdomain.com:8080. Traffic is not encrypted and there is no password. There are ways to implement these things and I did not do that. I have this commented out in my docker-compose.yml file. I basically turn it on when I need to setup a new service and turn it off when I do not need it.

- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"

Defines two entry points: web and websecure. Note that "web" and "websecure" can be whatever you want and are referred to in later configurations.

- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"

Defines http redirector for the "web" entry point. Says redirect to websecure and use HTTPS.

- "--certificatesResolvers.le.acme.email=email@yourdomain.com"
- "--certificatesResolvers.le.acme.storage=acme.json"
- "--certificatesResolvers.le.acme.tlsChallenge=true"
- "--certificatesResolvers.le.acme.httpChallenge=true"
- "--certificatesResolvers.le.acme.httpChallenge.entryPoint=web"

Configures Let's Encrypt. "le" stands for Let's Encrypt, but this can be whatever you want it to be. For example, it could be "letsencrypt", "freeca" or whatever makes the most sense to you.

Now for the interesting bits in the screenconnect docker-compose.yml

ports:
  - 8041:8041
expose:
  - 8040

We keep ScreenConnect's relay port reachable by the Internets (this is already encrypted). We expose port 8040 internally on the Docker networks.

- traefik.enable=true

Enable traefik on this container.

- traefik.http.routers.sc.rule=Host(`host.yourdomain.com`)

Rule to match http traffic to host.yourdomain.com. Note: the "sc" can be whatever you wish.

- traefik.http.services.sc.loadbalancer.server.port=8040

This is how you tell traefik that you want to forward this traffik to port 8040 on the screenconnect container.

- traefik.http.routers.sc.entrypoints=websecure
- traefik.http.routers.sc.tls=true
- traefik.http.routers.sc.tls.certresolver=le

Defines the entry point, tells it to use TLS and to use Let's Encrypt as a certificate resolver.

Notice the use of "websecure" and "le" these were defined in the configuration of traefik in its docker-compose.yml file.

Last note: you will have to create the proxy network manually.

docker network create proxy

That is it. I think. I hope this saves you time. Let me know if you have any questions or comments.

Max Abramowitz
max@williammax.com