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

Publishing and deploying Avalonia UI applications on Linux can be tricky, especially when dealing with file sizes, dependencies, and making your app feel like a native Linux application. This comprehensive guide covers everything you need to know about publishing, optimizing, and installing Avalonia apps on Linux systems.
This option bundles the .NET runtime with your app, so users don’t need .NET installed:
dotnet publish -r linux-x64 --self-contained -c Release
Pros:
Cons:
This creates a smaller package but requires users to have .NET installed:
dotnet publish -r linux-x64 -c Release
Pros:
Cons:
Combines everything into one executable:
dotnet publish -r linux-x64 --self-contained -c Release -p:PublishSingleFile=true
Removes unused code to reduce size:
dotnet publish -r linux-x64 --self-contained -c Release -p:PublishSingleFile=true -p:PublishTrimmed=true
Add these properties to your .csproj file for optimal size:
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
<InvariantGlobalization>true</InvariantGlobalization>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
If your app uses JSON serialization (saving data to files), trimming might remove necessary code.
Solution 1: Preserve JSON Serialization
Add to your .csproj:
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
<InvariantGlobalization>true</InvariantGlobalization>
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
<ItemGroup>
<TrimmerRootAssembly Include="System.Text.Json" />
</ItemGroup>
Solution 2: Use Source Generators (Recommended)
Create a JSON context for your models:
using System.Text.Json.Serialization;
[JsonSerializable(typeof(List<TodoTask>))]
[JsonSerializable(typeof(TodoTask))]
public partial class AppJsonContext : JsonSerializerContext
{
}
// Use it in your serialization code:
var json = JsonSerializer.Serialize(tasks, AppJsonContext.Default.ListTodoTask);
var tasks = JsonSerializer.Deserialize(json, AppJsonContext.Default.ListTodoTask);
Solution 3: Preserve Your Classes
Create a TrimmerRoots.xml file:
<linker>
<assembly fullname="YourAppName">
<type fullname="YourAppName.Models.TodoTask" preserve="all" />
</assembly>
</linker>
Add to .csproj:
<ItemGroup>
<TrimmerRootDescriptor Include="TrimmerRoots.xml" />
</ItemGroup>
Problem: Assembly.GetExecutingAssembly().Location returns empty string in single-file apps.
Solution: Use proper file paths:
// ❌ Don't use this:
var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
// ✅ Use this instead:
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var appDataDirectory = Path.Combine(homeDirectory, ".yourapp");
Directory.CreateDirectory(appDataDirectory);
var dataFilePath = Path.Combine(appDataDirectory, "data.json");
After publishing, even with PublishSingleFile=true, you’ll still have multiple files:
libSkiaSharp.so, libHarfBuzzSharp.so).pdb files – optional)Important: All these files must stay together – your app won’t work if you move just the executable.
# Navigate to publish directory
cd ./bin/Release/net9.0/linux-x64/publish/
# Create installation directory
sudo mkdir -p /opt/YourAppName
# Copy all files
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/net9.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
To make your app appear in the applications menu, create a desktop entry:
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=keyword1;keyword2;
StartupNotify=true
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/$USER/.local/opt/YourAppName/YourAppName
Icon=utilities-text-editor
Type=Application
Categories=Utility;Office;
Keywords=keyword1;keyword2;
StartupNotify=true
EOF
update-desktop-database ~/.local/share/applications/
Utility – System utilities and toolsOffice – Productivity applicationsDevelopment – Programming and development toolsGraphics – Image editing and design applicationsAudioVideo – Media players and editorsGame – Games and entertainmentNetwork – Network and internet applicationsHere’s a comprehensive installation script that handles everything:
#!/bin/bash
# Configuration
APP_NAME="YourAppName"
DISPLAY_NAME="Your App Display Name"
DESCRIPTION="Brief description of your app"
INSTALL_DIR="/opt/$APP_NAME"
BIN_LINK="/usr/local/bin/$APP_NAME"
DESKTOP_FILE="/usr/share/applications/$APP_NAME.desktop"
echo "Building $DISPLAY_NAME..."
dotnet publish -r linux-x64 --self-contained -c Release -p:PublishSingleFile=true -p:PublishTrimmed=true
# Check if build was successful
if [ ! -f "./bin/Release/net9.0/linux-x64/publish/$APP_NAME" ]; then
echo "Error: Build failed or executable not found"
exit 1
fi
echo "Installing $DISPLAY_NAME..."
# Create installation directory
sudo mkdir -p "$INSTALL_DIR"
# Copy all files from publish directory
sudo cp -r ./bin/Release/net9.0/linux-x64/publish/* "$INSTALL_DIR/"
# Make main executable
sudo chmod +x "$INSTALL_DIR/$APP_NAME"
# Create symlink for command line access
sudo ln -sf "$INSTALL_DIR/$APP_NAME" "$BIN_LINK"
# Create desktop entry for GUI integration
sudo tee "$DESKTOP_FILE" > /dev/null << EOF
[Desktop Entry]
Name=$DISPLAY_NAME
Comment=$DESCRIPTION
Exec=$INSTALL_DIR/$APP_NAME
Icon=utilities-text-editor
Type=Application
Categories=Utility;Office;
Keywords=todo;task;productivity;
StartupNotify=true
EOF
# Update desktop database
sudo update-desktop-database /usr/share/applications/ 2>/dev/null
echo ""
echo "✅ $DISPLAY_NAME installed successfully!"
echo ""
echo "You can now:"
echo " • Run from terminal: $APP_NAME"
echo " • Find it in your applications menu"
echo " • Installed to: $INSTALL_DIR"
echo ""
install.sh in your project rootchmod +x install.sh
./install.sh
<DebugSymbols>false</DebugSymbols>-p:PublishTrimmed=true*.so files)ls -la /usr/share/applications/YourApp.desktopsudo update-desktop-database /usr/share/applications//usr/local/bin is in PATH: echo $PATHls -la /usr/local/bin/YourAppName/usr/local/bin/YourAppNameThis is normal for .NET applications with UI frameworks. To reduce:
While Avalonia apps are larger than traditional native applications, proper deployment strategies can create a professional, user-friendly installation experience. The trade-off between convenience and resource usage is often acceptable for most desktop applications.
Remember to test your deployment thoroughly, especially when using trimming optimizations, as they can break functionality in subtle ways that only appear in production builds.
This guide covers .NET 9.0 and Avalonia UI on Linux. Commands and paths may vary slightly between different Linux distributions.