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