Also available here https://alex-ber.medium.com/4d861cdd366c
Note:Windows alternative for Kate is Zed. As alternative Lapce can be considered, but it lost development momentum recently.
Limitation: Here there is no description for debugging. Even running on bare metal, not in docker containers.
Basic Installation:
sudo apt update
sudo apt install konsole
sudo snap install kate --classic
Now, many LSP uses Node.js. Because Node.js breaks backward compatibility we will use mise to manage it's versions.
Mise
Installing:
curl https://mise.run | sh
Making it work automatically when terminal is opened:
# Add shims to PATH instead of activating via eval
# echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
echo 'export PATH="$HOME/.local/share/mise/shims:$PATH"' >> ~/.bashrc
# Reload bashrc
source ~/.bashrc
There is more below.
Ниже есть продолжение.No system-level utils by default
#If you want to set one, but I do not recommend
#mise use --global uv@0.11.21
#mise use --global node@20.11.0
Unset global versions
Unfortunately,
mise unset --global uv
mise unset --global node
doesn't work for whatever reason. Instead run
nano ~/.config/mise/config.toml
directly. Revert it to it's default state that should look exactly like
[settings]
# Enable variable expansion
env_shell_expand = true
[tools]
[env]
Note:: env_shell_expand is set to true for forward compatibility. By default, this line doesn't exists and current behavior is as if it is set to false. But we in future version the default value will be changed to true, so we're just preparing ourselves to this change in advance. And we will exploit true value later in our scripts anyway just now.
After editing:
# Verify the config is clean
cat ~/.config/mise/config.toml
mise cache clear
mise reshim
# Checking that shims works
mise doctor
# Check versions - shouldn't found anything
uv --version
node --version
Once in a while you can run
mise prune
to clean up space on the disk for unused tools.
Making sudo uv works
Now, if you want to run uv or node with sudo just type:
sudo -E env PATH="$PATH" uv
sudo -E env PATH="$PATH" node
Some useful mise commands
mise tool-alias ls node- here you can see LTS versions of the tool. Node supports this, uv doesn't.mise latest uv- latest version of the tool.mise ls-remote uv- list of full supported version of the tool.mise ls uv- What version of uv is used and where it is configured.mise list- list of currently installed utils.
For more advanced mise commands see my https://github.com/alex-ber/ubuntu2404-snapshots/blob/master/README-mise.md
Installing LSP Servers
First of all we will create isolated directory that will serve as LSP "control center":
sudo mkdir -p /work
sudo chmod 755 /work
sudo chown USER:USER /work
cd /work
Note:: Change USER for your real Linux user.
Important: All Git Repositories and all other thing that you want to benefit from your default miseconfig file you should strictly checkout to /work.
Than we'll use mise use. This command will create a mise.toml file where versions will be pinned.
#Note, here we're using more advance version of node than above
#It is LTS version of node at a time of writting of the story
mise use node@22
mise use uv@0.11.21
I recommend you to rename it to .mise.toml.
cd /work
mv mise.toml .mise.toml
Now a .mise.toml file has appeared in the folder.
After that, run:
mise install
Verify that the tools work locally:
uv --version
It should show 0.11.21.
npm --version
It should show 10.9.8.
About V8
The V8 engine is never installed separately. It is hard-wired inside Node.js (or inside Deno). Therefore, you cannot check the V8 version simply from the terminal. But you can ask Node.js itself which version of V8 is embedded in it! Run:
node -p "process.versions.v8"
You can also optionally verify that you doesn't have Deno installed by running:
deno --version
You should see Command 'deno' not found, did you mean: as first line at the output.
Environment variables
One of the best features of mise is that it can manage environment variables.
First of all verify that ~/.config/mise/config.toml looks like this:
[settings]
# Enable variable expansion
env_shell_expand = true
[tools]
[env]
Note:: env_shell_expand is set to true for forward compatibility. By default, this line doesn't exists and current behavior is as if it is set to false. But we in future version the default value will be changed to true, so we're just preparing ourselves to this change in advance. And we will exploit true value later in our scripts anyway just now.
Now, open /work/.mise.toml and make it look like this (insert your own data):
[tools]
node = "22"
uv = "0.11.21"
"npm:dockerfile-language-server-nodejs" = "0.15.0"
"npm:pyright" = "1.1.410"
"npm:typescript" = "5.5.2"
"npm:typescript-language-server" = "4.3.3"
"npm:vscode-langservers-extracted" = "4.10.0"
"npm:yaml-language-server" = "1.23.0"
"npm:eslint" = "10.5.0"
#ruff = "0.15.17"
[env]
# Your Git settings
GIT_TOKEN = "ghp_KR..."
GIT_USER = "alex-ber"
GIT_CONFIG_COUNT = "2"
GIT_CONFIG_KEY_0 = "url.@github.com/.insteadOf">https://${GIT_USER}:${GIT_TOKEN}@github.com/.insteadOf"
GIT_CONFIG_VALUE_0 = "https://github.com/"
GIT_CONFIG_KEY_1 = "url.@gist.github.com/.insteadOf">https://${GIT_USER}:${GIT_TOKEN}@gist.github.com/.insteadOf"
GIT_CONFIG_VALUE_1 = "https://gist.github.com/"
# Adding local binary paths to PATH so that Kate can see them
_.path = [
"./node_modules/.bin",
"./.venv/bin"
]
Note:: Change GIT_TOKEN and GIT_USER to your specific values.
Of course, this is only one of many of possible ways to configure GIT configuration value for Kate. I found this very straightforward. Note also that we're using variable expansion here.
WARNING:
1. You're storeing you GIT_TOKEN as plain text. On another hand attacher should know where to search GIT_TOKEN or it should scan whole file system for "GIT_TOKEN" key. Using /work to store is not standard in the industry. :-) I hope, yet. :-)
2. When Kate make git pushit passes your GIT_TOKEN as plain text. If this is unexectable security risk for you find another way to pass credenetial to GIT there are plenty documentaiton on the Internet.
Execute:
mise install
If you get
mise ERROR error parsing config file: /work/.mise.toml
mise ERROR Config files in /work/.mise.toml are not trusted.
Trust them with `mise trust`. See https://mise.en.dev/cli/trust.html for more information.
mise ERROR Version: 2026.6.6 linux-x64 (2026-06-13)
mise ERROR Run with --verbose or MISE_VERBOSE=1 for more information
The error Config files ... are not trusted is an excellent and very important built-in security system of mise.
Why it occurred:
Your file contains an [env] section where you define tokens and, most critically, modify paths (_.path). Imagine you download someone else's repository containing a malicious mise.toml that replaces system commands. To prevent this, mise blocks the execution of such settings by default until you explicitly confirm that you trust this file.
How to fix it:
Since this is your own file in your working directory /work, you need to do exactly what mise asks.
While in the /work directory, run:
mise trust
This command will add the /work/.mise.toml file to your system's global allowlist.
In order to verify that environment variables are set properly run
mise env
or
mise env | grep GIT_CONFIG_KEY_0
Tips: You can edit .mise.toml file and run mise prune command it will remove unused tools. If you edit .mise.toml manually you must run mise install command in order to execute the configuration changes, see below for more details.
I pinned versions that was latest at the moment of installation. You can use @latest if you prefer. There is also @lts label, but see caveats below.
Nuance: latest vs lts
In mise, you can use keywords:
node@lts- installs the latest Long-Term Support version (currently 22, but this will change over time). This is the safest choice for production work.node@latest- the absolute newest version (could be 23 or 24, which are still being "road-tested").
My advice: Use mise use node@22, as it currently offers the perfect balance between new features and stability.
Execute:
mise doctor
You should see something like this:
version: 2026.6.6 linux-x64 (2026-06-13)
activated: yes
shims_on_path: yes
self_update_available: yes
build_info:
Target: x86_64-unknown-linux-gnu
Features: openssl, rustls-native-roots, self_update
Built: Sat, 13 Jun 2026 11:27:15 +0000
Rust Version: rustc 1.94.0 (4a4ef493e 2026-03-02)
Profile: release
shell:
/bin/bash
GNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
aqua:
baked in registry: aquaproj/aqua-registry@93cd8ecfeef813610199913db392d0e8f8dc0c27
baked in registry tools: 2218
dirs:
cache: ~/.cache/mise
config: ~/.config/mise
data: ~/.local/share/mise
shims: ~/.local/share/mise/shims
state: ~/.local/state/mise
config_files:
~/.config/mise/config.toml
/work/.mise.toml
env_files:
(none)
ignored_config_files: (none)
backends:
aqua
asdf
cargo
conda
core
dotnet
forgejo
gem
github
gitlab
go
npm
pipx
spm
http
s3
ubi
vfox
plugins:
toolset:
aqua:astral-sh/uv@0.11.21
core:node@22.22.3
npm:dockerfile-language-server-nodejs@0.15.0
npm:pyright@1.1.410
npm:typescript-language-server@4.3.3
npm:typescript@5.5.2
npm:vscode-langservers-extracted@4.10.0
npm:yaml-language-server@1.23.0
path:
/work/node_modules/.bin
/work/.venv/bin
~/.local/share/mise/installs/node/22/bin
~/.local/share/mise/installs/uv/0.11.21/uv-x86_64-unknown-linux-musl
~/.local/share/mise/installs/npm-dockerfile-language-server-nodejs/0.15.0/bin
~/.local/share/mise/installs/npm-pyright/1.1.410/bin
~/.local/share/mise/installs/npm-typescript/5.5.2/bin
~/.local/share/mise/installs/npm-typescript-language-server/4.3.3/bin
~/.local/share/mise/installs/npm-vscode-langservers-extracted/4.10.0/bin
~/.local/share/mise/installs/npm-yaml-language-server/1.23.0/bin
~/.local/share/mise/shims
~/.local/bin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
/usr/games
/usr/local/games
/snap/bin
/snap/bin
~/go/bin
env_vars:
MISE_SHELL=bash
settings:
env_shell_expand true ~/.config/mise/config.toml
No problems found
Whether you are building Kate, a custom Neovim configuration, supercharging Emacs, or just wanting to understand what happens under the hood of VS Code, knowing your toolchain is essential.
Let's break down a modern developer's stack by looking at eight fundamental tools, exactly in the order you would build them into a polyglot setup, and explore what they bring to your editing experience.
The Power of Modularity
The true beauty of the modern development ecosystem is modularity. By understanding what each of these packages does - from the strict type checking of Pyright and TypeScript to the blazing-fast formatting of Ruff - you can mix, match, and tailor your environment to be exactly as responsive and rigorous as you need it to be.
1. Dockerfile Language Server Node.js
(npm:dockerfile-language-server-nodejs)
When you are writing a Dockerfile, you want to ensure your instructions are valid and follow best practices. This server is powered by Node.js and hooks directly into your editor to provide auto-completion for Docker instructions like RUN, COPY, or ENTRYPOINT. It also offers on-the-fly syntax validation, hover documentation, and structural linting, ensuring your container builds won't fail due to simple typos or missing arguments.
2. Pyright
(npm:pyright)
Developed by Microsoft, Pyright is an incredibly fast, statically typed checker and language server for Python. Unlike traditional Python linters, Pyright focuses heavily on type inference and PEP 484 type hinting. It runs instantly as you type, catching type mismatches, missing imports, and undefined variables. It is an absolute favorite for large-scale Python codebases that require strict type safety and intelligent refactoring.
3. TypeScript
(npm:typescript)
While technically a programming language and a compiler rather than a standalone LSP, this core package is the brain behind any JavaScript or TypeScript development environment. It contains the TypeScript Language Service (tsserver), which parses your code, understands your project's tsconfig.json, and computes all the rich type information, completions, and diagnostics. Without this base package, no modern TypeScript tooling could function.
4. TypeScript Language Server
(npm:typescript-language-server)
Because the core tsserver does not natively speak the standardized Language Server Protocol (LSP), this wrapper was created to bridge the gap. It takes the deep intelligence provided by the base TypeScript package and translates it into standard LSP JSON-RPC messages. If you are using an editor outside of VS Code - this is the exact piece of software that grants you official, VS Code-level TypeScript and JavaScript support.
5. VSCode Langservers Extracted
(npm:vscode-langservers-extracted)
VS Code has brilliant built-in language support for front-end web technologies, but those servers are normally tightly integrated into the editor itself. This package extracts those official servers - specifically for HTML, CSS, SCSS, LESS, and JSON - so they can be used in any LSP-compatible editor. By installing this, you get Microsoft's official auto-completion, schema validation, and linting for standard web languages anywhere you choose to code.
6. YAML Language Server
(npm:yaml-language-server)
Writing YAML without assistance can lead to frustrating indentation errors and silent configuration bugs. Developed by Red Hat, this language server transforms the YAML writing experience. Its most powerful feature is the ability to map your files to JSON Schemas (for example, mapping to Kubernetes manifests, GitHub Actions, or Docker Compose files). This means you get intelligent auto-completion and validation tailored exactly to the specific DevOps tool you are configuring.
7. ESLint
(npm:eslint)
ESLint is the industry standard for identifying and reporting on patterns in JavaScript and TypeScript code. While it is fundamentally a linter and not an LSP on its own, modern development environments run ESLint as a background service (often wrapped in its own LSP integration) to provide real-time squiggly lines and auto-fix capabilities upon saving. It ensures your code adheres strictly to your team's style guide and catches logical errors before they ever reach production.
Note: I'm not using this LSP Server, but rely on Kate's built-in plugin, it works more reliability.
8. Ruff
(#ruff)
Ruff is the new heavyweight champion of Python tooling. Written in Rust, it is an astonishingly fast Python linter and formatter that replaces a dozen legacy tools like Flake8, isort, and Black. Many developers integrate Ruff directly into their editor via its own language server (such as ruff-lsp or the newer native ruff server) to get instantaneous feedback. It flags unused imports, stylistic errors, and code smells, fixing them in milliseconds without ever slowing down your editor.
Note: It is turned off in the configuration provided above, because personally I do not see any value of it. I started from Python 2 without any type hints and I have bad experiences with similar tools in Java scosystem. And Java is statically typed. IMHO such tools can be never be perfect.
Especially in such dynamic languages like Pyhton where you have eval(discoursage I know, but still in use), locals(), metaclassess, type hints are optional, if you're using duck typing without Protocol Ruff can't help you. Basically, in typicall code-base there too many "too dynamic" code that Ruff will be able to handle statically. You can treat it as my personal heuristic, but actually Turing halting problem and Gödel incompleteness theorems mathimatically proves that such tools can be never be perfect.
Set local versions (for a specific project)
Navigate to your project folder and run:
cd /path/to/your/project
mise use node@22
mise use uv@0.11.21
These commands won't download anything (since the versions are already downloaded globally), they will simply create a mise.toml file in the folder that locks these versions to the project. Don't forget to commit it to Git.
Manual editing of mise.toml
This is a completely valid and standard scenario. Many people do exactly this: edit the file by hand.
Just execute:
mise install
In 99% cases this is enough. If you manually added uv = "0.11.21" to mise.toml, running mise install will read this file, download the required version of uv, and make it available in the current directory. Nothing else is needed for the installation itself.
Note: If you there are problems (e.g., if your internet disconnects while downloading uv, the archive becomes corrupted, and mise install gives an unpacking error) you can use mise cache clear. Under normal workflow, you don't need to use this command.
Since we're working via shims the mise install command automatically creates and updates shims for new tools, so in 99% cases mise reshimis not needed. Manually calling mise reshim may only be needed as a "first aid" if you installed a tool but get command not found when typing its command in the terminal (e.g., this sometimes happens if you install a global CLI package via npm -g or pip, and mise hasn't had a chance to rebuild shims for the new binaries).
Once in a while you can run mise prune to clean up space on the disk for unused tools.
Note:
config_files:
~/.config/mise/config.toml
/work/alexsmail-dns-fix/mise.toml
/work/alexsmail-dns-fix/mise.toml was added to config_files and ~/.local/share/mise/installs/uv/0.11.21/uv-x86_64-unknown-linux-musl was added to the path.
Create a startup script (Kate Wrapper)
To make Kate pick up the entire mise environment, the simplest approach is to launch it via the mise exec command.
Create the file /work/mise-app-launcher.sh:
Deprecated version:
#!/bin/bash
## Navigate to /work so that mise finds the correct .mise.toml
cd /work
# Use the absolute path to the snap version of Kate to avoid conflicts
exec ~/.local/bin/mise exec -- /snap/bin/kate -b "$@"
Robust version:
#!/bin/bash
# Check that at least one argument (executable) has been passed
if [ $# -lt 1 ]; then
echo "Usage: $0 <executable_path> [args...]"
exit 1
fi
# 1. Take the first argument as the command to run and shift the $@ array
APP_EXEC="$1"
shift
# Default base directory
TARGET_DIR="/work"
# Function for URL decoding (turns %20 into spaces, decodes Cyrillic, etc.)
urldecode() {
local url_encoded="${1//+/ }"
printf '%b' "${url_encoded//%/\\x}"
}
# Look for the first argument that is an existing file or directory
for arg in "$@"; do
# 1. Strip protocol prefix if present
clean_arg="${arg#file://}"
# 2. Decode the URL if KDE/GNOME/Windows passed an escaped path
if [[ "$clean_arg" == *%* ]]; then
clean_arg=$(urldecode "$clean_arg")
fi
# 3. WSL2 AND WINDOWS PATH SUPPORT
if [[ "$clean_arg" =~ ^/?([a-zA-Z]:[\\/].*) ]]; then
clean_arg="${BASH_REMATCH[1]}" # Remove leading slash, keep "C:/..."
# If we are in WSL, convert Windows path to Linux path (/mnt/c/...)
if command -v wslpath >/dev/null 2>&1; then
clean_arg=$(wslpath -u "$clean_arg" 2>/dev/null || echo "$clean_arg")
fi
fi
# 4. Check if the directory or file exists (with protection against filenames starting with a dash)
if [ -d -- "$clean_arg" ]; then
TARGET_DIR="$clean_arg"
break
elif [ -f -- "$clean_arg" ]; then
TARGET_DIR=$(dirname -- "$clean_arg")
break
fi
done
# 5. TOMCAT MAGIC: Convert the path to absolute and resolve all symlinks.
if command -v realpath >/dev/null 2>&1; then
TARGET_DIR=$(realpath "$TARGET_DIR")
else
# Reliable POSIX fallback for systems without the realpath utility
TARGET_DIR=$(cd "$TARGET_DIR" 2>/dev/null && pwd -P)
fi
# 6. Change to the resolved directory and execute the requested app via mise
cd "$TARGET_DIR" || exit 1
# Use APP_EXEC, while $@ now contains only flags and files (since we performed shift)
exec ~/.local/bin/mise exec -- "$APP_EXEC" "$@"
Make it executable:
chmod +x /work/mise-app-launcher.sh
Update the shortcut (.desktop file)
Now, we will create .desktop file on your desktop. These instructions are GNOME specific.
Run
find /usr/share/applications -iname '*kate*.desktop'
or
find /var/lib/snapd/desktop/applications/ -iname '*kate*.desktop'
Typically second command will return something like this:
/var/lib/snapd/desktop/applications/kate_kate.desktop
Now execute:
cp /var/lib/snapd/desktop/applications/kate_kate.desktop ~/Desktop/
chmod +x ~/Desktop/kate_kate.desktop
gio set ~/Desktop/kate_kate.desktop metadata::trusted true
Now modify the Exec line in your .desktop file. For example:
nano ~/Desktop/kate_kate.desktop
Change the Exec line:
Exec=/work/mise-app-launcher.sh /snap/bin/kate -b %U
Plugins
These are plugins that I'm using:
- Document Switcher (was default)
- Documents Tree (was default)
- ESLInt - note I'm not using LSP Server (see above), but rely on built-in plugin, it works more reliability.
- External tools (was default)
- LSP Client (see below)
- Project plugin (was default) - very convenient minimalist Git client. I have provided above one version of passing auth params via env/export.
- Search & replace (was default)
- Symbol viewer
- Terminal - built-in terminal. Personally, I prefer to us regular terminal, but it is handy for quick experimentation.
- Text Filter
LSP Client's User Server Settings
Now click on .desktop shortcut. Kate should opened up.
Go to Settings->Configure Kate->LSP Client. Go to User Server Settings tab. Replace the content with following:
{
"servers": {
"python": {
"command": ["pyright-langserver", "--stdio"],
"rootIndicationFileNames": ["pyproject.toml", "mise.toml", ".git"],
"highlightingModeRegex": "^Python$",
"documentLanguageId": "python"
},
"javascript": {
"command": ["typescript-language-server", "--stdio"],
"rootIndicationFileNames": ["tsconfig.json", "package.json"],
"highlightingModeRegex": "^JavaScript.*$",
"documentLanguageId": "javascript"
},
"typescript": {
"use": "javascript",
"highlightingModeRegex": "^TypeScript.*$",
"documentLanguageId": "typescript"
},
"json": {
"command": ["vscode-json-language-server", "--stdio"],
"highlightingModeRegex": "^JSON$",
"documentLanguageId": "json"
},
"html": {
"command": ["vscode-html-language-server", "--stdio"],
"highlightingModeRegex": "^HTML$",
"documentLanguageId": "html"
},
"css": {
"command": ["vscode-css-language-server", "--stdio"],
"highlightingModeRegex": "^CSS$",
"documentLanguageId": "css"
},
"yaml": {
"command": ["yaml-language-server", "--stdio"],
"highlightingModeRegex": "^YAML$",
"documentLanguageId": "yaml",
"initializationOptions": {
"settings": {
"yaml": {
"schemaStore": {
"enable": true
}
}
}
}
},
"dockerfile": {
"command": ["docker-langserver", "--stdio"],
"highlightingModeRegex": "^Dockerfile$",
"documentLanguageId": "dockerfile"
},
"markdown": {
"highlightingModeRegex": "DisabledMode"
}
}
}
Note:: When you're openning yaml file, say docker-compose.yml you can see error in the logs. If syntax highlighting and autocomplition works, you can safely ignore this error. It is harmless.
How it works and why it's great:
- Clean system: If you open a regular terminal and type uv or node, the system won't find them (just as you wanted).
- Automation: When you launch Kate via the script, mise exec looks at .mise.toml, instantly activates the correct versions of Node.js and uv, passes through GIT_TOKEN, and adds the node_modules/.bin and .venv/bin folders to PATH.
- LSP in Kate: When the LSP plugin activates in Kate, it looks for the command (e.g., typescript-language-server). Since the startup script added the local project folder to PATH, Kate successfully finds and launches the server.
Verification:
- Launch Kate via the new shortcut.
- Open the built-in terminal in Kate (F4).
- Type echo $GIT_TOKEN - you should see your token.
Code Index
Note:: I did
sudo apt install universal-ctags
but it is not used in Kate.
1. Configuring the Project Plugin
The foundation of a smooth IDE experience in Kate is the Project Plugin. This step is critical because LSP servers work significantly better when they know the exact "root" of your codebase.
Go to Settings → Configure Kate → Plugins.
Ensure that the Project Plugin is enabled.
When you open a folder as a project (via Project → Open Folder in the top menu), Kate automatically scans for version control directories (such as .git). Once detected, it passes this exact root path to the LSP server as the rootUri.
Note: Always open your working directory as a project via the Project menu rather than opening files individually. This simple habit guarantees that your LSP server will correctly resolve all local imports, modules, and dependencies across your entire workspace.
2. Enabling the Symbol Outline
To get a clear visual representation of your code architecture, we can utilize the structural data already provided by the LSP.
In the Plugins menu, verify that both the LSP Client and Symbol Viewer plugins are enabled.
Navigate to View → Tool Views in the main menu and check Show Symbol Outline (or Symbol Viewer).
If the panel appears empty at first, you can force a refresh by navigating to LSP Client → Restart All LSP Servers.
A sidebar or a bottom tab will immediately appear. It automatically builds a dynamic, clickable tree of all functions, classes, and variables based on the real-time data parsed by your Python or TypeScript language servers.
3. Testing the Magic
Finally, let us verify that your smart code navigation is fully operational.
Open your project folder using the Project menu.
At the bottom of the Kate window, click on the LSP Client panel and switch to the Log tab. You should see a confirmation message indicating that your specific servers (e.g., pyright-langserver) have started successfully and attached to your project root.
Open any source file, right-click on a custom function or class name, and select Go to Definition (or use the default Ctrl + Click / F12 shortcut).
Kate will instantly navigate you to the exact file and line where that symbol was declared, leveraging your local, fully isolated LSP environment.
Extras
Because Ubuntu doesn't provide built in base docker repositroy that is frozen in time I built one myself. This is https://github.com/alex-ber/ubuntu2404-snapshots project. You can pull latest golden/frozen version by
docker pull alexberkovich/ubuntu2404-snapshot:latest
or provide specific date for snapshot (if it exists in Docker hub).
Usually, it will be best on latest ubuntu:24.04 docker images with some extra OS-level dependecies installed and pinned.
For example, it has curl installed with specific version. No matter what security vulnaribility will be found if you're using my base image you will have this exact version.
It provides determenism and reproducability. Fixing, security whole often breaks your docker images. And it "just happen" in undefined time.
If there is serious security patch you will know about it and you should move to another snapshot (that I will provide) when you're ready to deal with broken docker image.
It also has get-image-hash and get-pkg-version abilities that can be utilized via docker-compose.yml. It also has update-uv-lock that is template for Python project's that want to utilize uv.
get-image-hash
The engine behind this entire workflow is a dedicated Docker Compose service named get-image-hash. It is designed to retrieve the latest immutable SHA256 digest for absolutely ANY Docker image.
Here is how you can utilize the daemon in your day-to-day workflow:
- Default Execution:
Runningdocker compose run --rm get-image-hashwithout any arguments defaults to targeting ghcr.io/astral-sh/uv:latest. - Custom Target Resolution:
You can append any image name to fetch its specific hash. For example, runningdocker compose run --rm get-image-hash ubuntu:24.04will instantly return the exact pointer for that Ubuntu release. - Debugging Mode:
If the daemon is failing or you need to inspect the container environment, you can override the default execution by dropping into a shell. Simply rundocker compose run --rm --entrypoint sh get-image-hash.
By standardizing how you retrieve and lock down your image hashes, you eliminate "it works on my machine" issues and create a foundation for truly bulletproof infrastructure.
The core concept starts with fetching the exact SHA256 digest of a specific tool before building. For example, if you want to use version 0.11.21 of the Astral uv tool, you would execute the daemon like this:
docker compose run --rm get-image-hash ghcr.io/astral-sh/uv:0.11.21
This query returns an absolute, immutable hash. Armed with this string, you can replace vulnerable floating tags in your Dockerfile with a bulletproof copy command:
COPY --from=ghcr.io/astral-sh/uv@sha256:abcd… /uv /uvx /bin/
By doing this, your container build is guaranteed to pull the exact same binaries every single time, regardless of what the maintainers publish in the future.
Managing Host Tooling with Mise
While Docker handles containerized dependencies, you also need to manage local versions predictably. If you use the mise tool manager, you can easily query version availability.
To check if a specific tool (like Node.js) natively supports Long Term Support (LTS) aliases, you can run:
mise tool-alias ls node
If your desired tool does not show up, you can search the remote registry:
mise ls-remote uv
Or, to simply grab the most current version number (which, in our example, returns 0.11.21), you can use:
mise latest uv
Note: For more advanced mise commands see my https://github.com/alex-ber/ubuntu2404-snapshots/blob/master/README-mise.md
The Reproducibility Warning: Forcing LTS
Sometimes, a tool lacks an official LTS policy. You might be tempted to force one to keep your local environment consistently updated within a minor version range. You can do this by setting a custom alias:
mise tool-alias set uv lts 0.11
Warning: While this is convenient for local development, doing this entirely breaks strict reproducibility. Because "0.11" is a moving target, your tools will shift beneath your feet as new patches are released.
get-pkg-version
Before you can pin a package, you need to know exactly which operating system environment you are targeting. Your first step when setting up your builds should always be to verify your base system. You can do this by running:
cat /etc/os-release
This ensures that the package versions you are about to resolve actually match the distribution (like Ubuntu 24.04) you intend to use in your final production Dockerfile.
This get-pkg-version queries the official Ubuntu repositories and returns the exact version string required for hard-pinning. Here is how you can use it in various scenarios:
- Default Execution:
If you run the service without passing any arguments, it is configured to resolve the ca-certificates package by default. You trigger this by running:
docker compose run --rm get-pkg-version - Custom Target Resolution:
You are not limited to the default. You can append any package names you need to resolve multiple dependencies at once. For example, to find the exact versions for the Nano editor and Git, you would run:
docker compose run --rm get-pkg-version nano git - Debugging Mode:
If the package resolver fails, or if you need to manually explore the APT repositories using tools like apt-cache policy, you can override the entrypoint and drop straight into a Bash shell:
docker compose run --rm --entrypoint bash get-pkg-version
Under the hood, the get-pkg-version service needs to run inside a container that mirrors your production base image.
When configuring this service, you face a strategic choice regarding the image tag. The primary goal of this daemon is to discover what packages are currently available in the repositories before you permanently lock the exact package versions and image SHA in your main Dockerfile.
Because of this discovery phase, developers will often use a floating tag (like ubuntu:24.04) to query the most up-to-date repository state. However, in our specific configuration, we lock the resolver itself to a highly specific, immutable digest:
image: ubuntu@sha256:786a8b558f7be160c6c8c4a54f9a57274f3b4fb1491cf65146521ae77ff1dc54
By using this locked digest instead of the floating tag, you ensure that your Package Version Resolver queries the exact same snapshot of the Ubuntu repositories every single time you run it, guaranteeing total predictability during your dependency resolution phase.
update-uv-lock
Note:: The configuration discussed below is a conceptual template and not an executable utility directly runnable from https://github.com/alex-ber/ubuntu2404-snapshots the repository. We will break down the actual, practical usage below.
This utility is specifically designed to regenerate a uv.lock file, entirely without installing the Astral uv package on your local computer.
The goal of this service is to run interactively, update your dependencies, and then vanish. Here is how you trigger this ephemeral daemon:
- Standard Execution: The exact, full command to run this specific tool service is
docker compose run --rm update-uv-lock - Debugging Mode: If the lock generation fails and you need to inspect the container from the inside, you can override the execution and drop into a shell by running
docker compose run --rm --entrypoint bash update-uv-lock
To make this utiliy work seamlessly with your local filesystem, the service configuration for update-uv-lock must be precisely tuned.
Image and Working Directory
The service boots up using a lightweight Python image. Currently, it is pinned to
ghcr.io/astral-sh/uv:python3.13-bookworm-slim
Note: It has hard coded dependeny on your python version.
The Hardware Bridge (Identity Mapping)
When a Docker container creates a file, it defaults to the root user. If Docker generates your lock file, you won't be able to edit or delete it on your host machine without using sudo.
To avoid permission decoherence between the Host OS and the Docker runtime (especially on Linux environments), you must map your bare-metal User ID and Group ID into the container. This guarantees that files created by the runtime belong to you, preventing root-owned pollution on the host.
Initialize the local environment bridge:
cp env.example .env
Inject your bare-metal host IDs (Linux/Ubuntu):
echo "HOST_UID=$(id -u)" >> .env
echo "HOST_GID=$(id -g)" >> .env
To prevent permission decoherence, we use an identity mapping bridge:
user: "${HOST_UID:-1000}:${HOST_GID:-1000}"
{HOST_GID:-1000}"
This crucial line ensures that the newly generated uv.lock file belongs exactly to your local user account, keeping your file permissions intact.
See https://github.com/alex-ber/ubuntu2404-snapshots/blob/master/README.md for more details about .env and HOST_UID and HOST_GID.
The utility executes its single purpose:
command: ["uv", "lock"]
Test case1: JavaScript/TypeScript test project
This https://github.com/alex-ber/js-hello-world project was built specifically to test integration of Kate and LSP Servers. It is typicall hello world project and is not expected to be maintained. You can see it as reference example that can be outdated at some point.
Test case2: Python real project
This https://github.com/alex-ber/alexsmail-dns-fix project was written to solve some very specific problem.
This is mainained reference project that did the real job once and I plan to keep it up to date as template despite the fact that it was one time job I do maintain this project as template project for my another Python project.
So, when you will run it, it will fail with error. But this is expected behaviour.
It's docker-compose.yml contain update-uv-lock utility that is exact copy from https://github.com/alex-ber/ubuntu2404-snapshots/blob/master/docker-compose.yml But basic image doesn't have uv(or python) installed and this project does have. So here, update-uv-lock utility will actually work.
If you have project that has another Python version you can easily modify image tag.
Note: You can read https://github.com/alex-ber/ubuntu2404-snapshots/blob/master/README-docker-compose.md for some explanation of how docker-compose.yml utilitys (the official name is actually service, but I deliberetely avoid to use this term up untill this point to not overcomplicate things).
No comments:
Post a Comment