Apache Guacamole: manual installation with docker-compose

Kenneth KOFFI
9 min readFeb 19, 2023

--

Updated on 23/11/2023: This article has been revised to incorporate information relevant to the release of Apache Guacamole version 1.5.3.

Apache Guacamole is one of the best open source tools out there. I love it and use it daily. But unfortunately, I’ve already heard many people say that it’s too hard to install. So I decided to break this myth and popularize the deployment of this gem. Everyone should benefit from it.

What is Apache Guacamole ?

Apache Guacamole is a free, open source clientless remote desktop gateway that allows you to access remote Desktop and Server machines via a web browser. It supports standard protocols like VNC, RDP, and SSH, and use HTML5 for remote connection. It can run on most Linux distributions, and the client runs on any modern web browser. You don’t need to install any software on your system. Just browse and connect to any remote server defined on your server.

In this article, I will show you how to install Apache Guacamole with Docker on a Linux server.

Requirements

To follow this tutorial, all you need is a Linux server/laptop.

Deployment

I run all the commands in this article as root user. If this is not the case for you, you must precede each command with sudo.

Install docker and docker-compose

Download and execute the convenience script provided by docker.

curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh | grep -qE "ERROR: Unsupported distribution 'rocky'|ERROR: Unsupported distribution 'almalinux'" && sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo && sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin

Enable and start the docker service

sudo systemctl start docker
sudo systemctl enable docker

Add the current user to the docker group to allow him to execute docker commands without sudo

sudo usermod -a ${USER} -G docker

You will need to log out and log in back for the change to take effect.

Install Guacamole

Create a folder for the project

mkdir ${HOME}/docker-stack
cd ${HOME}/docker-stack

Initialize the database

mkdir -p ${HOME}/docker-stack/guacamole/init
chmod -R +x ${HOME}/docker-stack/guacamole/init
docker run --rm guacamole/guacamole:1.5.3 /opt/guacamole/bin/initdb.sh --postgresql > ${HOME}/docker-stack/guacamole/init/initdb.sql

You should get an output similar to this

Create the ${HOME}/docker-stack/guacamole/docker-compose.yml file:

nano ${HOME}/docker-stack/guacamole/docker-compose.yml

Paste the following content into it :

version: '3.9'

# networks
# create a network 'guacamole_net' in mode 'bridged'
networks:
guacamole_net:
driver: bridge
haproxy_net:
external: true

# services
services:
# guacd
guacd:
container_name: guacamole_backend
image: guacamole/guacd:1.5.3
networks:
guacamole_net:
restart: always
volumes:
- ./drive:/drive:rw
- ./record:/var/lib/guacamole/recordings:rw

# postgres
postgres:
container_name: guacamole_database
environment:
PGDATA: /var/lib/postgresql/data/guacamole
POSTGRES_DB: guacamole_db
POSTGRES_PASSWORD: '${POSTGRES_PASSWORD}'
POSTGRES_USER: '${POSTGRES_USER}'
image: postgres:15.0
networks:
guacamole_net:
restart: always
volumes:
- ./init:/docker-entrypoint-initdb.d:ro
- ./data:/var/lib/postgresql/data:rw

# guacamole
guacamole:
container_name: guacamole_frontend
depends_on:
- guacd
- postgres
environment:
GUACD_HOSTNAME: guacd
POSTGRESQL_DATABASE: guacamole_db
POSTGRESQL_HOSTNAME: postgres
POSTGRESQL_PASSWORD: '${POSTGRES_PASSWORD}'
POSTGRESQL_USER: '${POSTGRES_USER}'
POSTGRESQL_AUTO_CREATE_ACCOUNTS: true
image: guacamole/guacamole:1.5.3
links:
- guacd
networks:
- guacamole_net
- haproxy_net
restart: always
volumes:
- ./drive:/drive:rw
- ./record:/var/lib/guacamole/recordings

You may wonder what’s going on here, so let me explain. This docker-compose.yml file contains the declaration of all the docker containers needed to run Apache Guacamole. Guacamole is not a self-contained web application and is made up of many parts. By default, Guacamole need 3 containers to run:

  • guacamole_frontend: the web UI, the part of Guacamole that a user actually interacts with.
  • guacamole_backend: guacd is the heart of Guacamole, which dynamically loads support for remote desktop protocols (called “client plugins”) and connects them to remote desktops based on instructions received from the web application.
  • guacamole_database: the database that Guacamole will use for authentication and storage of connection configuration data.

Create the ${HOME}/docker-stack/guacamole/.env file to store the database credentials. Like this:

POSTGRES_PASSWORD='PleasePutAStrongPasswordHere'
POSTGRES_USER='guacamole_user'

The docker-compose.yml file also contains the declaration of two networks:

  • guacamole_net: the docker network to isolate communication between the different guacamole services.
  • haproxy_net: the docker network to link guacamole_frontend container and HAProxy container

You just said HAProxy ? What is that ?

