Docker Compose

Running Kimai with Docker compose

Several docker-compose example files that use the latest Kimai version.

Apache (prod)

services:

  sqldb:
    image: mysql:8.3
    volumes:
      - mysql:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=kimai
      - MYSQL_USER=kimaiuser
      - MYSQL_PASSWORD=kimaipassword
      - MYSQL_ROOT_PASSWORD=changemeplease
    command: --default-storage-engine innodb
    restart: unless-stopped
    healthcheck:
      test: mysqladmin -p$$MYSQL_ROOT_PASSWORD ping -h localhost
      interval: 20s
      start_period: 10s
      timeout: 10s
      retries: 3

  kimai:
    image: kimai/kimai2:apache
    volumes:
      - data:/opt/kimai/var/data
      - plugins:/opt/kimai/var/plugins
    ports:
      - 8001:8001
    environment:
      - ADMINMAIL=admin@kimai.local
      - ADMINPASS=changemeplease
      - "DATABASE_URL=mysql://kimaiuser:kimaipassword@sqldb/kimai?charset=utf8mb4&serverVersion=8.3.0"
    restart: unless-stopped

volumes:
  data:
  mysql:
  plugins:

Environment variables

Instead of hardcoding your credentials and/or duplicating them in several places, you can also use a .env file like that:

DATABASE_NAME=kimai
DATABASE_USER=kimaiuser
DATABASE_PASSWORD=kimaipassword
DATABASE_ROOT_PASSWORD=changemeplease
ADMIN_EMAIL=admin@kimai.local
ADMIN_PASSWORD=changemeplease

And then reference those from your docker-compose.yaml:

services:

  sqldb:
    image: mysql:8.3
    volumes:
      - mysql:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=${DATABASE_NAME}
      - MYSQL_USER=${DATABASE_USER}
      - MYSQL_PASSWORD=${DATABASE_PASSWORD}
      - MYSQL_ROOT_PASSWORD=${DATABASE_ROOT_PASSWORD}
    command: --default-storage-engine innodb
    restart: unless-stopped
    healthcheck:
      test: mysqladmin -p$$MYSQL_ROOT_PASSWORD ping -h localhost
      interval: 20s
      start_period: 10s
      timeout: 10s
      retries: 3

  kimai:
    image: kimai/kimai2:apache
    volumes:
      - data:/opt/kimai/var/data
      - plugins:/opt/kimai/var/plugins
    ports:
      - 8001:8001
    environment:
      - ADMINMAIL=${ADMIN_EMAIL}
      - ADMINPASS=${ADMIN_PASSWORD}
      - "DATABASE_URL=mysql://${DATABASE_USER}:${DATABASE_PASSWORD}@sqldb/${DATABASE_NAME}?charset=utf8mb4&serverVersion=8.3.0"
    restart: unless-stopped

volumes:
  data:
  mysql:
  plugins:

Caddy Reverse Proxy

Kimai has extensive examples of using Caddy as reverse proxy in frontend of Kimai.

See Hetzner Cloud and Digital Ocean docs.

Installing plugins

When using plugins, you can either use a volume or a bind mount. As plugin are accessed read-only by Kimai, you can use both methods.

Using a volume:

services:
    [...]

  kimai:
    image: kimai/kimai2:apache
    volumes:
      - data:/opt/kimai/var/data
      - plugins:/opt/kimai/var/plugins
    [...]

volumes:
  data:
  plugins:

Or using a bind mount, pointing to the local filesystem at /home/kimai/plugins:

services:

  sqldb:
    [...]

  kimai:
    image: kimai/kimai2:apache
    volumes:
      - data:/opt/kimai/var/data
      - /home/kimai/plugins:/opt/kimai/var/plugins
    [...]

volumes:
  data:

Once the Docker container is running up, you need to step into the container and install the plugin:

docker exec -ti kimai /opt/kimai/bin/console cache:clear --env=prod
docker exec -ti kimai /opt/kimai/bin/console kimai:bundle:workcontract:install

Using a local.yaml

This requires a new mount, e.g. mounting the local file /home/kimai/local.yaml into the correct location inside the image.

