Skip to main content

How to Publish a .NET Application from a Batch Script

Publishing a .NET application creates a self-contained, deployable package that includes the compiled application, its dependencies, configuration files, and optionally the .NET runtime itself. Unlike a simple build, publishing produces output that is ready to be copied directly to a server and run without requiring the .NET SDK on the target machine. Automating this process through Batch Script ensures consistent, reproducible deployments.

In this guide, we will explore how to publish .NET applications from a Batch Script using the dotnet publish command, covering framework-dependent and self-contained deployments, runtime identifiers, single-file publishing, and production-grade workflows.

Understanding Publish Modes

ModeFlagRuntime Required on Server?Output Size
Framework-dependent(default)Yes (.NET Runtime)Small
Self-contained--self-containedNoLarge (includes runtime)
Single-file/p:PublishSingleFile=trueDepends on modeSingle executable
ReadyToRun (R2R)/p:PublishReadyToRun=trueDepends on modeLarger, faster startup

Method 1: Basic Framework-Dependent Publish

@echo off
setlocal

set "project=MyWebApp"
set "config=Release"
set "output=publish"

:: Verify dotnet CLI is available
where dotnet >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] dotnet CLI not found. Install the .NET SDK.
pause
exit /b 1
)

echo Publishing %project% (%config%^)...

:: Clean output
if exist "%output%" rmdir /s /q "%output%"

dotnet publish "%project%" -c %config% -o "%output%"

if %errorlevel%==0 (
echo.
echo [SUCCESS] Published to: %output%
echo.
echo Contents:
dir /b "%output%\*.dll" "%output%\*.exe" "%output%\*.json" 2>nul
) else (
echo.
echo [ERROR] Publish failed.
exit /b 1
)

pause

Method 2: Self-Contained Publish

Include the .NET runtime so the target server does not need .NET installed:

@echo off
setlocal

set "project=MyWebApp"
set "config=Release"
set "rid=win-x64"
set "output=publish\%rid%"

echo Publishing self-contained for %rid%...

if exist "%output%" rmdir /s /q "%output%"

dotnet publish "%project%" -c %config% -r %rid% --self-contained -o "%output%"

if %errorlevel%==0 (
echo.
echo [SUCCESS] Self-contained publish complete.
echo Output: %output%
echo.
echo The application can run on any Windows x64 machine without .NET installed.
) else (
echo [ERROR] Publish failed.
exit /b 1
)

pause

Common Runtime Identifiers (RIDs)

RIDPlatform
win-x64Windows 64-bit
win-x86Windows 32-bit
win-arm64Windows ARM64
linux-x64Linux 64-bit
linux-arm64Linux ARM64
osx-x64macOS Intel
osx-arm64macOS Apple Silicon

Method 3: Single-File Publish

Create a single executable that bundles everything:

@echo off
setlocal enabledelayedexpansion

set "project=MyConsoleApp"
set "config=Release"
set "rid=win-x64"
set "output=publish\single"

echo Publishing as single file...

if exist "%output%" rmdir /s /q "%output%"

dotnet publish "%project%" -c %config% -r %rid% --self-contained ^
/p:PublishSingleFile=true ^
/p:PublishTrimmed=true ^
/p:IncludeNativeLibrariesForSelfExtract=true ^
-o "%output%"

if !errorlevel!==0 (
echo.
echo [SUCCESS] Single file published.
for %%F in ("%output%\*.exe") do (
echo File: %%~nxF
echo Size: %%~zF bytes
)
) else (
echo [ERROR] Failed.
exit /b 1
)

pause
info

/p:PublishTrimmed=true removes unused framework code, reducing the single-file size significantly. However, trimming can break applications that use reflection. Test thoroughly after enabling trimming.

Method 4: Full Publish Pipeline with Versioning

@echo off
setlocal enabledelayedexpansion

:: Configuration
set "project=MyWebApp"
set "config=Release"
set "publish_dir=artifacts\publish"
set "package_dir=artifacts\packages"

:: Verify dotnet CLI
where dotnet >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] dotnet CLI not found.
exit /b 1
)

echo =============================================
echo .NET PUBLISH PIPELINE
echo =============================================
echo.

:: Version
set "version=1.0.0"
if exist VERSION (
set /p "version=" < VERSION
)
set "build=0"
if exist BUILD_NUMBER (
set /p "build=" < BUILD_NUMBER
)
set /a build+=1
>BUILD_NUMBER echo !build!

set "full_version=!version!.!build!"
echo Version: !full_version!
echo.

:: Stage 1: Clean
echo [1/4] Cleaning...
if exist "%publish_dir%" rmdir /s /q "%publish_dir%"
if exist "%package_dir%" rmdir /s /q "%package_dir%"
mkdir "%publish_dir%"
mkdir "%package_dir%"

dotnet clean "%project%" -c %config% --verbosity quiet >nul 2>&1
echo Done.

:: Stage 2: Test
echo [2/4] Running tests...
if exist "%project%.Tests" (
dotnet test "%project%.Tests" -c %config% --verbosity quiet
if !errorlevel! neq 0 (
echo [FAIL] Tests failed. Aborting publish.
exit /b 1
)
echo Done.
) else (
echo No test project found (%project%.Tests^). Skipping.
)