HAProxy is a reverse proxy which can run inside a docker container. We’re going to put it in front of our guacamole stack to uplift the Guacamole UI to SSL/HTTPS.

Install HAProxy

Create a folder to hold our HAProxy configuration

mkdir -p ${HOME}/docker-stack/haproxy
cd ${HOME}/docker-stack/haproxy

Create the ${HOME}/docker-stack/haproxy/docker-compose.yml file

nano ${HOME}/docker-stack/haproxy/docker-compose.yml

and paste the following content inside:

version: '3.9'
services:
haproxy:
container_name: haproxy
image: haproxytech/haproxy-alpine:2.4
ports:
- 80:80
- 443:443
- 8404:8404
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
networks:
- haproxy_net
restart: always
environment:
ENDPOINT: '${ENDPOINT}'
networks:
haproxy_net:
name: haproxy_net
driver: bridge

Let’s define the HAProxy configuration file.
Create the ${HOME}/docker-stack/haproxy/haproxy.cfg file

nano ${HOME}/docker-stack/haproxy/haproxy.cfg

and paste the following content into it :

global
stats socket /var/run/api.sock user haproxy group haproxy mode 660 level admin expose-fd listeners
log stdout format raw local0 info
maxconn 50000

resolvers docker_resolver
nameserver dns 127.0.0.11:53

defaults
mode http
timeout client 10s
timeout connect 5s
timeout server 10s
timeout http-request 10s
default-server init-addr none
log global

frontend stats
bind *:8404
stats enable
stats uri /
stats refresh 10s

frontend myfrontend
mode http
bind :80
use_backend %[req.hdr(Host),lower]

backend "${ENDPOINT}"
server guacamole guacamole:8080 check inter 10s resolvers docker_resolver

These directives are used to define how the reverse proxy should work.

Let’s define the Fully Qualified Domain Name (FQDN) where we want to reach our guacamole web UI.
Create the file${HOME}/docker-stack/haproxy/.envwith the following template.

ENDPOINT="your fqdn"

In my case, I want to use 161-35-39-33.traefik.me as FQDN. Where 161.35.39.33 is the public IP address of my server hosting the guacamole app. So in my case, this is the ${HOME}/docker-stack/haproxy/.env file content:

ENDPOINT='161-35-39-33.traefik.me'

Then what about that .traefik.me appended at the end ?

That’s an excellent question !

traefik.me is a magic domain name that can be used by anyone on the internet. It provides wildcard DNS for any IP address. Say your host IP address is 159.74.28.170. Using traefik.me,

…and so on. This avoids you buying domain name to test your projects or editing /etc/hosts on your devices. On top of that, a wildcard SSL certificate signed by Let’s encrypt is available for *.traefik.me. Just grab the files from traefik.me and you’re ready to go.

Why don’t you just use the IP address of your server to access your application ?

That’s also a possibility but as I said, traefik.me provides free SSL certificate signed by a public CA and some applications integrations don’t work with IP address.

But buddy, feel free to use any FQDN that suits you. I was just giving you a tip. Now let’s continue our deployment.

At the end, your folder architecture should look like this:

Don’t forget to change files ownership

chown -R ${USER}:${USER} ${HOME}/docker-stack/

Now let’s bring everything up

docker compose -f ${HOME}/docker-stack/haproxy/docker-compose.yml up -d
docker compose -f ${HOME}/docker-stack/guacamole/docker-compose.yml up -d

Your output should look like this:

Et voilàà !! You get your Guacamole instance deployed

Access Apache Guacamole Dashboard

You can now access the Apache Guacamole web interface using the URL http://your-fqdn/guacamole. In my case, my instance is reachable at http://161-35-39-33.traefik.me/guacamole

The default admin credentials are:

  • username: guacadmin
  • password: guacadmin

Enable SSL

We have deployed our application and we are happy. But satisfaction is not yet at its peak. For the moment, we are using the HTTP protocol, which by default is not very secure. So we will add a security layer with an SSL/TLS certificate. There are many possibilities.

Use SSL certificate from traefik.me

As I said in the previous section, traefik.me offers free wildcard SSL certificate issued by Let’s encrypt.

To use it, we will add a new docker container to our HAProxy docker-compose configuration file. This docker container will use alpine image. Its role will be to: download the certificate files from traefik.me, convert them to HAProxy supported format and pass it to a docker volume shared by both containers.

So update your previous ${HOME}/docker-stack/haproxy/docker-compose.yml file like this:

version: '3.9'
services:
haproxy:
container_name: haproxy
image: haproxytech/haproxy-alpine:2.4
ports:
- 80:80
- 443:443
- 8404:8404
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
# New line added here
- certs:/usr/local/etc/haproxy/certs/
networks:
- haproxy_net
restart: always
environment:
ENDPOINT: '${ENDPOINT}'

