# Dockerfile

> Build a Linux container image for an ASP.NET Core app with TX Text Control: multi-stage Dockerfile, NuGet feed secret, base images, chiseled images and gconv.

- **Product:** TX Text Control .NET Server
- **Section:** Documentation
- **Kind:** Topic
- **Path:** TX Text Control .NET Server > Deployment Guide > Docker > Dockerfile
- **2672 words**
- **Web URL:** https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/dockerfile/
- **LLMs URL:** https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/dockerfile/llms.txt
- **LLMs-Full URL:** https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/dockerfile/llms-full.txt

---

A container image for an application with TX Text Control .NET Server is built in two stages: the .NET SDK image restores the packages from the Text Control Private NuGet Feed and publishes the application for `linux-x64`, and the ASP.NET Core runtime image receives only the published files. The engine, its native libraries, its fonts and the Document Editor backend travel inside the publish output, so the image needs no further installation.

What follows builds such an image for an ASP.NET Core application with document processing, the Document Editor and the Document Viewer, keeps the feed credentials out of every image layer and shows which base images work.

### Prerequisites

- **Docker with BuildKit**, the builder behind `docker buildx build`. On Windows, Docker Desktop must run Linux containers.
- **An ASP.NET Core application that uses the Core engine**, for example the application of [Document Editor: ASP.NET Core](https://docs.textcontrol.com/tx-text-control-dotnet-server/getting-started/document-editor/aspnet-core/llms-full.txt). The examples use a .NET 10 project *MyApp* in the folder *MyApp*.
- **Your *nuget.config*** with the credentials of the Text Control Private NuGet Feed. [nuget.config](https://docs.textcontrol.com/tx-text-control-dotnet-server/installation/nuget/private-feed/nuget-config/llms-full.txt) describes the file.
- **Runtime licenses.** Each running container instance needs one. See [Runtime Licenses](https://docs.textcontrol.com/tx-text-control-dotnet-server/installation/licensing/runtime/#cloud-and-container-deployments).

### Choose the Base Image

The final stage of the Dockerfile starts from one of Microsoft's ASP.NET Core runtime images. For .NET 10, the images are based on Ubuntu 24.04:

| Image | TX Text Control |
|---|---|
| `mcr.microsoft.com/dotnet/aspnet:10.0` | Works without changes. Document processing, the Document Editor and the Document Viewer run in it. |
| `mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extra` | Works after you copy the gconv modules into the image. The image contains ICU. |
| `mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled` | Needs the gconv modules as well, and runs .NET in globalization-invariant mode, in which the Document Editor backend logs culture errors. Use the `-extra` variant instead. |
| `mcr.microsoft.com/dotnet/aspnet:10.0-alpine` | Doesn't work. Alpine Linux uses musl instead of the GNU C Library, and the application process ends when it creates a document. |

For a .NET 8 application, use the 8.0 images, for example `mcr.microsoft.com/dotnet/aspnet:8.0`. The build stage uses the SDK image of the same .NET version as the runtime image. [Linux Requirements](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/linux/requirements/llms-full.txt) explains the libraries behind these results.

### Write the Dockerfile

The build context is the folder that contains the project folder:

```text
Dockerfile
.dockerignore
nuget.config
MyApp/
    MyApp.csproj
    Program.cs
    ...
```

Save the following *Dockerfile* next to the project folder:

```dockerfile
# syntax=docker/dockerfile:1

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# Restore the packages with nuget.config, passed as a build secret
COPY MyApp/MyApp.csproj MyApp/
RUN --mount=type=secret,id=nugetconfig \
    dotnet restore MyApp/MyApp.csproj -r linux-x64 --configfile /run/secrets/nugetconfig

# Publish for Linux without restoring again
COPY MyApp/ MyApp/
RUN dotnet publish MyApp/MyApp.csproj -c Release -r linux-x64 --self-contained false \
    --no-restore -o /app/publish /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
USER $APP_UID
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]
```

The Dockerfile works as follows:

- **`# syntax=docker/dockerfile:1`** selects the Dockerfile syntax that supports secret mounts.
- **The build stage** copies the project file first and restores the packages. As long as the project file doesn't change, Docker reuses the restored layer.
- **`RUN --mount=type=secret,id=nugetconfig`** makes *nuget.config* available at */run/secrets/nugetconfig* for this one command. The file isn't written to any layer, so the API token of the feed stays out of the build cache and the image. `--configfile` makes `dotnet restore` use only this file, which lists both nuget.org and the Text Control feed.
- **`-r linux-x64`** in the restore is required, because `dotnet publish` with `--no-restore` doesn't restore again. Without it, the publish ends with error NETSDK1047. Publishing with `--no-restore` means that the second `dotnet` command doesn't need the credentials.
- **`dotnet publish`** puts the application, the TX Text Control assemblies, the native libraries such as *tx34core.so*, the *Fonts* folder and the files of the Document Editor backend into */app/publish*. `/p:UseAppHost=false` leaves out the executable, which isn't needed with `dotnet MyApp.dll`.
- **The final stage** starts again from the runtime image and copies only the publish output. `USER $APP_UID` runs the application as the non-root user `app`, which the .NET images define. ASP.NET Core in these images listens on port 8080.
- **`ENTRYPOINT`** starts the application with `dotnet`. The hosted service `DocumentEditorWorkerManager` starts the Document Editor backend in the same container with the `dotnet` command, so nothing else needs to run.

### Keep nuget.config Out of the Image

Add a *.dockerignore* file next to the *Dockerfile*, so that `COPY` never takes *nuget.config* or local build output into the build context:

```text
nuget.config
**/bin
**/obj
```

Keep *nuget.config* out of the project folder as well. A web project copies *.config* files from its folder into the build and publish output, so a *nuget.config* next to *MyApp.csproj* would end up in */app/publish* and in the final image, together with its API token.

On a build server, the pipeline writes *nuget.config* from secrets before the build. [CI/CD Authentication](https://docs.textcontrol.com/tx-text-control-dotnet-server/installation/nuget/private-feed/ci-cd/llms-full.txt) shows this step for GitHub Actions, GitLab, Azure DevOps and other systems. Pass the generated file to `docker buildx build` with `--secret`.

### Build and Run the Image

Build the image in the folder with the *Dockerfile* and pass *nuget.config* as the secret:

```bash
docker buildx build --secret id=nugetconfig,src=nuget.config -t myapp .
```

Run a container and map port 8080 of the container to port 8080 of your machine:

```bash
docker run --rm -p 8080:8080 myapp
```

The container output shows the Document Editor backend starting before the application listens on port 8080:

```text
info: TXTextControl.Web.DocumentEditor.Backend.DocumentEditorWorkerManager[0]
      Started TXTextControl.Web.Server.Core with Process ID: 23
info: TXTextControl.Web.DocumentEditor.Backend.DocumentEditorWorkerManager[0]
      [TXTextControl.Web.Server.Core] Successfully read config file 'TXTextControl.Web.Server.Core.config.json'.
info: TXTextControl.Web.DocumentEditor.Backend.DocumentEditorWorkerManager[0]
      [TXTextControl.Web.Server.Core] Starting TCP listener on port 4283 for IPv4 and IPv6.
info: TXTextControl.Web.DocumentEditor.Backend.DocumentEditorWorkerManager[0]
      [TXTextControl.Web.Server.Core] Logging initialized. LogLevel=Info, LogTarget=Console
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:8080
```

Open `http://localhost:8080` in a browser. The Document Editor loads, and each editor that connects adds the line "Starting web editor worker process with connection ID" to the output. Port 4283 of the backend is used only inside the container, so don't publish it with `-p`.

### Use a Chiseled Image

Chiseled images contain only the files .NET needs, have no shell or package manager and run as a non-root user by default. They leave out the gconv modules of the GNU C Library, which TX Text Control uses to convert character sets. Without them, the application process ends as soon as it creates a PDF document.

Copy the modules from the regular ASP.NET Core image of the same Ubuntu release. Replace the final stage of the Dockerfile with these stages:

```dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS gconv

FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extra AS final
WORKDIR /app

# TX Text Control needs the gconv modules, which chiseled images leave out
COPY --from=gconv /usr/lib/x86_64-linux-gnu/gconv /usr/lib/x86_64-linux-gnu/gconv

COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]
```

The `-extra` variant contains ICU; [System Files](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/linux/requirements/#system-files) in Linux Requirements explains why the plain `10.0-noble-chiseled` image isn't recommended. The blog article [Using TX Text Control with Ultra-Minimal Chiseled Linux Containers](https://www.textcontrol.com/blog/2025/03/12/using-tx-text-control-with-ultra-minimal-chiseled-linux-containers/) shows the same approach for the .NET 8 image `8.0-jammy-chiseled`.

### Create the Dockerfile With Visual Studio

A project created in Visual Studio with **Enable container support** gets a Dockerfile with a base, a build, a publish and a final stage, and the **Container (Dockerfile)** startup profile runs it in a Linux container on your machine without changes. [Development Environments](https://docs.textcontrol.com/tx-text-control-dotnet-server/installation/requirements/ide/#visual-studio-2026-and-visual-studio-2022) describes these options, and "Run the Application in a Linux Container" in [Document Editor: ASP.NET Core](https://docs.textcontrol.com/tx-text-control-dotnet-server/getting-started/document-editor/aspnet-core/#run-the-application) shows the steps for the application of the Getting Started tutorial.

The blog article [Document Editor with ASP.NET Core and Docker Support with Linux Containers using Visual Studio 2026 and .NET 10](https://www.textcontrol.com/blog/2025/12/30/document-editor-aspnet-core-docker-linux-visual-studio-2026-dotnet-10/) copies the backend files by hand, as articles for 33.0 did; the `TXTextControl.Web.DocumentEditor.Backend` package replaces that step. The generated file restores the packages without credentials for the private feed; add the secret mount and the publish options shown above before you build it for production.

### Next Steps

- [Fonts](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/fonts/llms-full.txt): add company fonts to the image.
- [Multiple Processes](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/multiple-processes/llms-full.txt): run the Document Editor backend in its own container.
- [Scaling Containers](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/scaling/llms-full.txt): run several instances behind a load balancer.
- [CI/CD](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/ci-cd/llms-full.txt): build and push the image in a pipeline.

---

## Nearby

- **Previous:** [Docker](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/llms-full.txt)
- **Next:** [Fonts](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/fonts/llms-full.txt)