services:
    [...]

  kimai:
    image: kimai/kimai2:apache
    volumes:
      - data:/opt/kimai/var/data
      - plugins:/opt/kimai/var/plugins
      - /home/kimai/local.yaml:/opt/kimai/config/packages/local.yaml
      
    [...]

volumes:
  data:
  plugins:

FPM and nginx

Listed here are example setups for running the image(s). If you’d like to contribute a new one them please raise a PR for this page.

services:

  sqldb:
    image: mysql:8.3
    volumes:
      - mysql:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=kimai
      - MYSQL_USER=kimaiuser
      - MYSQL_PASSWORD=kimaipassword
      - MYSQL_ROOT_PASSWORD=changemeplease
    command: --default-storage-engine innodb
    restart: unless-stopped
    healthcheck:
      test: mysqladmin -p$$MYSQL_ROOT_PASSWORD ping -h localhost
      interval: 20s
      start_period: 10s
      timeout: 10s
      retries: 3

  nginx:
    image: tobybatch/nginx-fpm-reverse-proxy
    ports:
      - 8001:80
    volumes:
      - public:/opt/kimai/public:ro
    restart: unless-stopped
    depends_on:
      - kimai
    healthcheck:
      test:  wget --spider http://nginx/health || exit 1
      interval: 20s
      start_period: 10s
      timeout: 10s
      retries: 3

  kimai:
    image: kimai/kimai2:latest
    environment:
      - ADMINMAIL=admin@kimai.local
      - ADMINPASS=changemeplease
      - "DATABASE_URL=mysql://kimaiuser:kimaipassword@sqldb/kimai?charset=utf8mb4&serverVersion=8.3.0"
    volumes:
      - public:/opt/kimai/public
      - plugins:/opt/kimai/var/plugins
      # - data:/opt/kimai/var/data
      # - ./ldap.conf:/etc/openldap/ldap.conf:z
      # - ./ROOT-CA.pem:/etc/ssl/certs/ROOT-CA.pem:z
    restart: unless-stopped

volumes:
    data:
    public:
    mysql:
    plugins:

Updating Kimai

The usual update step is simple: stop, pull latest version, restart.

This example is based on the Apache image used with the Docker compose plugin:

# Pull latest version
docker compose pull
# Stop and remove older version
docker compose down
# Start the container
docker compose up -d

FPM image

The FPM image will need to be upgraded with a manual step. Because the FPM image will have a HTTP proxy (e.g. caddy or nginx) serving the static assets the public directory is mounted into that container. This is done via volumes:

services:
    kimai:
        image: kimai/kimai2:latest
        ...
        volumes:
            - public:/opt/kimai/public
        ...
    nginx:
        ...
        volumes:
            - public:/opt/kimai/public:ro
    ...

When the Kimai image is updated, and the container is restarted any new assets in the public directory are never included. These will be things like CSS files, images and especially version specific javascript code! To fix this you need to copy the newer files from a fresh image over the top.

# You might need to use `docker volume ls | grep kimai` to find the name of your Kimai "public" volume
docker run --rm -ti -v kimai_public:/public --entrypoint /bin/bash kimai/kimai2
#                      ^^^^^^^^^^^^ -> Kimai public volume
cp -r /opt/kimai/public /
exit

Now you’ll need to tell the running kimai to update its assets:

docker-compose exec kimai /opt/kimai/bin/console assets:install

Apache (dev)

services:

  sqldb:
    image: mysql:8.3
    environment:
      - MYSQL_DATABASE=kimai
      - MYSQL_USER=kimaiuser
      - MYSQL_PASSWORD=kimaipassword
      - MYSQL_ROOT_PASSWORD=changemeplease
    command: --default-storage-engine innodb
    restart: unless-stopped
    healthcheck:
      test: mysqladmin -p$$MYSQL_ROOT_PASSWORD ping -h localhost
      interval: 20s
      start_period: 10s
      timeout: 10s
      retries: 3

  kimai:
    image: kimai/kimai2:apache-dev
    ports:
      - 8001:8001
    environment:
      - ADMINMAIL=admin@kimai.local
      - ADMINPASS=changemeplease
      - "DATABASE_URL=mysql://kimaiuser:kimaipassword@sqldb/kimai?charset=utf8mb4&serverVersion=8.3.0"
    restart: unless-stopped
Top