Haskell tooling in a Docker container

Create a Docker image with Debian Linux in which you can install and use Haskell tooling via ghcup and that uses your computer's native GUI. This can be useful because using ghcup under NixOS is a bit of a nuisance.

On the Docker host computer, if you run the container manually, do not forget to use xhost to allow access to the X-server, as follows.

1xhost +LOCAL:

See the Makefile for an easier way to to build and start the container.

The below project is on Codeberg: photonsphere/haskell-tooling-docker. I use it under NixOS.

Shared files with container

This is done via bind mounts for the ~/src and ~/Development subdirectories (change these for your environment) and also various files and subdirectories in ~/lib/haskell (for persistence and sharing common Haskell configuration between projects). Check if your uid is 1000 via the id command. If it is not then find and change all uid instances in the files of the project and change the 1000 to your uid value.

Define the image via a Dockerfile

The Dockerfile:

 1FROM debian:unstable
 2
 3# Install dependencies
 4# RUN sed -i "s#\smain\s*\$# main contrib non-free#" /etc/apt/sources.d/debian.list
 5RUN apt-get update; \
 6    apt-get upgrade -y; \
 7    apt-get install -y \
 8    procps \
 9    build-essential \
10    file \
11    curl \
12    sudo \
13    tmux \
14    git \
15    bat \
16    eza \
17    silversearcher-ag \
18    vim-nox \
19    fonts-hack \
20    emacs \
21    chromium \
22    pkg-config \
23    libffi-dev \
24    libgmp-dev \
25    libncurses-dev \
26    libpq-dev \
27    zlib1g-dev \
28    tzdata; \
29    apt-get clean
30
31    # && apt-get autoremove -y \
32    # && rm -rf /var/lib/apt/lists/* 
33    # && ln -sf /usr/share/zoneinfo/Europe/Amsterdam /etc/localtime
34
35# Set up haskell user
36RUN useradd -ms /bin/bash --uid 1000 --gid 100 haskell; \
37    usermod -G audio,video,sudo haskell; \
38    echo "haskell:yourchosenpassword" | chpasswd
39USER haskell
40WORKDIR /home/haskell
41
42# Configure PATH
43ENV HOME=/home/haskell \
44    PATH=/home/haskell/.ghcup/bin:/home/haskell/bin:/home/haskell/.local/bin:$PATH \
45    LC_ALL=C.UTF-8 \
46    DISPLAY=:0.0
47
48# Install ghcup (or later in .bashrc)
49# ENV BOOTSTRAP_HASKELL_NONINTERACTIVE=1
50# RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
51
52RUN mkdir -p /home/haskell/bin
53
54# Install GHC and cabal-install (or do this manually, later)
55# RUN mkdir -p /home/haskell/bin \
56#     && ghcup install ghc 9.6.7 --set \
57#     && ghcup install cabal 3.10.3.0 --set \
58#     && cabal update
59
60# CMD ["bash"]

This section in e.g. your ~/.bashrc file installs ghcup, and uses it to install recommended version of Haskell Language Server and indirectly of cabal, stack and ghc:

 1ghcup --version 
 2if [ $? -ne 0 ]; then
 3    # Install ghcup
 4    echo "Installing ghcup ..."
 5    export BOOTSTRAP_HASKELL_NONINTERACTIVE=1;
 6    curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh;
 7
 8    # Install GHC and cabal-install if not in default (or do this manually, later)
 9    ghcup install hls recommended --set;
10    # The install of hls installs recommended ghc, cabal and stack
11    # ghcup install ghc recommended --set;
12    # ghcup install cabal recommended --set;
13    # ghcup install stack recommended --set;
14    cabal update;
15    echo "\t done.";
16fi

