Skip to content
Valerio edited this page Apr 28, 2026 · 3 revisions

CI/CD and Production Deployment

UncannyPrompt ships with a GitHub Actions pipeline that builds the WebApp container image and pushes it to the GitHub Container Registry (ghcr.io).

The pipeline publishes only the application image:

ghcr.io/<owner>/uncannyprompt-webapp

SQL Server is not built or published by this pipeline. In production it can be:

  • a container owned by the production Compose file
  • a managed SQL Server instance
  • an externally maintained shared SQL Server

Pipeline overview

The workflow is defined in .github/workflows/deploy.yml and triggers on every push to main, or manually from the GitHub Actions tab.

Developer machine
      |  git push -> main
      v
GitHub Actions
  `- docker build -> ghcr.io/<owner>/uncannyprompt-webapp:latest

The workflow also publishes immutable sha-* tags. The latest tag is emitted only for the default branch.

Unlike some larger deployments, the current workflow does not SSH into the server. The intended operating model is:

  1. GitHub Actions builds and publishes the WebApp image.
  2. The production server pulls the image.
  3. The production server restarts the WebApp container through Docker Compose.

This keeps deployment credentials out of GitHub Actions and matches the current repository setup.

Key files

File Purpose
.github/workflows/deploy.yml Builds and pushes the WebApp image to GHCR
infra/Dockerfile.webapp Multi-stage .NET build for UncannyPrompt.WebApp
infra/deploy.sh Routine production rollout of the WebApp container only
infra/bootstrap.sh First bootstrap or full stack refresh
docker-compose.prod.example.yml Production Compose starting point using the pre-built GHCR image
docker-compose.yml Development Compose stack that builds locally
.env.example Template for runtime configuration
src/UncannyPrompt.WebApp/appsettings.json Runtime defaults, including ReverseProxy forwarded-header settings

Production topology

The default production topology is intentionally small:

Browser
  -> Nginx / reverse proxy / TLS termination
  -> UncannyPrompt.WebApp
  -> SQL Server

The production example in docker-compose.prod.example.yml is a convenient baseline, not a mandatory final shape. You can keep SQL Server in the same Compose project, or omit it and point Database__Host / Database__Port to a centralized SQL Server.

Ubuntu production deployment

This section describes a generic Ubuntu-based flow. Exact details depend on whether SQL Server is local to the same host or provided externally.

Prerequisites

  • Ubuntu 22.04 LTS or later
  • Docker Engine and Docker Compose plugin installed
  • A DNS record pointed at the server IP
  • A GitHub account able to pull the published GHCR package, if the package is private

If needed, verify Docker Compose is available:

docker compose version

First production setup

Create the deployment directory on the server:

mkdir -p /var/www/uncannyprompt
cd /var/www/uncannyprompt

Create the real production Compose file from the example:

cp docker-compose.prod.example.yml docker-compose.yml

Create a production .env with real values. Do not commit this file.

Important production values include:

  • DOTNET_ENVIRONMENT=Production
  • Database__Host, Database__Port, Database__Name, Database__Username, Database__Password
  • Database__TrustServerCertificate=false when production SQL Server uses a valid certificate
  • Database__ApplyMigrationsOnStartup=false if migrations are applied out-of-band
  • provider credentials under Authentication__Google__*, Authentication__EntraId__*, Authentication__GitHub__*
  • SeedOptions__AdminUsers__0__* for the first platform admin, when needed

If the GHCR package is private, authenticate the server before pulling:

echo "<github-token>" | docker login ghcr.io -u <github-user> --password-stdin

Then bootstrap the stack:

docker compose pull
docker compose up -d
docker compose logs -f webapp

Equivalent helper:

./infra/bootstrap.sh /var/www/uncannyprompt

Nginx reverse proxy with TLS

Install Nginx and Certbot:

sudo apt install -y nginx certbot python3-certbot-nginx

Create /etc/nginx/sites-available/uncannyprompt:

server {
    server_name uncannyprompt.com;

    location / {
        proxy_pass         http://localhost:8080;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection keep-alive;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
        client_max_body_size 10m;
    }
}

Enable the site and request the TLS certificate:

sudo ln -s /etc/nginx/sites-available/uncannyprompt /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d uncannyprompt.com

Deployment modes

The repository currently supports one deployment mode directly:

Option 1 - Build in GitHub Actions, deploy from the server

Standard application deploy after a new image has been published:

cd /var/www/uncannyprompt
docker compose pull webapp
docker compose up -d --no-deps --remove-orphans webapp
docker image prune -f

Equivalent helper:

./infra/deploy.sh /var/www/uncannyprompt

First bootstrap or full stack refresh:

cd /var/www/uncannyprompt
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f

If you later want GitHub Actions to SSH into the server and run these commands automatically, that can be added as a future extension, but it is not part of the current workflow.

Image naming

The workflow lowercases the repository owner before composing the GHCR image name. For the Ryadel repository, the production example currently points to:

ghcr.io/ryadel/uncannyprompt-webapp:latest

Forks or private deployments should update the server-local docker-compose.yml if they publish under a different owner.

Rolling deployments

Recommended flow:

  1. Let GitHub Actions build and publish the WebApp image.
  2. Apply EF migrations against the production database.
  3. Pull and deploy the new WebApp version.
  4. Verify /health.
  5. Check logs for migration, configuration, and authentication errors.

Avoid running schema-changing migrations automatically at startup in production unless the deployment platform guarantees singleton startup and controlled rollback behavior.

Troubleshooting

Symptom Check
docker pull returns unauthorized The GHCR package is private or the server is not logged in
WebApp exits immediately Inspect docker compose logs webapp and verify .env values
Database connection fails Check Database__Host, Database__Port, credentials, SQL Server reachability, and certificate settings
Login provider missing Provider client id/secret/tenant settings are incomplete
Google login returns redirect_uri_mismatch Verify the public callback URL is https://<domain>/signin-google and register that exact URI in Google Cloud Console
Entra login callback fails Verify Authentication__EntraId__CallbackPath and the app registration reply URL
New migrations are not applied Run EF migrations out-of-band or explicitly enable startup migrations only for controlled environments

Clone this wiki locally