:: Stage 3: Publish
echo [3/4] Publishing...
dotnet publish "%project%" -c %config% -o "%publish_dir%" ^
/p:Version=!full_version! ^
/p:AssemblyVersion=!full_version! ^
/p:InformationalVersion=!full_version!

if !errorlevel! neq 0 (
echo [FAIL] Publish failed.
exit /b 1
)
echo Done.

:: Add build info
for /f "tokens=2 delims==" %%T in ('wmic os get LocalDateTime /value') do set "dt=%%T"
set "build_date=%dt:~0,4%-%dt:~4,2%-%dt:~6,2% %dt:~8,2%:%dt:~10,2%:%dt:~12,2%"

(
echo version=!full_version!
echo published=!build_date!
echo machine=%COMPUTERNAME%
echo config=%config%
) > "%publish_dir%\build_info.txt"

:: Stage 4: Package
echo [4/4] Packaging...
set "package=%package_dir%\%project%_v!full_version!.zip"

powershell -NoProfile -Command ^
"try { Compress-Archive -Path '%publish_dir%\*' -DestinationPath '!package!' -Force -ErrorAction Stop; exit 0 }" ^
"catch { Write-Host ('[ERROR] ' + $_.Exception.Message); exit 1 }"

if !errorlevel! neq 0 (
echo [FAIL] Packaging failed.
exit /b 1
)

for %%F in ("!package!") do set "size=%%~zF"

echo.
echo =============================================
echo PUBLISH COMPLETE
echo Version: !full_version!
echo Output: %publish_dir%
echo Package: !package!
echo Size: !size! bytes
echo =============================================

pause

Method 5: Multi-Platform Publish

Build for multiple target platforms in one script:

@echo off
setlocal enabledelayedexpansion

set "project=MyToolApp"
set "config=Release"
set "output_base=artifacts"

set "platform_count=3"
set "platforms[0]=win-x64"
set "platforms[1]=linux-x64"
set "platforms[2]=osx-x64"

echo =============================================
echo MULTI-PLATFORM PUBLISH
echo =============================================
echo.

set "success=0"
set "failed=0"
set /a last=platform_count - 1

for /L %%i in (0,1,!last!) do (
set "rid=!platforms[%%i]!"
set "out=%output_base%\!rid!"

echo Building for !rid!...

if exist "!out!" rmdir /s /q "!out!"

dotnet publish "%project%" -c %config% -r !rid! --self-contained ^
/p:PublishSingleFile=true -o "!out!" --verbosity quiet

if !errorlevel!==0 (
set /a success+=1
for %%F in ("!out!\%project%*") do echo [OK] %%~nxF (%%~zF bytes^)
) else (
set /a failed+=1
echo [FAIL]
)
)

echo.
echo =============================================
echo RESULTS: !success! succeeded, !failed! failed
echo =============================================

if !failed! gtr 0 exit /b 1
pause

Docker Publish

Publish and create a Docker image:

@echo off
setlocal

set "project=MyWebAPI"
set "image_name=mywebapi"
set "publish_dir=publish\docker"

set "version=latest"
if exist VERSION (
set /p "version=" < VERSION
)

:: Verify Docker is available
where docker >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Docker not found.
pause
exit /b 1
)

if not exist "Dockerfile" (
echo [ERROR] Dockerfile not found in the current directory.
pause
exit /b 1
)

echo Publishing for Docker...

:: Publish for Linux
if exist "%publish_dir%" rmdir /s /q "%publish_dir%"

dotnet publish "%project%" -c Release -r linux-x64 --self-contained -o "%publish_dir%"
if %errorlevel% neq 0 (
echo [ERROR] dotnet publish failed.
exit /b 1
)

:: Build Docker image
docker build -t %image_name%:%version% .

if %errorlevel%==0 (
echo [SUCCESS] Docker image: %image_name%:%version%
) else (
echo [ERROR] Docker build failed.
exit /b 1
)

pause

Common Mistakes

The Wrong Way: Publishing Debug Configuration

:: WRONG - Debug builds include debug symbols, are not optimized
dotnet publish MyApp -c Debug -o publish

Output Concern: Debug configuration includes unoptimized code, debug symbols, and potentially development-only settings. Always publish with -c Release for production deployments.

The Wrong Way: Not Specifying Output Directory

:: PROBLEMATIC - Publishes to a deep nested default path
dotnet publish MyApp -c Release
:: Output goes to bin\Release\net8.0\publish\ (hard to find)

Always use -o publish (or similar) to specify a clear, predictable output location.

Best Practices

  1. Always publish in Release mode: -c Release enables optimizations and excludes debug artifacts.
  2. Specify the output directory: Use -o for a predictable deployment source.
  3. Embed version information: Use /p:Version=x.y.z to stamp assemblies with the release version.
  4. Use self-contained for portability: Include the runtime when the target server's .NET version is uncertain.
  5. Test after publishing: Run the published output locally before deploying to production.

Conclusion

Publishing a .NET application from a Batch Script is handled by the dotnet publish command, which produces a deployment-ready package from the project source. By choosing between framework-dependent and self-contained modes, enabling single-file publishing for simpler distribution, and embedding version information into the assemblies, teams create professional deployment artifacts. The publish pipeline pattern (clean, test, publish, package) ensures that every published artifact is verified, versioned, and ready for deployment to any target environment.