Skip to content

Deploying TMI Web Application

Eric Fitzgerald edited this page Apr 8, 2026 · 3 revisions

Deploying TMI Web Application

This guide covers how to deploy the TMI-UX Angular-based web application for production and development environments.

Overview

TMI-UX is an Angular/TypeScript single-page application (SPA) that provides:

  • Interactive threat modeling interface
  • Real-time collaborative diagram editing
  • OAuth authentication flow
  • RESTful API integration
  • WebSocket support for collaboration

Quick Start

Choose your deployment method:

Method Best For Link
Static Hosting Fastest, CDN-friendly (recommended) Static Hosting
Node.js Server Custom server configuration Node.js Server
Docker Containerized deployment Docker Deployment
Heroku Platform-as-a-Service deployment Heroku Deployment
Development Server Local development Development Server

Prerequisites

Before you begin, ensure you have:

  • Node.js 22 (required; see .nvmrc for the exact version)
  • pnpm (package manager; version pinned in the package.json packageManager field)
  • Access to a TMI API server (see Deploying-TMI-Server)
  • OAuth provider credentials, if you are self-hosting authentication (see Setting-Up-Authentication)

Building the Application

Install Dependencies

# Clone the repository
git clone https://github.com/ericfitz/tmi-ux.git
cd tmi-ux

# Install pnpm if needed
npm install -g pnpm

# Install dependencies
pnpm install

Environment Configuration

TMI-UX uses Angular file replacements to manage environment configuration. Each build configuration in angular.json swaps src/environments/environment.ts with a target-specific file at compile time.

To create a custom environment, copy the example file:

cp src/environments/environment.example.ts src/environments/environment.myenv.ts

Edit the new file with your deployment settings. A reference for all available properties is in src/environments/environment.interface.ts. Here is a typical production configuration:

// src/environments/environment.prod.ts
import { Environment } from './environment.interface';

export const environment: Environment = {
  production: true,
  logLevel: 'ERROR',
  apiUrl: 'https://api.tmi.example.com',  // Your TMI API server
  authTokenExpiryMinutes: 60,
  operatorName: 'TMI Operator',
  operatorContact: 'support@example.com',
  operatorJurisdiction: '',
  securityConfig: {
    enableHSTS: true,
    hstsMaxAge: 31536000,
    hstsIncludeSubDomains: true,
    hstsPreload: false,
    frameOptions: 'DENY',
    referrerPolicy: 'strict-origin-when-cross-origin',
    permissionsPolicy: 'camera=(), microphone=(), geolocation=()',
  },
};

Note: Angular bakes environment values into the JavaScript bundle at build time. However, when you use the Express server (server.js), you can override many settings at runtime through TMI_* environment variables. See Configuration-Reference for details.

Build for Production

# Production build (optimized, minified)
pnpm run build:prod

# Output: dist/tmi-ux/browser/

The build uses Angular's application builder, which places output in a browser/ subdirectory. Build output includes:

  • index.html -- main HTML file
  • *.js -- bundled JavaScript (hashed filenames for cache-busting)
  • *.css -- compiled stylesheets
  • Image and font assets

Other build configurations are available for specific deployment targets:

pnpm run build:hosted-container  # Heroku / hosted container deployments
pnpm run build:oci               # Oracle Cloud Infrastructure
pnpm run build:container         # Generic container (Chainguard, etc.)

Static Hosting

Static hosting is the best option for most deployments. You serve pre-built files through a CDN or web server.

Nginx Configuration

Create /etc/nginx/sites-available/tmi-ux:

server {
    listen 443 ssl http2;
    server_name tmi.example.com;

    ssl_certificate /etc/ssl/certs/tmi.crt;
    ssl_private_key /etc/ssl/private/tmi.key;

    root /var/www/tmi-ux;
    index index.html;

    # Enable gzip compression
    gzip on;
    gzip_vary on;
    gzip_types text/plain text/css text/xml text/javascript
               application/x-javascript application/xml+rss
               application/javascript application/json;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Angular routing - serve index.html for all routes
    location / {
        try_files $uri $uri/ /index.html;
    }

    # API proxy (optional - if not using a separate API domain)
    location /api/ {
        proxy_pass http://localhost:8080/;
        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;

        # WebSocket support
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

# HTTP to HTTPS redirect
server {
    listen 80;
    server_name tmi.example.com;
    return 301 https://$server_name$request_uri;
}

Deploy to Nginx

# Copy build files
sudo mkdir -p /var/www/tmi-ux
sudo cp -r dist/tmi-ux/browser/* /var/www/tmi-ux/

# Set permissions
sudo chown -R www-data:www-data /var/www/tmi-ux

# Enable the site
sudo ln -s /etc/nginx/sites-available/tmi-ux /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Apache Configuration

For Apache, create /etc/apache2/sites-available/tmi-ux.conf:

<VirtualHost *:443>
    ServerName tmi.example.com
    DocumentRoot /var/www/tmi-ux

    SSLEngine on
    SSLCertificateFile /etc/ssl/certs/tmi.crt
    SSLCertificateKeyFile /etc/ssl/private/tmi.key

    <Directory /var/www/tmi-ux>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted

        # Angular routing
        RewriteEngine On
        RewriteBase /
        RewriteRule ^index\.html$ - [L]
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule . /index.html [L]
    </Directory>

    # Enable compression
    <IfModule mod_deflate.c>
        AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
        AddOutputFilterByType DEFLATE application/javascript application/json
    </IfModule>

    # Cache static assets
    <IfModule mod_expires.c>
        ExpiresActive On
        ExpiresByType image/* "access plus 1 year"
        ExpiresByType text/css "access plus 1 year"
        ExpiresByType application/javascript "access plus 1 year"
    </IfModule>
</VirtualHost>

<VirtualHost *:80>
    ServerName tmi.example.com
    Redirect permanent / https://tmi.example.com/
</VirtualHost>

Enable the required modules and restart:

sudo a2enmod rewrite ssl deflate expires headers
sudo a2ensite tmi-ux
sudo apache2ctl configtest
sudo systemctl reload apache2

CDN Deployment

AWS S3 + CloudFront

# Create S3 bucket
aws s3 mb s3://tmi-ux-prod

# Upload files with long-lived cache headers
aws s3 sync dist/tmi-ux/browser/ s3://tmi-ux-prod/ \
  --delete \
  --cache-control "public, max-age=31536000, immutable" \
  --exclude "index.html"

# Upload index.html without cache (so users always get the latest version)
aws s3 cp dist/tmi-ux/browser/index.html s3://tmi-ux-prod/ \
  --cache-control "no-cache, no-store, must-revalidate"

# Configure CloudFront distribution:
# - Origin: S3 bucket
# - Default root object: index.html
# - Error pages: 403, 404 -> /index.html (for Angular routing)
# - SSL certificate: ACM certificate
# - Compress objects automatically: Yes

Netlify

# Install Netlify CLI
npm install -g netlify-cli

# Deploy
pnpm run build:prod
netlify deploy --prod --dir=dist/tmi-ux/browser

Create netlify.toml:

[build]
  publish = "dist/tmi-ux/browser"
  command = "pnpm run build:prod"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

[[headers]]
  for = "/*.js"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[headers]]
  for = "/*.css"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

Vercel

# Install Vercel CLI
npm install -g vercel

# Deploy
pnpm run build:prod
vercel --prod

Create vercel.json:

{
  "version": 2,
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/static-build",
      "config": {
        "distDir": "dist/tmi-ux/browser"
      }
    }
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "/index.html"
    }
  ]
}

Node.js Server

TMI-UX includes a production-ready Express server (server.js) that serves the built Angular application and provides runtime configuration overrides.

Production Server

# Build the application
pnpm run build:prod

# Start the production server (uses server.js via the package.json "start" script)
pnpm start

The included server.js is an ES module that:

  • Serves the built Angular app from dist/tmi-ux/browser/
  • Provides a /config.json endpoint for runtime configuration overrides through TMI_* environment variables (see Configuration-Reference)
  • Applies rate limiting (1,000 requests per 15-minute window per IP)
  • Falls back to index.html for all unmatched routes (SPA routing)
  • Defaults to port 8080 (configurable through the PORT environment variable)

Systemd Service

Create /etc/systemd/system/tmi-ux.service:

[Unit]
Description=TMI Web Application
After=network.target

[Service]
Type=simple
User=tmi
Group=tmi
WorkingDirectory=/opt/tmi-ux
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=5

# Environment
Environment=NODE_ENV=production
Environment=PORT=8080

# Security
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Start the service:

sudo systemctl daemon-reload
sudo systemctl enable tmi-ux
sudo systemctl start tmi-ux

Docker Deployment

Use Docker for containerized deployment with consistent behavior across environments.

Dockerfiles

The repository includes several Dockerfiles for different deployment targets:

File Base Image Target
Dockerfile node:22-alpine Google Cloud Run / general container
Dockerfile.oci Oracle Linux 9 Oracle Cloud Infrastructure
Dockerfile.chainguard Chainguard Hardened container image

All Dockerfiles use a multi-stage build: the first stage builds the Angular app, and the second stage runs the Express server.js (not nginx). This means runtime configuration through TMI_* environment variables is supported in all container deployments.

The default Dockerfile looks like this:

# Build stage
FROM --platform=$BUILDPLATFORM node:22-alpine AS builder
WORKDIR /app

# Install pnpm
RUN npm install -g pnpm@10.18.3

# Copy package files
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# Copy source and build
COPY . .
RUN pnpm run build:hosted-container

# Production stage
FROM node:22-alpine
WORKDIR /app

# Create minimal package.json for runtime dependencies only
RUN echo '{"name":"tmi-ux-server","version":"1.0.0","type":"module","dependencies":{"express":"^5.1.0","express-rate-limit":"^8.1.0"}}' > package.json
RUN npm install --omit=dev --production

# Copy built application and server
COPY --from=builder /app/dist ./dist
COPY server.js ./

EXPOSE 8080
ENV PORT=8080
ENV NODE_ENV=production

CMD ["server.js"]

Build and Run

# Build the image
docker build -t tmi-ux:latest .

# Run the container
docker run -d \
  --name tmi-ux \
  -p 8080:8080 \
  tmi-ux:latest

# Run with runtime configuration overrides
docker run -d \
  --name tmi-ux \
  -p 8080:8080 \
  -e TMI_API_URL=https://api.tmi.example.com \
  -e TMI_OPERATOR_NAME="My Organization" \
  -e TMI_OPERATOR_CONTACT="support@example.com" \
  tmi-ux:latest

Docker Compose

Add TMI-UX to an existing docker-compose.yml:

services:
  tmi-ux:
    build:
      context: ../tmi-ux
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - TMI_API_URL=http://tmi:8080
      - TMI_OPERATOR_NAME=My Organization
    restart: unless-stopped
    depends_on:
      - tmi

Heroku Deployment

You can deploy TMI-UX to Heroku using the included Express server and pnpm buildpack.

Heroku Prerequisites

  • Heroku CLI installed and authenticated
  • Access to a Heroku app configured for TMI-UX

How It Works

Angular applications require environment variables to be baked into the build at compile time (they cannot read runtime environment variables directly). TMI-UX uses a hosted-container-specific environment file that contains all configuration:

  1. Environment file: src/environments/environment.hosted-container.ts contains the Heroku configuration.
  2. Build configuration: Angular's hosted-container build configuration uses this environment file.
  3. Server start: The Express server (server.js) serves the built Angular app.

Deploy to Heroku

git push heroku main

This triggers the following steps:

  1. Heroku installs dependencies using pnpm (through the buildpack).
  2. The heroku-postbuild script builds Angular with --configuration=hosted-container.
  3. Heroku starts the Express server through npm start.

Note: Heroku sets the PORT environment variable automatically. Do not configure it manually.

Verify Deployment

Check the app status:

heroku logs --tail --app tmi-ux

Visit the app:

heroku open --app tmi-ux

Buildpacks

The app uses two buildpacks, in this order:

  1. heroku/nodejs -- Node.js support
  2. https://github.com/unfold/heroku-buildpack-pnpm -- pnpm package manager

View the configured buildpacks:

heroku buildpacks --app tmi-ux

Modifying Configuration

To change the Heroku environment configuration:

  1. Edit src/environments/environment.hosted-container.ts directly, or run the configuration script to regenerate the file:
    bash scripts/configure-heroku-env.sh
  2. Commit the changes:
    git add src/environments/environment.hosted-container.ts
    git commit -m "Update Heroku configuration"
  3. Deploy:
    git push heroku main

Files Involved

File Purpose
scripts/configure-heroku-env.sh Script to generate environment.hosted-container.ts
src/environments/environment.hosted-container.ts Heroku environment config (committed to repo)
angular.json Contains the hosted-container build configuration
Procfile Tells Heroku how to run the app (web: npm start)
package.json Contains heroku-postbuild and build:hosted-container scripts
server.js Express server that serves the Angular app

Heroku Troubleshooting

Build Failures

Check the build logs:

heroku logs --tail --app tmi-ux

Common causes:

  • Missing buildpack: Ensure the pnpm buildpack is added.
  • Build timeout: Check for large dependencies that slow the build.

Server Fails to Start

The server uses process.env.PORT, which Heroku sets automatically. If the server fails to start, verify:

  • Procfile contains: web: npm start
  • server.js uses: const port = process.env.PORT || 8080

Architecture Note

Unlike typical server applications that read environment variables at runtime, Angular applications require environment configuration at build time. Configuration values are compiled directly into the JavaScript bundle.

The environment.hosted-container.ts file is committed to the repository and contains the configuration for Heroku deployments. Because it contains only public API URLs and operator information (no secrets), it is safe to commit.

Development Server

Use the development server for local development and testing.

Quick Start

# Install dependencies
pnpm install

# Start the development server (opens the browser automatically)
pnpm run dev

# Application available at http://localhost:4200

Environment-Specific Development

# Use a specific environment file
pnpm run dev:test         # environment.test.ts
pnpm run dev:prod         # environment.prod.ts

The dev command uses the development configuration by default, which loads environment.dev.ts. Additional build configurations (staging, local, shannon) are defined in angular.json and can be served with ng serve --configuration=<name>.

Configuration

Environment Settings

The following table lists the available environment settings. For the full configuration reference, including runtime overrides through TMI_* environment variables, see Configuration-Reference.

Setting Description Default
production Enable production mode false
logLevel Logging verbosity 'ERROR'
apiUrl API server URL 'http://localhost:8080'
authTokenExpiryMinutes Token validity in minutes 60
operatorName Service operator name 'TMI Operator'
operatorContact Contact information 'contact@example.com'
serverPort Server listening port 4200
serverInterface Server listening interface '0.0.0.0'
enableTLS Enable HTTPS false
tlsKeyPath TLS private key path undefined
tlsCertPath TLS certificate path undefined

Creating a Custom Environment

  1. Copy the example environment:

    cp src/environments/environment.example.ts src/environments/environment.custom.ts
  2. Edit the configuration:

    export const environment = {
      production: true,
      apiUrl: 'https://api.tmi.example.com',
      logLevel: 'ERROR',
      // ... other settings
    };
  3. Update angular.json if needed, or use an existing configuration.

API URL Configuration

Set apiUrl to point to your TMI API server:

// Development (local API server)
apiUrl: 'http://localhost:8080'

// Production (separate API domain)
apiUrl: 'https://api.tmi.example.com'

// Production (same domain, different path)
apiUrl: 'https://tmi.example.com/api'

Testing the Deployment

Smoke Tests

# Check if the app loads
curl -I https://tmi.example.com

# Check static assets
curl -I https://tmi.example.com/main.js

# Test Angular routing (should return index.html with 200 OK)
curl https://tmi.example.com/threat-models

Integration Tests

# Run end-to-end tests against the deployed app
pnpm run e2e --base-url=https://tmi.example.com

Browser Console Checks

  1. Open DevTools Console.
  2. Verify that no errors appear on page load.
  3. Check the API URL in the Network tab.
  4. Verify WebSocket connections if you are using collaboration features.

Performance Optimization

Build Optimization

# Production build with optimization
pnpm run build:prod

# Check bundle sizes
ls -lh dist/tmi-ux/*.js

Lazy Loading

Angular modules are already configured for lazy loading. To monitor chunk sizes:

# Analyze the bundle
pnpm run build:prod --stats-json
npx webpack-bundle-analyzer dist/tmi-ux/stats.json

CDN Configuration

When deploying behind a CDN, follow these recommendations:

  • Enable compression (gzip or Brotli)
  • Set cache headers: 1 year for hashed assets, no-cache for index.html
  • Enable HTTP/2
  • Configure a custom 404 page to serve index.html (required for Angular routing)

Troubleshooting

Blank Page on Load

Check the browser console for errors:

  • API connection failed: Verify apiUrl in your environment configuration.
  • 404 on assets: Check your server routing configuration.
  • CORS errors: Configure CORS headers on your API server.

OAuth Redirect Fails

Verify your OAuth configuration:

  1. Confirm the redirect URI matches exactly (watch for trailing slash mismatches).
  2. Confirm the OAuth application has the correct callback URL.
  3. Confirm the Client ID matches your environment configuration.

For details, see Setting-Up-Authentication.

API Calls Fail

Check the Network tab in your browser's developer tools:

// Test API connectivity from the browser console
fetch('https://api.tmi.example.com/version')
  .then(r => r.json())
  .then(console.log)

Common causes:

  • CORS is not configured on the API server.
  • The API URL is incorrect in your environment configuration.
  • A network firewall is blocking requests.

Angular Routing Not Working

This is a server configuration issue. Verify that your server falls back to index.html for unknown routes:

  • Nginx: Check that try_files $uri $uri/ /index.html is present.
  • Apache: Enable mod_rewrite and add the .htaccess rules.
  • S3/CloudFront: Configure error pages to serve index.html.

Security Considerations

For a comprehensive treatment, see Security-Best-Practices. The following are the key security measures for web application deployments:

  1. Content Security Policy: Configure CSP headers to restrict resource loading.
  2. HTTPS only: Never serve the application in production without TLS.
  3. Security headers: Enable X-Frame-Options, X-Content-Type-Options, and related headers.
  4. Dependency updates: Regularly update npm packages.
  5. Environment secrets: Never commit API keys or secrets to version control.
  6. Subresource Integrity: Consider using SRI for CDN-hosted resources.

Updating a Deployment

Rolling Update Process

  1. Build the new version:

    pnpm run build:prod
  2. Back up the current deployment:

    sudo cp -r /var/www/tmi-ux /var/www/tmi-ux.backup
  3. Deploy the new version:

    sudo cp -r dist/tmi-ux/browser/* /var/www/tmi-ux/
  4. Clear your CDN cache (if applicable).

  5. Test the deployment.

  6. Roll back if needed:

    sudo rm -rf /var/www/tmi-ux
    sudo mv /var/www/tmi-ux.backup /var/www/tmi-ux

Zero-Downtime Deployment

Use a blue-green deployment strategy:

  1. Deploy to a new directory (for example, /var/www/tmi-ux-v2).
  2. Update the nginx configuration to point to the new directory.
  3. Test the new deployment.
  4. Reload nginx: sudo systemctl reload nginx
  5. Remove the old deployment after verification.

Next Steps

Related Pages

Home

Releases


Getting Started

Deployment

Operation

Troubleshooting

Development

Integrations

Tools

API Reference

Reference

Clone this wiki locally