Deployment
Overview
CustomsHive ships as a container image. A tag on docker-X.Y.Z builds the image, publishes it to
the container registry, and the environment runs it at a pinned tag — nothing auto-updates.
flowchart TD
dev(["git tag docker-3.2.0<br/>git push --tags"])
dev --> ci
subgraph ci["Forgejo Actions"]
build["dotnet publish"] --> img["docker buildx<br/>linux/amd64 + linux/arm64"]
img --> push["push to ghcr.io"]
end
push --> reg["ghcr.io/rousseauxy/customshive:3.2.0"]
reg --> deploy
subgraph deploy["Container host"]
compose["compose pins the image tag"] --> app["customshive container"]
app --> db["SQL Server container"]
end
The build runs on a self-hosted runner, which is also what gives the job a Docker daemon to build with. Images are multi-architecture, so the same tag runs on both x86 and arm64 hosts.
Deployment itself is deliberately manual: the compose file names an exact version, so upgrading is a
one-line change and a docker compose up -d. There is no watcher that pulls new versions on its own.
Releasing
- Bump the version and commit.
- Tag
docker-X.Y.Zand push the tag. - The workflow builds and publishes
ghcr.io/rousseauxy/customshive:X.Y.Z. - Update the image tag in the environment's compose file and recreate the container.
The image is tagged from the git tag (docker-3.2.0 → 3.2.0, 3.2, 3), so an environment can
pin as loosely or as tightly as it wants. Pin the full version unless you have a reason not to.
History: IIS and Azure DevOps
Earlier versions deployed to IIS via an Azure DevOps pipeline, with a self-hosted agent
extracting a zip and switching the site's physical path, and a gMSA app-pool identity.
All of it is gone — the pipeline trigger went in August 2026 and the workflow, the
Deploy-IIS.ps1 script and the setup instructions were deleted in September 2026 (B-16).
Nothing has run on IIS since the move to containers; the instructions were kept "just in
case" long enough to start reading as a second supported path.
dv — Docker / NAS deployment
This section applies to the dv (development / self-hosted) environment only. It uses GitHub Container Registry and Docker — no IIS, no ADO, no Windows required.
How it works
Every tag matching docker-* triggers .github/workflows/docker.yml, which builds a
multi-stage .NET 10 Docker image and pushes it to ghcr.io:
git tag docker-0.6.0
git push origin docker-0.6.0
-> GitHub Actions builds image
-> pushes ghcr.io/rousseauxy/customshive:latest
-> pushes ghcr.io/rousseauxy/customshive:<version>
-> pushes ghcr.io/rousseauxy/customshive:<major>.<minor>
The NAS either pulls manually or uses Watchtower to pick up the new image automatically.
Cost: GitHub Actions free tier provides 2,000 minutes/month.
A full .NET build takes ~2-4 minutes; layer caching (type=gha) reduces repeat builds to ~30-60 s.
Normal commit frequency stays well within the free allowance.
Persistent volumes
| Container path | What goes there |
|---|---|
/app/uploads |
Uploaded PDFs / XLSXes and temp files |
Mount /app/uploads so files survive image updates.
One-time NAS setup
1. Authenticate to ghcr.io (required for private repos)
Create a GitHub PAT with the read:packages scope, then on the NAS:
2. Create persistent directories
3. Create a .env file (never commit this to git)
# /volume1/docker/customstuf/.env
MSSQL_SA_PASSWORD=your-sql-password
AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
4. Create the compose file
# /volume1/docker/customstuf/docker-compose.dv.yml
services:
customstuf:
image: ghcr.io/rousseauxy/customshive:latest
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- /volume1/docker/customstuf/uploads:/app/uploads
environment:
ASPNETCORE_ENVIRONMENT: Production
Auth__Provider: EntraId
ConnectionStrings__DefaultConnection: Server=your-sql-host,1433;Database=customstuf;User Id=sa;Password=${MSSQL_SA_PASSWORD};TrustServerCertificate=true
ConnectionStrings__TarbelConnection: Server=your-sql-host,1433;Database=tarbel;User Id=sa;Password=${MSSQL_SA_PASSWORD};TrustServerCertificate=true
AI__Provider: AzureOpenAI
AI__AzureOpenAI__Endpoint: https://your-resource.openai.azure.com/
AI__AzureOpenAI__DeploymentName: gpt-4o
AI__AzureOpenAI__UseKeylessAuth: "true"
DocumentIntelligence__Endpoint: https://your-resource.cognitiveservices.azure.com/
DocumentIntelligence__UseKeylessAuth: "true"
Descartes__Smf__ValidateXsd: "true"
Descartes__Smf__XsdFolder: /app/schemas/smf
Azure__TenantId: ${AZURE_TENANT_ID}
Azure__ClientId: ${AZURE_CLIENT_ID}
Azure__ClientSecret: ${AZURE_CLIENT_SECRET}
/app/schemas/smf is bundled in the image during publish and contains SMF.xsd and included schema files.
5. Start
Updating manually
Auto-updating with Watchtower (optional)
Watchtower polls ghcr.io every 5 minutes and
restarts the container when a new latest is published:
docker run -d \
--name watchtower \
--restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock \
-e WATCHTOWER_CLEANUP=true \
containrrr/watchtower \
--interval 300 \
customstuf
For private registries, also pass:
Configuration reference
All settings follow ASP.NET Core's Section__Key env-var convention.
| Variable | Default in image | Description |
|---|---|---|
ASPNETCORE_ENVIRONMENT |
Production |
Environment name (Development loads appsettings.Development.json) |
Descartes__Smf__ValidateXsd |
true |
Enables SMF XSD validation in the wrapper service |
Descartes__Smf__XsdFolder |
/app/schemas/smf |
Folder containing SMF.xsd and included XSDs |
ASPNETCORE_URLS |
http://+:8080 |
Listening address inside the container |
ConnectionStrings__DefaultConnection |
(empty) | Required SQL Server connection string for main app DB |
ConnectionStrings__TarbelConnection |
(empty) | Required SQL Server connection string for Tarbel DB |
Storage__UploadFolder |
/app/uploads |
Upload and temp file root |
AI__Provider |
AzureOpenAI |
AzureOpenAI or OpenAI |
AI__AzureOpenAI__Endpoint |
(empty) | Azure OpenAI resource endpoint URL |
AI__AzureOpenAI__DeploymentName |
(empty) | Deployment name (e.g. gpt-4o) |
AI__AzureOpenAI__UseKeylessAuth |
true |
true = use Azure__* credentials; false = use ApiKey |
AI__AzureOpenAI__ApiKey |
(empty) | Required when UseKeylessAuth is false |
AI__OpenAI__BaseUrl |
(empty) | Optional OpenAI-compatible endpoint when using OpenAI provider |
AI__OpenAI__ApiKey |
(empty) | OpenAI API key when using OpenAI provider |
AI__OpenAI__Model |
gpt-4o-mini |
Model name for OpenAI provider |
DocumentIntelligence__Endpoint |
(empty) | Azure Document Intelligence resource endpoint URL |
DocumentIntelligence__UseKeylessAuth |
true |
true = use Azure__* credentials; false = use ApiKey |
DocumentIntelligence__ApiKey |
(empty) | Required when UseKeylessAuth is false |
DocumentIntelligence__ModelId |
prebuilt-layout |
Document model for OCR analysis |
Azure__TenantId |
(empty) | App registration tenant ID (shared by OpenAI + DI keyless auth) |
Azure__ClientId |
(empty) | App registration client ID |
Azure__ClientSecret |
(empty) | App registration client secret |