diff --git a/recipes/Dockerfile b/recipes/Dockerfile
index e4f2cdffd4d3bc1a801a57fb7b30620b28480023..2310a136a835831a6a6865b2438311ebde6278d0 100644
--- a/recipes/Dockerfile
+++ b/recipes/Dockerfile
@@ -7,15 +7,20 @@ RUN apt-get update && apt-get install -y \
  && rm -rf /var/lib/apt/lists/* \
  && git clone --depth 1 --single-branch -b $BRANCH https://gitlab.pasteur.fr/nyx/larvatagger.jl $DIRNAME \
  && julia --project=$DIRNAME -e 'using Pkg; Pkg.instantiate()' \
- && ln -s /$DIRNAME/scripts/larvatagger.jl /bin
+ && ln -s /$DIRNAME/scripts/larvatagger.jl /bin \
+ && rm -f $DIRNAME/test/precompile* \
+ && rm -f $DIRNAME/scripts/larvatagger.jl
 
-COPY test/precompile.* test/data_sample* $DIRNAME/test/
+COPY scripts/larvatagger.jl $DIRNAME/scripts/
+COPY test/precompile* test/data_sample* $DIRNAME/test/
+COPY src/cli.jl $DIRNAME/src/
 
 ARG TIMEZONE=UTC
 RUN ln -snf /usr/share/zoneinfo/$TIMEZONE /etc/localtime \
  && echo $TIMEZONE > /etc/timezone \
  && $DIRNAME/test/precompile.sh --shallow \
- && rm -f test/data_sample*
+ && mv larvatagger.so /lib/ \
+ && rm -f $DIRNAME/test/data_sample*
 
 COPY external $DIRNAME/
 
@@ -30,7 +35,8 @@ RUN if [ "$BACKEND" = "MaggotUBA/20220418" ]; then \
  && python3 -m poetry remove structured-temporal-convolution \
  && python3 -m poetry add ../structured-temporal-convolution \
  && python3 -m poetry install \
+ && apt-get clean \
  && rm -rf /var/lib/apt/lists/*; \
     fi
 
-ENTRYPOINT ["larvatagger.jl"]
+ENTRYPOINT ["larvatagger.jl", "--sysimage", "/lib/larvatagger.so"]
diff --git a/scripts/larvatagger.jl b/scripts/larvatagger.jl
index ec839916cd9f96161c3b0f5a072bac9ca78b6666..609459d89d4c15f5951674920deada1dcfe72f34 100755
--- a/scripts/larvatagger.jl
+++ b/scripts/larvatagger.jl
@@ -1,97 +1,14 @@
 #!/bin/bash
 #=
 PROJECT_DIR=$(dirname $(dirname $(realpath "${BASH_SOURCE[0]}")))
-if [ "$1" == "open" ]; then FLAGS="-iq "; fi
+FLAGS=
+if [ "$1" == "--sysimage" ]; then FLAGS="--sysimage $2 "; shift 2; fi
+if [ "$1" == "open" ]; then FLAGS="$FLAGS -iq "; fi
 exec julia --project="$PROJECT_DIR" --color=yes --startup-file=no $FLAGS\
     "${BASH_SOURCE[0]}" "$@"
 =#
 
-using DocOpt
-using PlanarLarvae.Datasets, PlanarLarvae.Formats
-using LarvaTagger
-using JSServe: JSServe, Server
+projectdir = dirname(Base.active_project())
+include(joinpath(projectdir, "src/cli.jl")) # defines `main`
 
-usage = """Larva Tagger.
-
-Usage:
-  larvatagger.jl import <input-path> [<output-file>] [--id=<id>] [--framerate=<fps>]
-  larvatagger.jl open <input-path> [--backends=<path>] [--port=<number>] [--quiet]
-  larvatagger.jl open <input-path> [--viewer] [--browser]
-  larvatagger.jl train <backend-path> <data-repository> [--instance=<name>]
-  larvatagger.jl -h | --help
-
-Options:
-  -h --help          Show this screen.
-  -q --quiet         Do not show instructions.
-  --id=<id>          Run ID, e.g. `date_time`.
-  --framerate=<fps>  Camera frame rate, in frames per second.
-  --backends=<path>  Path to backend repository.
-  --port=<number>    Port number the server listens to.
-  --viewer           Disable editing capabilities.
-  --browser          Automatically open a browser tab at the served location.
-  --instance=<name>  Name of the trained model.
-
-"""
-
-function main()
-    parsed_args = docopt(usage)
-    if parsed_args["import"]
-        infile = parsed_args["<input-path>"]
-        if !isfile(infile)
-            @error "File not found" infile
-            return
-        end
-        outfile = parsed_args["<output-file>"]
-        kwargs = Dict{Symbol, Any}()
-        framerate = parsed_args["--framerate"]
-        if !isnothing(framerate)
-            kwargs[:framerate] = parse(Float64, framerate)
-        end
-        file = load(infile; kwargs...)
-        runid = parsed_args["--id"]
-        run = Run(isnothing(runid) ? file.run.id : runid, Track[];
-                  file.run.attributes[:metadata]...)
-        run.attributes[:labels] = String[]
-        Datasets.pushdependency!(run, infile)
-        if isnothing(outfile)
-            Datasets.write_json(stdout, run)
-        else
-            outfile = joinpath(dirname(infile), outfile)
-            Datasets.to_json_file(outfile, run)
-        end
-    elseif parsed_args["open"]
-        verbose = !parsed_args["--quiet"]
-        infile = parsed_args["<input-path>"]
-        if !isfile(infile)
-            @error "File not found" infile
-            return
-        end
-        if parsed_args["--viewer"]
-            app = larvaviewer(infile)
-        else
-            kwargs = Dict{Symbol, Any}()
-            backends = parsed_args["--backends"]
-            if !isnothing(backends)
-                kwargs[:backend_directory] = backends
-            end
-            app = larvaeditor(infile; kwargs...)
-        end
-        #
-        port = parsed_args["--port"]
-        port = isnothing(port) ? 9284 : parse(Int, port)
-        server = Server(app, "0.0.0.0", port)
-        if parsed_args["--browser"]
-            JSServe.openurl("http://127.0.0.1:$(port)")
-            #display(JSServe.BrowserDisplay(), app)
-        elseif verbose
-            @info "The server is listening to http://127.0.0.1:$(port)"
-        end
-        if verbose
-            @info "Press Ctrl+D to kill the server"
-        end
-    elseif parsed_args["train"]
-        @error "Not implemented yet"
-    end
-end
-
-main()
+main();
diff --git a/src/cli.jl b/src/cli.jl
new file mode 100644
index 0000000000000000000000000000000000000000..13b6829914215ee0f0ea48396991e419f365bd0d
--- /dev/null
+++ b/src/cli.jl
@@ -0,0 +1,88 @@
+using DocOpt
+using PlanarLarvae.Datasets, PlanarLarvae.Formats
+using LarvaTagger
+using JSServe: JSServe, Server
+
+usage = """Larva Tagger.
+
+Usage:
+  larvatagger.jl import <input-path> [<output-file>] [--id=<id>] [--framerate=<fps>]
+  larvatagger.jl open <input-path> [--backends=<path>] [--port=<number>] [--quiet]
+  larvatagger.jl open <input-path> [--viewer] [--browser]
+  larvatagger.jl train <backend-path> <data-repository> [--instance=<name>]
+  larvatagger.jl -h | --help
+
+Options:
+  -h --help          Show this screen.
+  -q --quiet         Do not show instructions.
+  --id=<id>          Run ID, e.g. `date_time`.
+  --framerate=<fps>  Camera frame rate, in frames per second.
+  --backends=<path>  Path to backend repository.
+  --port=<number>    Port number the server listens to.
+  --viewer           Disable editing capabilities.
+  --browser          Automatically open a browser tab at the served location.
+  --instance=<name>  Name of the trained model.
+
+"""
+
+function main(args=ARGS)
+    parsed_args = docopt(usage, args isa String ? split(args) : args)
+    if parsed_args["import"]
+        infile = parsed_args["<input-path>"]
+        if !isfile(infile)
+            @error "File not found" infile
+            return
+        end
+        outfile = parsed_args["<output-file>"]
+        kwargs = Dict{Symbol, Any}()
+        framerate = parsed_args["--framerate"]
+        if !isnothing(framerate)
+            kwargs[:framerate] = parse(Float64, framerate)
+        end
+        file = load(infile; kwargs...)
+        runid = parsed_args["--id"]
+        run = Run(isnothing(runid) ? file.run.id : runid, Track[];
+                  file.run.attributes[:metadata]...)
+        run.attributes[:labels] = String[]
+        Datasets.pushdependency!(run, infile)
+        if isnothing(outfile)
+            Datasets.write_json(stdout, run)
+        else
+            outfile = joinpath(dirname(infile), outfile)
+            Datasets.to_json_file(outfile, run)
+        end
+    elseif parsed_args["open"]
+        verbose = !parsed_args["--quiet"]
+        infile = parsed_args["<input-path>"]
+        if !isfile(infile)
+            @error "File not found" infile
+            return
+        end
+        if parsed_args["--viewer"]
+            app = larvaviewer(infile)
+        else
+            kwargs = Dict{Symbol, Any}()
+            backends = parsed_args["--backends"]
+            if !isnothing(backends)
+                kwargs[:backend_directory] = backends
+            end
+            app = larvaeditor(infile; kwargs...)
+        end
+        #
+        port = parsed_args["--port"]
+        port = isnothing(port) ? 9284 : parse(Int, port)
+        server = Server(app, "0.0.0.0", port)
+        if parsed_args["--browser"]
+            JSServe.openurl("http://127.0.0.1:$(port)")
+            #display(JSServe.BrowserDisplay(), app)
+        elseif verbose
+            @info "The server is listening to http://127.0.0.1:$(port)"
+        end
+        if verbose
+            @info "Press Ctrl+D to kill the server"
+        end
+        return server
+    elseif parsed_args["train"]
+        @error "Not implemented yet"
+    end
+end
diff --git a/test/precompile-common.jl b/test/precompile-common.jl
new file mode 100644
index 0000000000000000000000000000000000000000..4843e0259907da6f12813418eb27203f840487d2
--- /dev/null
+++ b/test/precompile-common.jl
@@ -0,0 +1,7 @@
+datafile = joinpath(@__DIR__, "data_sample")
+ispath(datafile) || exit()
+
+projectdir = dirname(Base.active_project())
+include(joinpath(projectdir, "src/cli.jl"))
+
+srv = main(["open", datafile])
diff --git a/test/precompile-electron.jl b/test/precompile-electron.jl
new file mode 100644
index 0000000000000000000000000000000000000000..42a0cc2a6543b84a64b2762c6e2814d3983f0e28
--- /dev/null
+++ b/test/precompile-electron.jl
@@ -0,0 +1,9 @@
+using Blink
+
+include(joinpath(@__DIR__, "precompile-common.jl")) # defines `srv`
+
+win = Window(async=false)
+loadurl(win, "http://127.0.0.1:9284")
+close(win)
+
+close(srv)
diff --git a/test/precompile.jl b/test/precompile.jl
index 00f22103b040bac3057e735a3243dbeb5e81a5dd..75f5b7c71eb42106b31a4255ce65f6564f4f83bf 100644
--- a/test/precompile.jl
+++ b/test/precompile.jl
@@ -1,24 +1,7 @@
-using LarvaTagger
-using JSServe: Server
+using HTTP
 
-datafile = joinpath(@__DIR__, "data_sample")
-ispath(datafile) || exit()
+include(joinpath(@__DIR__, "precompile-common.jl")) # defines `srv`
 
-app = larvaeditor(datafile)
-srv = Server(app, "0.0.0.0", 9284)
-
-if isempty(ARGS)
-
-    using HTTP
-    HTTP.request(:GET, "http://127.0.0.1:9284")
-
-elseif ARGS == ["--electron"]
-
-    using Blink
-    win = Window(async=false)
-    loadurl(win, "http://127.0.0.1:9284")
-    close(win)
-
-end
+HTTP.request(:GET, "http://127.0.0.1:9284")
 
 close(srv)
diff --git a/test/precompile.sh b/test/precompile.sh
index 4bda55d90a36c4c16fc0a0f0333f1ea86ca4f38b..970dfa413aab5a0eda86f6151c9202fb7a618d53 100755
--- a/test/precompile.sh
+++ b/test/precompile.sh
@@ -1,27 +1,34 @@
 #!/bin/sh
 root=$(dirname $(dirname $0))
 if [ "$1" = "--shallow" ]; then
-shift
-echo "Project root: $root"
-julia --project=$root -e 'using Pkg; Pkg.add("HTTP")'
-echo "[$(date +%T)] Further precompiling..."
-julia --project=$root $root/test/precompile.jl
-echo "[$(date +%T)] Precompilation done"
-julia --project=$root -e 'using Pkg; Pkg.rm("HTTP")'
+
+client=HTTP
+julia="julia"
+script="$root/test/precompile.jl"
+packages="g++"
+
 else
-packages="unzip xvfb libgtk-3-0 libnss3 libxss1 libasound2"
+
+client=Blink
+julia="xvfb-run julia"
+script="$root/test/precompile-electron.jl"
+# dependencies on Debian Bullseye and Ubuntu Focal
+packages="unzip xvfb libgtk-3-0 libnss3 libxss1 libasound2 g++"
+
+fi
 apt-get update
 apt-get install -y $packages
+
 echo "Project root: $root"
-julia --project=$root -e 'using Pkg; Pkg.add("Blink")'
-# to find out missing dependencies:
-# electron=$(julia --project=$root -e 'using Blink; println(Blink.AtomShell.electron())')
-# echo $electron
-# $electron
-echo "[$(date +%T)] Further precompiling..."
-xvfb-run julia --project=$root $root/test/precompile.jl --electron
-echo "[$(date +%T)] Precompilation done"
-julia --project=$root -e 'using Pkg; Pkg.rm("Blink")'
+$julia --project=$root -e "
+using Pkg
+Pkg.add([\"$client\",\"PackageCompiler\"])
+using PackageCompiler
+println(\"🢃 🢃 🢃 Includes logs from coverage script. Please ignore 🢃 🢃 🢃\")
+create_sysimage(String[], sysimage_path=\"larvatagger.so\", precompile_execution_file=\"$script\")
+println(\"🢁 🢁 🢁 Includes logs from coverage script. Please ignore 🢁 🢁 🢁\")
+#Pkg.rm([\"PackageCompiler\",\"$client\"])"
+
 apt-get autoremove -y $packages
+apt-get clean
 rm -rf /var/lib/apt/lists/*
-fi