############# New section added ##############
reverse-proxy-https-helper:
image: alpine
command: sh -c "cd /etc/ssl/traefik
&& wget traefik.me/cert.pem -O cert.pem
&& wget traefik.me/privkey.pem -O privkey.pem
&& cat cert.pem privkey.pem > traefik.me.pem"
volumes:
- certs:/etc/ssl/traefik
####################################################

networks:
haproxy_net:
name: haproxy_net
driver: bridge

############# New section added ##############
volumes:
certs:
##############################################

I have placed comments to highlight the changes made.

We also need to edit the HAProxy configuration file ${HOME}/docker-stack/haproxy/haproxy.cfg to instruct HAProxy to use the SSL certificate.

Update the frontend section of that file.

frontend myfrontend
mode http
bind :80
# New line added here
bind :443 ssl crt /usr/local/etc/haproxy/certs/traefik.me.pem
#http-request redirect scheme https code 301 unless { ssl_fc }
use_backend %[req.hdr(Host),lower]

Uncomment the line #http-request redirect scheme https code 301 unless { ssl_fc } if you want to redirect all HTTP requests to HTTPS.

Execute these commands to apply the changes made:

docker compose -f ${HOME}/docker-stack/haproxy/docker-compose.yml up -d
docker restart haproxy

Now you can access your instance with HTTPS protocol.
In my case, the URL is https://161-35-39-33.traefik.me/guacamole

Use SSL self-signed certificate

In case you want to use a self-signed certificate, here are the steps to follow.

Generate the certificate

mkdir ${HOME}/docker-stack/haproxy/certs
openssl req -nodes -newkey rsa:2048 -new -x509 -keyout ${HOME}/docker-stack/haproxy/certs/self-ssl.key -out ${HOME}/docker-stack/haproxy/certs/self.cert -subj '/CN=your-fqdn' -addext 'subjectAltName=DNS:your-fqdn'

NOTE: At the end of the above command, make sure to replace your-fqdn by the correct value of your FQDN.

Convert the certificate to HAProxy supported format

bash -c 'cat ${HOME}/docker-stack/haproxy/certs/self.cert ${HOME}/docker-stack/haproxy/certs/self-ssl.key > ${HOME}/docker-stack/haproxy/certs/haproxy-ssl.pem'

Update the ${HOME}/docker-stack/haproxy/docker-compose.yml file as below:

version: '3.9'
services:
haproxy:
container_name: haproxy
image: haproxytech/haproxy-alpine:2.4
ports:
- 80:80
- 443:443
- 8404:8404
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
# New line added here
- ./certs:/usr/local/etc/haproxy/certs/
networks:
- haproxy_net
restart: always
environment:
ENDPOINT: '${ENDPOINT}'
networks:
haproxy_net:
name: haproxy_net
driver: bridge

The difference between the old one and the new one:

diff --label 'old' -u --color=always ${HOME}/docker-stack/haproxy/docker-compose.yml.a --label 'new' ${HOME}/docker-stack/haproxy/docker-compose.yml
--- old
+++ new
@@ -9,6 +9,7 @@
- 8404:8404
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
+ - ./certs:/usr/local/etc/haproxy/certs/
networks:
- haproxy_net
restart: always

Update the frontend section of ${HOME}/docker-stack/haproxy/haproxy.cfg file

frontend myfrontend
mode http
bind :80
# New line added here
bind :443 ssl crt /usr/local/etc/haproxy/certs/haproxy-ssl.pem
#http-request redirect scheme https code 301 unless { ssl_fc }
use_backend %[req.hdr(Host),lower]

We can use the diff command to check the difference between the old and new version:

diff -u --color=always --label 'old version' ~/docker-stack/haproxy/hapr
oxy.cfg.a --label 'new version' ~/docker-stack/haproxy/haproxy.cfg
--- old version
+++ new version
@@ -24,6 +24,9 @@
frontend myfrontend
mode http
bind :80
+ # New line added here
+ bind :443 ssl crt /usr/local/etc/haproxy/certs/haproxy-ssl.pem
+ #http-request redirect scheme https code 301 unless { ssl_fc }
use_backend %[req.hdr(Host),lower]

Uncomment the line #http-request redirect scheme https code 301 unless { ssl_fc } if you wish to redirect all HTTP requests to HTTPS.

Execute these commands to apply the changes made

docker compose -f ${HOME}/docker-stack/haproxy/docker-compose.yml up -d
docker restart haproxy

Now you can access your Guacamole instance with HTTPS protocol.

Use CA signed SSL certificate

For certificates issued by public certificate authorities, the procedure is the same as for self-signed certificates. Except that you do not generate certificates yourself. You just have to adapt the paths. I mean replacing self.crt and self-ssl.key with the filenames of the certificate and associated private key respectively.

In this article, we deployed Apache Guacamole with docker and HAProxy as reverse proxy. Thank you for reading to the end, and see you soon for new articles.
You can reach me here in the comment section or on LinkedIn via this link.

--

--

Kenneth KOFFI

Administrateur systèmes avec une appétence pour le Cloud et le DevOps