How to install Apache Guacamole with Docker and MySQL

I've been using jwetzell/guacamole docker image for quite some time now and mostly been happy with it. But I would like to start using the official Guac image as I prefer to use official images in case development of a custom image gets dropped in the future.

The reason why I started with the custom image was simple - I had issues with running the official image and I couldn't figure out what I was doing wrong. So, I decided to tackle this project again and I was intent on making it work this time.

Finally, with some tweaking, I was able to make it work.

So, let's start with the easy part, docker-compose file:

version: "3"
services:

# daemon
  bastion-dmn:
    image: guacamole/guacd
    container_name: bastion-dmn
    restart: unless-stopped
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - /path/to/docker/dir/drive:/drive:rw
      - /path/to/docker/dir/record:/record:rw
    networks:
      other:

# mysql
  bastion-db:
    container_name: bastion-db
    image: mariadb:10.9.5
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_DATABASE: 'guacamole_db'
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - '/path/to/docker/dir/db-data:/var/lib/mysql'
    networks:
      other:
    restart: always
    
# guacamole
  bastion-fe:
    container_name: acceess-fe
    depends_on:
    - bastion-dmn
    - bastion-db
    environment:
      GUACD_HOSTNAME: bastion-dmn
      MYSQL_DATABASE: guacamole_db
      MYSQL_HOSTNAME: bastion-db
      MYSQL_PASSWORD: '${MYSQL_PASSWORD}'
      MYSQL_USER: '${MYSQL_USER}'
    image: guacamole/guacamole:latest
    links:
    - bastion-dmn
    restart: always
    volumes:
    - /path/to/docker/dir/drive:/drive:rw
    labels:
      - traefik.enable=true
      - traefik.http.services.bastion.loadbalancer.server.port=8080
      - "traefik.http.routers.bastion.rule=Host(`bastion.domain.com`)"
      - traefik.http.routers.bastion.tls.certresolver=zero
      - traefik.http.routers.bastion.entrypoints=websecure
      - traefik.http.routers.bastion.middlewares=guac-addprefix
      - "traefik.http.middlewares.guac-addprefix.addprefix.prefix=/guacamole"
    networks:
      external:
      other:

networks:
  external:
    external: true
  other:
    external: true

A few things here. I'm using two networks - one called external and the other one called other. I'm used to placing dockers to different networks, even though both are considered external, just because I like to separate things that will be exposed to the Internet (such as guacamole container) from the ones that are staying local (guacd and mysql).

Also, I'm using previously configured traefik to provide reverse proxy to the container and from there I'm mapping port 8080 to be intercepted by traefik and provide access to it. I'm also providing SSL certificates to the container via previously configured certresolver called zero which uses ZeroSSL.

Another thing that I've added is a traefik middleware called guac-addprefix and adding it to the router bastion. This allows me to access the frontend by using the bastion.domain.com in URL instead of using bastion.domain.com/guacamole.

Everything else is more or less standard, except for a few variables (MYSQL_PASSWORD and MYSQL_USER) that are defined in the .env file.

Main issue I was having before was the fact that, if I remember correctly, I never initialized the database before I deployed the containers.

Hopefully I'm not missing any steps here, but this is how I did it this time and was actually successful.

First things first, we need to create an initial database. The easiest way to do it is this:

docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --mysql > initdb.sql

This will create a file called initdb.sql that we will use inside of a container to populate the database.

Next, let's start the database container only:

docker compose up -d bastion-db

Next, we will copy the previously created database file to the container itself:

docker cp initdb.sql bastion-db:/guac_db.sql

We could've mapped a directory to bastion-db and create the initial db there, but this works as well.

Then, we enter the container:

docker exec -it bastion-db bash

Because we initialized the container with user, database and root password, all of it should already be created in MySQL, but if not, you can always create them manually. First, enter MySQL as root user:

mysql -u root -p

Make modifications to the database (you can change the user and the password variabls to your liking):

ALTER USER 'root'@'localhost' IDENTIFIED BY 'password';
CREATE DATABASE guacamole_db;
CREATE USER 'guacamole_user'@'%' IDENTIFIED BY 'password';
GRANT SELECT,INSERT,UPDATE,DELETE ON guacamole_db.* TO 'guacamole_user'@'%';
FLUSH PRIVILEGES;

Remember, if you make changes to the database, you will most likely need to change some of the environment variables in your docker-compose file. Exit MySQL with quit command after you're done.

Finally, populate the initial database with the proper tables:

cat guac_db.sql | mysql -u root -p guacamole_db

If you want, you can check if the tables have been created:

mysql -u admin -p

I changed the guacamole_user to admin so I use that to log into MySQL. Once in, check status of the guacamole_db:

use guacamole_db;
show tables;

Result should be something like this:

+---------------------------------------+
| Tables_in_guacamole_db                |
+---------------------------------------+
| guacamole_connection                  |
| guacamole_connection_attribute        |
| guacamole_connection_group            |
| guacamole_connection_group_attribute  |
| guacamole_connection_group_permission |
| guacamole_connection_history          |
| guacamole_connection_parameter        |
| guacamole_connection_permission       |
| guacamole_entity                      |
| guacamole_sharing_profile             |
| guacamole_sharing_profile_attribute   |
| guacamole_sharing_profile_parameter   |
| guacamole_sharing_profile_permission  |
| guacamole_system_permission           |
| guacamole_user                        |
| guacamole_user_attribute              |
| guacamole_user_group                  |
| guacamole_user_group_attribute        |
| guacamole_user_group_member           |
| guacamole_user_group_permission       |
| guacamole_user_history                |
| guacamole_user_password_history       |
| guacamole_user_permission             |
+---------------------------------------+
23 rows in set (0.000 sec)

If everything is OK, quit MySQL with quit command and then use exit to exit the container itself.

Finally, it's time to start the rest of the containers. Run docker compose up -d or docker-compose up -d, depending on the version you're running and it should pull the rest of the containers and start them. Your new Apache Guacamole should now be available at https://bastion.domain.com, and if you have traefik properly set up, it should pull an SSL cert for you as well. Don't forget that you also need a working DNS server in your own network to resolve this new host.