(The call of ghcup --version is used to not install it when it's already installed.)

The Docker compose file

 1services:
 2  haskell:
 3    image: tooling-haskell
 4    build:
 5      context: .
 6      dockerfile: Dockerfile
 7    stdin_open: true
 8    tty: true
 9    privileged: true
10    devices:
11      - "/dev/snd/:/dev/snd/"
12    ipc: host
13    environment:
14      - TZ=Europe/Amsterdam
15    network_mode: host
16    volumes:
17      - "~/lib/haskell/.inputrc:/home/haskell/.inputrc:rw"
18      - "~/lib/haskell/.tmux.conf:/home/haskell/.tmux.conf:rw"
19      - "~/lib/haskell/.bashrc:/home/haskell/.bashrc:rw"
20      - "~/lib/haskell/.ghcup/:/home/haskell/.ghcup/:rw"
21      - "~/lib/haskell/.stack/:/home/haskell/.stack/:rw"
22      - "~/lib/haskell/.cabal:/home/haskell/.cabal:rw"
23      - "~/lib/haskell/.local/:/home/haskell/.local/:rw"
24      - "~/lib/haskell/usr/local/:/usr/local/:rw"
25      - "~/src/:/home/haskell/src/:rw"
26      - "~/Development/:/home/haskell/Development/:rw"
27      - "~/.gitconfig:/home/haskell/.gitconfig/:rw"
28      - "~/.git-credential-cache:/home/haskell/.git-credential-cache/:rw"
29      - "~/.ssh/:/home/haskell/.ssh/:rw"
30      - "~/.gnupg/:/home/haskell/.gnupg/:rw"
31      - "~/.emacs.d/:/home/haskell/.emacs.d/:rw"
32      - "~/.Xauthority:/home/haskell/.Xauthority:rw"
33      - "/tmp/.X11-unix/:/tmp/.X11-unix/:ro"
34      - "/var/lib/usbmux:/var/lib/usbmux"
35      - "/var/run/user/1000/pulse:/run/user/1000/pulse"
36      - "/etc/asound.conf:/etc/asound.conf"
37
38  # TODO Fix postgres:17 `permission denied for schema public` errors.
39  database:
40    image: postgres:14  # Bumped from 9 for security/support
41    environment:
42      - POSTGRES_USER=postgres
43      - POSTGRES_PASSWORD=postgres  # Consider changing for security
44      # - POSTGRES_DB=dbname  # Optional: creates a default database
45      - TZ=Europe/Amsterdam
46    network_mode: host
47    volumes:
48      - pgdata:/var/lib/postgresql/data  # Persist database data
49    # Optional: Initialize with a SQL script
50    # volumes:
51    #   - ./sql/001.initial.sql:/docker-entrypoint-initdb.d/001.initial.sql
52
53volumes:
54  pgdata:  # Named volume for PostgreSQL data persistence

Makefile

Use the make command to build the Docker container and bring it up. The Makefile is shown below. Initially use make rebuild to create the container. Use make database for an interactive shell in the database container and make haskell for an interactive shell in the Haskell tooling container. To bring it down completely, use make down.

 1.PHONY: help
 2help: ## print make targets 
 3	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
 4
 5.PHONY: edit
 6edit:   ## Start editor
 7	emacs &
 8
 9HASKELL="haskell-dev-haskell-1"
10DATABASE="haskell-dev-database-1"
11
12.PHONY: up
13up: ## bring up development container
14	docker-compose up -d
15
16.PHONY: down
17down: ## bring down development container
18	sync -f
19	docker-compose down
20
21# If problems persist after a force-down then manually restart Docker daemon.
22.PHONY: force-down
23force-down: ## forcibly bring down development container
24	sync -f
25	docker rm -f $(HASKELL)
26
27.PHONY: ps
28ps: ## list containers
29	docker ps -a
30
31.PHONY: start
32start: ## start Haskell development container
33	docker start $(HASKELL)
34
35.PHONY: stop
36stop: ## stop Haskell development container
37	docker stop $(HASKELL)
38
39# Get custom seccomp profile (the wget) for browser sound.
40.PHONY: rebuild
41rebuild: ## fully rebuild development container
42	docker-compose build --no-cache
43
44.PHONY: build
45build: ## build development container
46	docker-compose build
47
48.PHONY: haskell
49haskell: ## shell into Haskell development container
50	docker exec -it $(HASKELL) /bin/bash
51
52.PHONY: database
53database: ## shell into database development container
54	docker exec -it $(DATABASE) /bin/bash