Mohammed Chami
.NET Developer | Content Creator
Mohammed Chami
.NET Developer | Content Creator

Publishing and deploying Avalonia UI applications on Linux requires careful consideration of deployment methods, optimization strategies, and system integration. This comprehensive guide covers everything from Native AOT compilation to desktop integration.
Recommended Order of Preference:
you can also check the documentation
Native AOT provides the best combination of performance, startup time, and distribution size for Avalonia applications.
Add these properties to your .csproj file:
<PropertyGroup>
<PublishAot>true</PublishAot>
<!-- Native AOT specific settings -->
<BuiltInComInteropSupport>false</BuiltInComInteropSupport>
<TrimMode>link</TrimMode>
<!-- Optional optimizations -->
<InvariantGlobalization>true</InvariantGlobalization>
<DebuggerSupport>false</DebuggerSupport>
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
<EventSourceSupport>false</EventSourceSupport>
</PropertyGroup>
<ItemGroup>
<!-- Preserve essential Avalonia assemblies -->
<TrimmerRootAssembly Include="Avalonia.Themes.Fluent" />
<TrimmerRootAssembly Include="Avalonia.Themes.Default" />
<TrimmerRootAssembly Include="Avalonia.Controls" />
</ItemGroup>
Ensure your XAML is AOT-ready:
<!-- Enable compiled bindings in your UserControls and Windows -->
<UserControl x:Class="YourApp.Views.MainView"
x:CompileBindings="True"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Use static resources instead of dynamic where possible -->
<UserControl.Resources>
<SolidColorBrush x:Key="MyBrush" Color="Blue" />
</UserControl.Resources>
</UserControl>
dotnet publish -r linux-x64 -c Release
dotnet publish -r linux-x64 --self-contained -c Release -p:PublishSingleFile=true -p:PublishTrimmed=true
Pros:
Cons:
dotnet publish -r linux-x64 -c Release
Pros:
Cons:
Problem: JSON serialization often breaks with trimming and AOT.
Solution 1: Use Source Generators (Recommended for AOT)
using System.Text.Json.Serialization;
[JsonSerializable(typeof(List<TodoTask>))]
[JsonSerializable(typeof(TodoTask))]
[JsonSerializable(typeof(AppSettings))]
public partial class AppJsonContext : JsonSerializerContext
{
}
// Usage in your code:
public class DataService
{
public void SaveTasks(List<TodoTask> tasks)
{
var json = JsonSerializer.Serialize(tasks, AppJsonContext.Default.ListTodoTask);
File.WriteAllText(GetDataPath(), json);
}
public List<TodoTask> LoadTasks()
{
var json = File.ReadAllText(GetDataPath());
return JsonSerializer.Deserialize(json, AppJsonContext.Default.ListTodoTask) ?? new();
}
}
Solution 2: Preserve Types in Trimmer
Create TrimmerRoots.xml:
<linker>
<assembly fullname="YourAppName">
<type fullname="YourAppName.Models.TodoTask" preserve="all" />
<type fullname="YourAppName.Models.AppSettings" preserve="all" />
<type fullname="YourAppName.ViewModels*" preserve="all" />
</assembly>
</linker>
Add to .csproj:
<ItemGroup>
<TrimmerRootDescriptor Include="TrimmerRoots.xml" />
</ItemGroup>
For MVVM applications using dependency injection:
<ItemGroup>
<!-- Preserve your ViewModels -->
<TrimmerRootAssembly Include="YourApp.ViewModels" />
</ItemGroup>
Or in TrimmerRoots.xml:
<linker>
<assembly fullname="YourApp">
<namespace fullname="YourApp.ViewModels" preserve="all" />
<namespace fullname="YourApp.Services" preserve="all" />
</assembly>
</linker>
Problem: Assembly.GetExecutingAssembly().Location returns empty string with AOT.
Solution: Use proper cross-platform paths:
public static class AppPaths
{
private static readonly string AppName = "YourAppName";
public static string GetAppDataDirectory()
{
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var appDataDir = Path.Combine(homeDir, $".{AppName}");
Directory.CreateDirectory(appDataDir);
return appDataDir;
}
public static string GetConfigFilePath()
{
return Path.Combine(GetAppDataDirectory(), "config.json");
}
public static string GetDataFilePath()
{
return Path.Combine(GetAppDataDirectory(), "data.json");
}
}
After AOT publishing, you’ll typically have:
The output is much cleaner than traditional .NET deployments.
# Navigate to publish directory
cd ./bin/Release/net8.0/linux-x64/publish/
# Create installation directory
sudo mkdir -p /opt/YourAppName
# Copy all files (usually just the executable)
sudo cp -r ./* /opt/YourAppName/
# Make executable
sudo chmod +x /opt/YourAppName/YourAppName
# Create symlink for command line access
sudo ln -sf /opt/YourAppName/YourAppName /usr/local/bin/YourAppName
# Create local installation directory
mkdir -p ~/.local/opt/YourAppName
mkdir -p ~/.local/bin
# Copy files
cp -r ./bin/Release/net8.0/linux-x64/publish/* ~/.local/opt/YourAppName/
# Make executable
chmod +x ~/.local/opt/YourAppName/YourAppName
# Create symlink
ln -sf ~/.local/opt/YourAppName/YourAppName ~/.local/bin/YourAppName
# Ensure ~/.local/bin is in PATH
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
sudo tee /usr/share/applications/YourAppName.desktop > /dev/null << EOF
[Desktop Entry]
Name=Your App Display Name
Comment=Brief description of your app
Exec=/opt/YourAppName/YourAppName
Icon=utilities-text-editor
Type=Application
Categories=Utility;Office;
Keywords=productivity;task;management;
StartupNotify=true
Terminal=false
EOF
# Update desktop database
sudo update-desktop-database /usr/share/applications/
mkdir -p ~/.local/share/applications/
cat > ~/.local/share/applications/YourAppName.desktop << EOF
[Desktop Entry]
Name=Your App Display Name
Comment=Brief description of your app
Exec=$HOME/.local/opt/YourAppName/YourAppName
Icon=utilities-text-editor
Type=Application
Categories=Utility;Office;
Keywords=productivity;task;management;
StartupNotify=true
Terminal=false
EOF
update-desktop-database ~/.local/share/applications/
To use a custom icon:
# System-widesudo cp your-icon.png /usr/share/pixmaps/yourapp.png# User-onlymkdir -p ~/.local/share/icons/hicolor/256x256/apps/cp your-icon.png ~/.local/share/icons/hicolor/256x256/apps/yourapp.pngIcon=yourapp# or full path: Icon=/usr/share/pixmaps/yourapp.pngHere’s a comprehensive script that handles Native AOT building and installation:
#!/bin/bash
set -e
# Configuration
APP_NAME="YourAppName"
DISPLAY_NAME="Your App Display Name"
DESCRIPTION="Brief description of your app"
VERSION="1.0.0"
INSTALL_TYPE="${1:-system}" # system or user
# Paths
if [ "$INSTALL_TYPE" = "user" ]; then
INSTALL_DIR="$HOME/.local/opt/$APP_NAME"
BIN_LINK="$HOME/.local/bin/$APP_NAME"
DESKTOP_FILE="$HOME/.local/share/applications/$APP_NAME.desktop"
ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
UPDATE_CMD="update-desktop-database ~/.local/share/applications/"
else
INSTALL_DIR="/opt/$APP_NAME"
BIN_LINK="/usr/local/bin/$APP_NAME"
DESKTOP_FILE="/usr/share/applications/$APP_NAME.desktop"
ICON_DIR="/usr/share/pixmaps"
UPDATE_CMD="sudo update-desktop-database /usr/share/applications/"
SUDO="sudo"
fi
echo "Building $DISPLAY_NAME with Native AOT..."
dotnet publish -r linux-x64 -c Release
# Verify build
PUBLISH_DIR="./bin/Release/net8.0/linux-x64/publish"
if [ ! -f "$PUBLISH_DIR/$APP_NAME" ]; then
echo "Error: Build failed or executable not found"
exit 1
fi
echo "Installing $DISPLAY_NAME ($INSTALL_TYPE installation)..."
# Create directories
if [ "$INSTALL_TYPE" = "user" ]; then
mkdir -p "$INSTALL_DIR" "$HOME/.local/bin" "$HOME/.local/share/applications" "$ICON_DIR"
else
$SUDO mkdir -p "$INSTALL_DIR"
fi
# Copy files
if [ "$INSTALL_TYPE" = "user" ]; then
cp -r $PUBLISH_DIR/* "$INSTALL_DIR/"
chmod +x "$INSTALL_DIR/$APP_NAME"
else
$SUDO cp -r $PUBLISH_DIR/* "$INSTALL_DIR/"
$SUDO chmod +x "$INSTALL_DIR/$APP_NAME"
fi
# Create symlink
if [ "$INSTALL_TYPE" = "user" ]; then
ln -sf "$INSTALL_DIR/$APP_NAME" "$BIN_LINK"
else
$SUDO ln -sf "$INSTALL_DIR/$APP_NAME" "$BIN_LINK"
fi
# Install icon if exists
if [ -f "icon.png" ]; then
if [ "$INSTALL_TYPE" = "user" ]; then
cp icon.png "$ICON_DIR/$APP_NAME.png"
ICON_NAME="$APP_NAME"
else
$SUDO cp icon.png "$ICON_DIR/$APP_NAME.png"
ICON_NAME="$APP_NAME"
fi
else
ICON_NAME="utilities-text-editor"
fi
# Create desktop entry
DESKTOP_CONTENT="[Desktop Entry]
Name=$DISPLAY_NAME
Comment=$DESCRIPTION
Exec=$INSTALL_DIR/$APP_NAME
Icon=$ICON_NAME
Type=Application
Categories=Utility;Office;
Keywords=productivity;task;management;
StartupNotify=true
Terminal=false
Version=$VERSION"
if [ "$INSTALL_TYPE" = "user" ]; then
echo "$DESKTOP_CONTENT" > "$DESKTOP_FILE"
else
echo "$DESKTOP_CONTENT" | $SUDO tee "$DESKTOP_FILE" > /dev/null
fi
# Update desktop database
eval $UPDATE_CMD 2>/dev/null || true
# Add to PATH if needed (user installation)
if [ "$INSTALL_TYPE" = "user" ] && ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
echo ""
echo "Adding ~/.local/bin to PATH..."
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
echo " Please run: source ~/.bashrc"
fi
echo ""
echo "$DISPLAY_NAME installed successfully!"
echo ""
echo "Installation details:"
echo " Installed to: $INSTALL_DIR"
echo " Command: $APP_NAME"
echo " Desktop entry: Available in applications menu"
echo ""
echo "You can now:"
echo " • Run from terminal: $APP_NAME"
echo " • Find it in your applications menu"
echo " • Launch with: $INSTALL_DIR/$APP_NAME"
install.sh in your project rootchmod +x install.sh# System-wide installation (requires sudo)./install.sh system# User-only installation./install.sh userTrimming Breaks Functionality
<PublishAot>true</PublishAot><TrimmerRootAssembly>TrimmerRoots.xml for reflection-heavy codeMissing Avalonia Controls
<ItemGroup>
<TrimmerRootAssembly Include="Avalonia.Controls" />
<TrimmerRootAssembly Include="Avalonia.Controls.DataGrid" />
<!-- Add other control libraries you use -->
</ItemGroup>
Performance Issues
Large File Size
InvariantGlobalization if you don’t need localization<DebugSymbols>false</DebugSymbols>Desktop Entry Not Appearing
ls -la ~/.local/share/applications/update-desktop-database ~/.local/share/applications/Command Not Found
ls -la /usr/local/bin/YourApp or ls -la ~/.local/bin/YourApp/opt/YourApp/YourAppRuntime Errors
ldd YourAppls -la /opt/YourApp/x:CompileBindings="True" to XAMLThis guide provides a complete workflow for publishing professional Avalonia UI applications on Linux with optimal performance and proper system integration.