# Deploy MapleFile Frontend on Separate Dedicated Server

**Audience**: Junior DevOps Engineers, Infrastructure Team
**Time to Complete**: 60-75 minutes
**Prerequisites**:
- ✅ Completed guides 09-10 (Backend and Caddy deployed on worker-8)
- ✅ Domain name `maplefile.com` registered
- ✅ Email address for Let's Encrypt SSL certificate notifications
- ✅ Access to DigitalOcean account to create new droplet

---

## Overview

This guide sets up **worker-9** from scratch and deploys the MapleFile React frontend with its own dedicated Caddy reverse proxy serving static files.

**What you'll build:**
- Fresh worker-9 droplet ($6/month - 1GB RAM, 1 vCPU)
- Worker-9 joined to existing Docker Swarm
- Git repository cloned with automatic frontend builds
- Dedicated Caddy serving React build with automatic HTTPS
- Separate frontend domain: `maplefile.com` (frontend) vs `maplefile.ca` (backend API)

**Architecture:**
```
Internet (HTTPS)
    ├─ maplefile.ca (Backend API)
    │   ↓
    │   Caddy (worker-8) → Backend Service
    │
    └─ maplefile.com (Frontend) **THIS GUIDE**
        ↓
        Caddy (worker-9) → React Static Files
                           └─ Built from git repo
```

**Why separate worker?**
- **Isolation**: Frontend and backend failures don't affect each other
- **Independent scaling**: Scale frontend independently based on traffic
- **Resource optimization**: Frontend is lightweight - $6/mo droplet is sufficient
- **Simpler SSL**: Dedicated Caddy on worker-9 manages frontend certificates only
- **Standard ports**: Both use 80/443 without conflicts

**Difference from guide 11:**
- **Guide 11**: Deploys frontend to EXISTING worker-8 (shared server, single Caddy)
- **This guide**: Deploys frontend to NEW worker-9 (dedicated server, separate Caddy)

Choose this approach if you want better isolation and independent scaling.

---

## Table of Contents

1. [Create Worker-9 Droplet](#step-1-create-worker-9-droplet)
2. [Initial Server Setup](#step-2-initial-server-setup)
3. [Configure Firewall](#step-3-configure-firewall)
4. [Join Worker-9 to Docker Swarm](#step-4-join-worker-9-to-docker-swarm)
5. [Configure DNS](#step-5-configure-dns)
6. [Clone Repository and Setup Build](#step-6-clone-repository-and-setup-build)
7. [Deploy Caddy for Frontend](#step-7-deploy-caddy-for-frontend)
8. [Verify Deployment](#step-8-verify-deployment)
9. [Update Process](#step-9-update-process)
10. [Troubleshooting](#troubleshooting)

---

## Step 1: Create Worker-9 Droplet

### 1.1 Create Droplet in DigitalOcean

1. Log into DigitalOcean dashboard
2. Click **Create** → **Droplets**
3. Configure:
   - **Region**: Same as your other workers (e.g., New York 3)
   - **Image**: Ubuntu 24.04 LTS x64
   - **Size**: Basic shared CPU, 1 GB / 1 vCPU ($6/mo) - Frontend is lightweight
   - **Hostname**: `mapleopentech-swarm-worker-9-prod`
   - **VPC Network**: Select same VPC as your swarm (mapleopentech-vpc-prod)
   - **SSH Keys**: Add your SSH key
   - **Tags**: Add tags like `production`, `maplefile`, `frontend`
4. Click **Create Droplet**
5. Wait 1-2 minutes for droplet to provision

**Cost**: $6/month ($0.009/hour) - cheapest option for static file serving

### 1.2 Record IP Addresses

Once created, copy both IPs from DigitalOcean dashboard:
- **Public IPv4**: `<worker-9-public-ip>` (e.g., 159.65.XXX.XXX)
- **Private IPv4**: `<worker-9-private-ip>` (e.g., 10.116.0.X)

**Update your local notes:**

```bash
# On your local machine - save these for reference
Worker-9 Public IP: 159.65.XXX.XXX
Worker-9 Private IP: 10.116.0.X
```

**✅ Checkpoint:** You should be able to ping worker-9:

```bash
ping <worker-9-public-ip>
# Should get responses
```

---

## Step 2: Initial Server Setup

### 2.1 SSH to Worker-9

```bash
# From your local machine
ssh root@<worker-9-public-ip>

# Should connect successfully
```

### 2.2 Update System Packages

```bash
# Update package lists
apt update

# Upgrade all packages
apt upgrade -y

# Install essential tools
apt install -y curl wget git vim apt-transport-https ca-certificates gnupg lsb-release software-properties-common

# Reboot if kernel was updated (check if /var/run/reboot-required exists)
if [ -f /var/run/reboot-required ]; then
    echo "Reboot required - rebooting in 5 seconds..."
    sleep 5
    reboot
fi
```

This takes 3-7 minutes. If system reboots, wait 30 seconds then reconnect:

```bash
ssh root@<worker-9-public-ip>
```

### 2.3 Install Docker

```bash
# Add Docker GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# Add Docker repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

# Update package list with Docker packages
apt update

# Install Docker
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# Verify installation
docker --version
# Should show: Docker version 27.x.x or higher

# Check Docker is running
docker ps
# Should show empty list (not error)
```

**✅ Checkpoint:** `docker --version` shows version 27+

### 2.4 Create dockeradmin User

```bash
# Create user
adduser dockeradmin
# Enter password when prompted
# SAVE THIS PASSWORD IN YOUR PASSWORD MANAGER!

# Add to sudo group (can run admin commands)
usermod -aG sudo dockeradmin

# Add to docker group (can run docker commands)
usermod -aG docker dockeradmin

# Copy SSH keys to new user
rsync --archive --chown=dockeradmin:dockeradmin ~/.ssh /home/dockeradmin

# Verify SSH keys copied
ls -la /home/dockeradmin/.ssh/
# Should show: authorized_keys
```

### 2.5 Test dockeradmin Access

**From your local machine (open new terminal):**

```bash
# Test SSH login
ssh dockeradmin@<worker-9-public-ip>

# Should login WITHOUT password prompt (using SSH key)

# Test docker access
docker ps
# Should show empty list (NOT permission denied)

# Test sudo access
sudo ls /root
# Enter dockeradmin password when prompted
# Should list root directory contents

# Exit back to local machine
exit
```

**✅ Checkpoint:** Can SSH as dockeradmin without password, run docker commands

---

## Step 3: Configure Firewall

**SSH to worker-9 as root:**

```bash
ssh root@<worker-9-public-ip>
```

### 3.1 Setup UFW Firewall

```bash
# Enable firewall (force to avoid prompt)
ufw --force enable

# Allow SSH (CRITICAL - do this first!)
ufw allow 22/tcp

# Allow HTTP and HTTPS (for Caddy)
ufw allow 80/tcp
ufw allow 443/tcp

# Allow Docker Swarm (only from private VPC network)
ufw allow from 10.116.0.0/16 to any port 2377 proto tcp  # Swarm management
ufw allow from 10.116.0.0/16 to any port 7946            # Node communication
ufw allow from 10.116.0.0/16 to any port 4789 proto udp  # Overlay network

# Check firewall status
ufw status verbose
```

**Expected output:**

```
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
2377/tcp                   ALLOW       10.116.0.0/16
7946                       ALLOW       10.116.0.0/16
4789/udp                   ALLOW       10.116.0.0/16
```

**✅ Checkpoint:** UFW active, ports open correctly

**Exit root session:**

```bash
exit
```

---

## Step 4: Join Worker-9 to Docker Swarm

### 4.1 Get Swarm Join Token

**From your local machine, SSH to manager:**

```bash
ssh dockeradmin@143.110.210.162

# Get worker join token
docker swarm join-token worker
```

**Copy the entire output command.** It looks like:

```bash
docker swarm join --token SWMTKN-1-xxxxxx... 10.116.0.2:2377
```

**Important:** Use the **private IP** shown in the command (e.g., `10.116.0.2:2377`), NOT the public IP.

### 4.2 Join Worker-9 to Swarm

**SSH to worker-9 as dockeradmin:**

```bash
# From your local machine
ssh dockeradmin@<worker-9-public-ip>

# Paste the join command from manager
docker swarm join --token SWMTKN-1-xxxxxx... <manager-private-ip>:2377
```

**Expected output:**

```
This node joined a swarm as a worker.
```

### 4.3 Verify Worker-9 Joined

**Back on manager:**

```bash
# List all nodes
docker node ls
```

**You should see:**

```
ID                            HOSTNAME                            STATUS    AVAILABILITY   MANAGER STATUS
abc123...                     mapleopentech-swarm-manager-1-prod      Ready     Active         Leader
def456...                     mapleopentech-swarm-worker-8-prod       Ready     Active
xyz789...                     mapleopentech-swarm-worker-9-prod       Ready     Active         <-- NEW!
```

**✅ Checkpoint:** Worker-9 appears in `docker node ls` with status `Ready Active`

### 4.4 Label Worker-9 for MapleFile Frontend

**On manager:**

```bash
# Add MapleFile frontend label (tells swarm to deploy MapleFile frontend here)
docker node update --label-add maplefile-frontend=true mapleopentech-swarm-worker-9-prod

# Verify label
docker node inspect mapleopentech-swarm-worker-9-prod --format '{{.Spec.Labels}}'
# Should show: map[maplefile-frontend:true]
```

**✅ Checkpoint:** Worker-9 has `maplefile-frontend=true` label

**Exit manager session:**

```bash
exit
```

---

## Step 5: Configure DNS

### 5.1 Update DNS Records

1. Log into your domain registrar (where you bought maplefile.com)
2. Find DNS settings / DNS records
3. Add/update these A records:

| Type | Name | Value | TTL |
|------|------|-------|-----|
| A | @ | `<worker-9-public-ip>` | 3600 |
| A | www | `<worker-9-public-ip>` | 3600 |

**Replace `<worker-9-public-ip>` with your actual worker-9 public IP!**

**Example:**
- **@ record**: Points `maplefile.com` → 159.65.XXX.XXX
- **www record**: Points `www.maplefile.com` → 159.65.XXX.XXX

**Note:** This is a DIFFERENT IP than your backend domain (`maplefile.ca` points to worker-8).

### 5.2 Wait for DNS Propagation

DNS changes take 5-10 minutes (sometimes up to 1 hour).

**Test from your local machine:**

```bash
# Test root domain
dig maplefile.com +short
# Should return: <worker-9-public-ip>

# Test www subdomain
dig www.maplefile.com +short
# Should return: <worker-9-public-ip>

# Verify different from backend
dig maplefile.ca +short
# Should return: 143.110.212.253 (worker-8 - DIFFERENT!)
```

**⚠️ Don't proceed until both frontend domains return worker-9's IP!**

**✅ Checkpoint:** DNS resolves to worker-9 public IP (different from backend)

---

## Step 6: Clone Repository and Setup Build

### 6.1 Install Node.js on Worker-9

**SSH to worker-9 as dockeradmin:**

```bash
ssh dockeradmin@<worker-9-public-ip>

# Install Node.js 20 LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Verify installation
node --version
# Should show: v20.x.x

npm --version
# Should show: 10.x.x or 11.x.x
```

**Note:** You may see a notice about npm update available - that's normal and safe to ignore or optionally update:

```bash
# Optional: Update npm to latest
sudo npm install -g npm@latest
```

### 6.2 Clone Repository

```bash
# Create directory for web files
sudo mkdir -p /var/www
sudo chown dockeradmin:dockeradmin /var/www

# Clone the repository
cd /var/www
git clone https://codeberg.org/mapleopentech/monorepo.git

# Navigate to frontend
cd monorepo/web/maplefile-frontend

# Check you're on the correct branch
git branch
# Should show: * main (or your production branch)
```

### 6.3 Configure Production Environment

**Create production environment file:**

```bash
# Create .env.production with production API URL
cat > .env.production << 'EOF'
# MapleFile Frontend - Production Environment

# Backend API URL (production)
VITE_API_BASE_URL=https://maplefile.ca

# Node environment (set by Vite automatically)
NODE_ENV=production
EOF

# Verify file created
cat .env.production
```

**Understanding environment variables:**
- **`VITE_API_BASE_URL`**: Backend API endpoint on worker-8 (used by frontend to make API calls)
- Frontend on worker-9 will make HTTPS calls to backend on worker-8
- Vite reads `.env.production` when building for production
- Without this file, frontend uses development default (`http://localhost:8000`)

**Note:** The `NODE_ENV=production` line will show a warning during build - this is expected and harmless.

### 6.4 Build React Frontend

```bash
# Install dependencies
npm install

# Build for production (uses .env.production)
npm run build

# Verify build succeeded
ls -la dist/
# Should show: index.html, assets/, etc.

# Verify production API URL is in build
grep -r "maplefile.ca" dist/assets/*.js | head -2
# Should show: maplefile.ca in compiled JavaScript
```

**Expected output:**

```
dist/
├── index.html
├── assets/
│   ├── index-abc123.js
│   └── index-xyz789.css
└── favicon.ico
```

**Expected warnings during build:**
- ⚠️ `NODE_ENV=production is not supported in the .env file` - **Safe to ignore**
- ⚠️ `Browserslist: browsers data is 6 months old` - **Safe to ignore**
- ⚠️ PostCSS warnings about @import - **Safe to ignore**
- ⚠️ Dynamic import warnings - **Safe to ignore**
- ⚠️ Chunk size warnings - **Safe to ignore** (or optimize later)

**⚠️ Important:** If you see `localhost:8000` instead of `maplefile.ca`, rebuild after creating `.env.production`.

### 6.5 Create Symlink for Caddy

```bash
# Create symlink to simplify Caddy config
ln -s /var/www/monorepo/web/maplefile-frontend/dist /var/www/maplefile-frontend

# Verify symlink
ls -la /var/www/maplefile-frontend
# Should point to: /var/www/monorepo/web/maplefile-frontend/dist

# Verify contents
ls /var/www/maplefile-frontend/
# Should show: index.html, assets/, etc.

# Exit back to local machine
exit
```

**✅ Checkpoint:** React app built successfully at `/var/www/maplefile-frontend` on worker-9

---

## Step 7: Deploy Caddy for Frontend

### 7.1 Create Caddyfile on Manager

**SSH to manager:**

```bash
# From your local machine
ssh dockeradmin@143.110.210.162

# Create directory for frontend Caddy config
cd ~/stacks
mkdir -p maplefile-frontend-caddy-config
cd maplefile-frontend-caddy-config
```

### 7.2 Create Caddyfile

```bash
vi Caddyfile
```

**Paste this configuration:**

```caddy
{
    # Global options
    email bart@mikasoftware.com

    # Use Let's Encrypt production
    acme_ca https://acme-v02.api.letsencrypt.org/directory
}

# Frontend domain configuration
maplefile.com www.maplefile.com {
    # Root directory for static files
    root * /var/www/maplefile-frontend

    # Enable file server
    file_server

    # SPA routing - serve index.html for all non-file routes
    try_files {path} /index.html

    # Gzip compression
    encode gzip

    # Logging
    log {
        output stdout
        format json
        level INFO
    }

    # Security headers
    header {
        # Prevent clickjacking
        X-Frame-Options "SAMEORIGIN"
        # Prevent MIME type sniffing
        X-Content-Type-Options "nosniff"
        # Enable XSS protection
        X-XSS-Protection "1; mode=block"
        # HSTS - Force HTTPS for 1 year
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        # Control referrer information
        Referrer-Policy "strict-origin-when-cross-origin"
        # Remove Server header
        -Server
    }

    # Cache static assets (JS, CSS, images, fonts)
    @static {
        path *.js *.css *.png *.jpg *.jpeg *.gif *.svg *.woff *.woff2 *.ttf *.eot *.ico
    }
    header @static Cache-Control "public, max-age=31536000, immutable"
}
```

**Important replacements:**
1. Replace `bart@mikasoftware.com` with your real email (Let's Encrypt notifications)
2. Domain names are already set correctly

Save: `Esc`, `:wq`, `Enter`

**Understanding the config:**
- **`root * /var/www/maplefile-frontend`**: Serves files from our build directory
- **`file_server`**: Enables static file serving
- **`try_files {path} /index.html`**: SPA routing - all routes serve index.html (critical for React Router)
- **`encode gzip`**: Compresses responses for faster load times
- **Static asset caching**: JS/CSS/images cached for 1 year (good for hashed filenames)

### 7.3 Create Frontend Stack File

**On manager:**

```bash
cd ~/stacks
vi maplefile-frontend-stack.yml
```

**Paste this:**

```yaml
version: '3.8'

volumes:
  maplefile_frontend_caddy_data:
    # SSL certificates for maplefile.com
  maplefile_frontend_caddy_config:
    # Caddy config cache

configs:
  frontend_caddyfile:
    file: ./maplefile-frontend-caddy-config/Caddyfile

services:
  frontend-caddy:
    image: caddy:2.9.1-alpine
    hostname: maplefile-frontend-caddy
    ports:
      # Port 80 - HTTP (for Let's Encrypt challenges and HTTP→HTTPS redirect)
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      # Port 443 - HTTPS (encrypted traffic)
      - target: 443
        published: 443
        protocol: tcp
        mode: host
      # Port 443 UDP - HTTP/3 support
      - target: 443
        published: 443
        protocol: udp
        mode: host
    configs:
      - source: frontend_caddyfile
        target: /etc/caddy/Caddyfile
    volumes:
      - maplefile_frontend_caddy_data:/data
      - maplefile_frontend_caddy_config:/config
      - /var/www/maplefile-frontend:/var/www/maplefile-frontend:ro
    deploy:
      replicas: 1
      placement:
        constraints:
          # Deploy on worker-9 only
          - node.labels.maplefile-frontend == true
      restart_policy:
        condition: on-failure
        delay: 5s
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
      resources:
        limits:
          memory: 256M
        reservations:
          memory: 128M
    # Note: No healthcheck - prevents SIGTERM restart loops during cert renewal
```

Save: `Esc`, `:wq`, `Enter`

**Key configuration:**
- **Standard ports**: 80/443 (no conflicts since this is a dedicated server)
- **Placement constraint**: Only deploys to worker-9 (maplefile-frontend=true label)
- **Read-only volume**: Frontend files mounted as `:ro` for security
- **Resource limits**: 256MB max (frontend is lightweight)

### 7.4 Verify Prerequisites

```bash
# On manager

# 1. Check worker-9 has frontend label
docker node inspect mapleopentech-swarm-worker-9-prod --format '{{.Spec.Labels}}'
# Should show: map[maplefile-frontend:true]

# 2. Test DNS resolution
dig maplefile.com +short
# Should return: <worker-9-public-ip>
```

### 7.5 Deploy Frontend Stack

```bash
# Make sure you're in the stacks directory
cd ~/stacks

# Deploy Caddy for frontend
docker stack deploy -c maplefile-frontend-stack.yml maplefile-frontend

# Expected output:
# Creating network maplefile-frontend_default
# Creating config maplefile-frontend_frontend_caddyfile
# Creating service maplefile-frontend_frontend-caddy
```

### 7.6 Watch Caddy Start

```bash
# Watch service come up
docker service ls | grep frontend

# Expected output (within 30 seconds):
# maplefile-frontend_frontend-caddy   replicated   1/1

# Follow logs to see certificate acquisition
docker service logs -f maplefile-frontend_frontend-caddy

# You'll see:
# - "serving initial configuration"
# - "enabling automatic TLS certificate management"
# - "certificate obtained successfully" (for maplefile.com and www.maplefile.com)

# Press Ctrl+C when done watching
```

**Expected certificate messages:**

```json
{"level":"info","msg":"certificate obtained successfully","identifier":"maplefile.com"}
{"level":"info","msg":"certificate obtained successfully","identifier":"www.maplefile.com"}
```

**✅ Checkpoint:** Service shows `1/1` replicas running, certificates obtained

---

## Step 8: Verify Deployment

### 8.1 Test HTTP Redirect

```bash
# From your local machine
curl -I http://maplefile.com

# Should show:
# HTTP/1.1 308 Permanent Redirect
# Location: https://maplefile.com/
```

### 8.2 Test HTTPS Access

```bash
# Test HTTPS
curl -I https://maplefile.com

# Should show:
# HTTP/2 200
# content-type: text/html
```

### 8.3 Verify Production API URL in Build

```bash
# Fetch a JavaScript file and check for production API URL
curl -s https://maplefile.com/assets/*.js | grep -o "https://maplefile.ca" | head -3

# Should show:
# https://maplefile.ca
# https://maplefile.ca
# https://maplefile.ca
```

### 8.4 Verify Version Information

```bash
# Check version endpoint
curl https://maplefile.com/version.json

# Should show version info with git hash and build time:
# {
#   "version": "0.0.0",
#   "gitHash": "6e60ccfe",
#   "gitBranch": "main",
#   "buildTime": "2025-11-18T02:50:38.657Z",
#   "nodeVersion": "v24.5.0"
# }
```

### 8.5 Test Backend Still Works

```bash
# Verify backend on worker-8 still accessible
curl -I https://maplefile.ca/health

# Should show:
# HTTP/2 200
# content-type: application/json
```

### 8.6 Test in Browser

**Test frontend:**
1. Open: `https://maplefile.com`
2. Should see: Your React app loading correctly
3. 🔒 Green padlock in address bar
4. Open Developer Console (F12)
5. **Version info should be logged** with styled output showing git hash and build time
6. Test version access: Type `window.__APP_VERSION__` in console
7. Check Network tab: Verify API calls go to `https://maplefile.ca` (worker-8)

**Test backend:**
1. Open: `https://maplefile.ca/health`
2. Should see: JSON health response
3. 🔒 Green padlock in address bar

**✅ Checkpoint:** Both frontend and backend load successfully with valid SSL on separate servers!

---

## Step 9: Update Process

When you make changes to your React frontend:

### 9.1 Update Frontend Code

**SSH to worker-9:**

```bash
ssh dockeradmin@<worker-9-public-ip>

# Navigate to repository
cd /var/www/monorepo

# Pull latest changes
git pull origin main

# Navigate to frontend
cd web/maplefile-frontend

# Note: .env.production is tracked in git - no need to create it manually

# Install any new dependencies
npm install

# Build updated frontend
npm run build
```

**That's it!** No service restart needed. Caddy serves the updated files immediately.

**Build Process Includes**:
- Automatic version generation (`version.json` with git hash, build time, etc.)
- Production environment configuration
- Optimized asset bundling

### 9.2 Automated Deployment Script

Create a deployment script on worker-9:

```bash
# On worker-9
vi ~/deploy-frontend.sh
```

**Paste:**

```bash
#!/bin/bash
set -e

echo "🚀 Deploying MapleFile Frontend..."

cd /var/www/monorepo
echo "📥 Pulling latest code..."
git pull origin main

cd web/maplefile-frontend

# Note: .env.production is now tracked in git with all required settings
# including VITE_PASSWORD_STORAGE_MODE=localStorage

echo "📦 Installing dependencies..."
npm install

echo "🏗️  Building frontend..."
npm run build

echo "✅ Verifying production API URL in build..."
if grep -q "maplefile.ca" dist/assets/*.js 2>/dev/null; then
    echo "✅ Production API URL confirmed in build"
else
    echo "⚠️  WARNING: Production API URL not found in build!"
    echo "   Build may be using development settings"
fi

echo "✅ Deployment complete!"
echo "🌐 Frontend: https://maplefile.com (worker-9)"
echo "🔌 Backend: https://maplefile.ca (worker-8)"
```

**Make executable:**

```bash
chmod +x ~/deploy-frontend.sh
```

**Use it:**

```bash
~/deploy-frontend.sh
```

### 9.3 Verify Deployment Version

After deployment, check the version information:

```bash
# Check version endpoint
curl https://maplefile.com/version.json

# Should show:
# {
#   "version": "0.0.0",
#   "gitHash": "6e60ccfe",
#   "gitBranch": "main",
#   "buildTime": "2025-11-18T02:50:38.657Z",
#   "nodeVersion": "v24.5.0"
# }
```

**In Browser:**
1. Open https://maplefile.com
2. Open Developer Console (F12)
3. Version info is logged automatically on page load
4. Or check: `window.__APP_VERSION__`

### 9.4 Update Caddy Configuration

If you need to update the Caddyfile:

```bash
# On manager
ssh dockeradmin@143.110.210.162

# Edit Caddyfile
cd ~/stacks/maplefile-frontend-caddy-config
vi Caddyfile

# Make your changes, then save

# Redeploy stack to apply changes
docker stack rm maplefile-frontend
sleep 10
docker stack deploy -c ~/stacks/maplefile-frontend-stack.yml maplefile-frontend

# Watch for successful restart
docker service logs -f maplefile-frontend_frontend-caddy
```

---

## Troubleshooting

### Problem: Caddy Won't Start

**Symptom:** `docker service ps maplefile-frontend_frontend-caddy` shows service restarting

**Check:**

```bash
# View logs for errors
docker service logs maplefile-frontend_frontend-caddy --tail 100

# Common issues:
# 1. Invalid Caddyfile syntax
# 2. Volume mount path doesn't exist
# 3. Port 80 or 443 already in use

# Check volume exists on worker-9
ssh dockeradmin@<worker-9-public-ip>
ls -la /var/www/maplefile-frontend
# Should show index.html
```

**Solution for invalid Caddyfile:**

```bash
# On manager
cd ~/stacks/maplefile-frontend-caddy-config
vi Caddyfile
# Fix syntax errors

# Redeploy
docker stack rm maplefile-frontend
sleep 10
docker stack deploy -c ~/stacks/maplefile-frontend-stack.yml maplefile-frontend
```

### Problem: Config Immutability Error

**Symptom:** `failed to update config: only updates to Labels are allowed`

**Solution:**

```bash
# Remove stack and config
docker stack rm maplefile-frontend
sleep 10
docker config rm maplefile-frontend_frontend_caddyfile

# Redeploy
cd ~/stacks
docker stack deploy -c maplefile-frontend-stack.yml maplefile-frontend
```

### Problem: 404 Not Found for React Routes

**Symptom:** Homepage loads but `/dashboard` returns 404

**Root Cause:** Missing `try_files {path} /index.html` in Caddyfile

**Fix:**

```bash
# On manager
cd ~/stacks/maplefile-frontend-caddy-config
vi Caddyfile

# Ensure this line exists in the maplefile.com block:
# try_files {path} /index.html

# Redeploy
docker stack rm maplefile-frontend
sleep 10
docker stack deploy -c ~/stacks/maplefile-frontend-stack.yml maplefile-frontend
```

### Problem: Frontend Calling localhost:8000

**Symptom:** Browser console shows:
```
Fetch API cannot load http://localhost:8000/health
```

**Root Cause:** Frontend built without `.env.production`

**Solution:**

```bash
# On worker-9
ssh dockeradmin@<worker-9-public-ip>
cd /var/www/monorepo/web/maplefile-frontend

# Create production environment file
cat > .env.production << 'EOF'
VITE_API_BASE_URL=https://maplefile.ca
NODE_ENV=production
EOF

# Rebuild
npm run build

# Verify
grep -r "maplefile.ca" dist/assets/*.js | head -2
```

### Problem: Certificate Acquisition Fails

**Check:**

```bash
# 1. Verify DNS
dig maplefile.com +short
# Should return worker-9 IP

# 2. Verify port 80 accessible
curl -I http://maplefile.com

# 3. Check firewall on worker-9
ssh dockeradmin@<worker-9-public-ip>
sudo ufw status | grep 80
```

**Solution:**

```bash
# On worker-9, ensure firewall allows port 80
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# On manager, retry certificate acquisition
docker service update --force maplefile-frontend_frontend-caddy
```

### Problem: CORS Errors

**Symptom:** Browser shows:
```
Access to fetch at 'https://maplefile.ca/api/...' blocked by CORS
```

**Root Cause:** Backend CORS config doesn't include frontend domain

**Solution:**

```bash
# On manager (worker-8's backend stack)
ssh dockeradmin@143.110.210.162
cat ~/stacks/maplefile-stack.yml | grep ALLOWED_ORIGINS

# Should include:
# - SECURITY_ALLOWED_ORIGINS=https://maplefile.com,https://www.maplefile.com

# If missing, update and redeploy backend
vi ~/stacks/maplefile-stack.yml
# Add frontend domains to ALLOWED_ORIGINS

docker stack deploy -c ~/stacks/maplefile-stack.yml maplefile
```

### Problem: Version Info Not Available

**Symptom:** `version.json` returns 404 or shows outdated information

**Root Cause:** Build didn't generate version file or old build cached

**Solution:**

```bash
# SSH to worker-9
ssh dockeradmin@<worker-9-public-ip>

# Rebuild frontend
cd /var/www/monorepo/web/maplefile-frontend
npm run build

# Verify version.json exists
ls -la public/version.json
cat public/version.json

# Should show current git hash and build time
```

### Problem: Version Shows "unknown" for Git Info

**Symptom:** `version.json` shows `"gitHash": "unknown"`

**Root Cause:** Git repository not available or git command failed

**Solution:**

```bash
# SSH to worker-9
ssh dockeradmin@<worker-9-public-ip>

# Verify git is installed
git --version

# Check repository status
cd /var/www/monorepo
git status

# Rebuild frontend
cd web/maplefile-frontend
npm run build
```

---

## Architecture Summary

**Final Setup:**

```
Worker-8 (143.110.212.253)
├── Backend Service (maplefile-backend:8000)
└── Backend Caddy
    ├── Listening on: 80, 443
    ├── SSL for: maplefile.ca, www.maplefile.ca
    └── Routing: maplefile.ca → maplefile-backend:8000

Worker-9 (<your-worker-9-ip>)
├── Frontend Files
│   └── /var/www/maplefile-frontend (symlink)
│       └── /var/www/monorepo/web/maplefile-frontend/dist
└── Frontend Caddy
    ├── Listening on: 80, 443
    ├── SSL for: maplefile.com, www.maplefile.com
    └── Routing: maplefile.com → /var/www/maplefile-frontend
```

**DNS Configuration:**

| Domain | Type | Points To | Server |
|--------|------|-----------|--------|
| maplefile.ca | A | 143.110.212.253 | Worker-8 (Backend) |
| www.maplefile.ca | A | 143.110.212.253 | Worker-8 (Backend) |
| maplefile.com | A | <worker-9-ip> | Worker-9 (Frontend) |
| www.maplefile.com | A | <worker-9-ip> | Worker-9 (Frontend) |

**Traffic Flow:**

1. **Frontend Request**: User visits `https://maplefile.com`
   - DNS → Worker-9 IP
   - Worker-9 Caddy receives on port 443
   - Serves static React files from `/var/www/maplefile-frontend`
   - React app loads in browser

2. **API Request from Frontend**: React app calls `https://maplefile.ca/api/users`
   - Browser makes HTTPS request to maplefile.ca
   - DNS → Worker-8 IP
   - Worker-8 Caddy receives on port 443
   - Reverse proxies to backend service
   - Backend processes and returns JSON
   - Response → Worker-8 Caddy → Browser

**Benefits of this architecture:**
- ✅ Independent scaling of frontend and backend
- ✅ Isolation - frontend failures don't affect backend
- ✅ Cost-effective - frontend on $6/mo droplet
- ✅ Simple SSL management - each Caddy manages its own certificates
- ✅ Standard ports on both servers (80/443)

---

## Cost Analysis

**Monthly costs:**
- Worker-8 (Backend): Existing infrastructure
- **Worker-9 (Frontend): $6/month** ($0.009/hour)

**Annual cost for frontend server**: $72/year

**When this architecture makes sense:**
- You want independent scaling
- You need better fault isolation
- You anticipate high frontend traffic
- You want to optimize each server independently

**When to use guide 11 instead (shared server):**
- Cost is primary concern
- Low traffic MVP/demo
- Simpler architecture preferred
- Single server is sufficient

---

## Next Steps

✅ **You now have:**
- Dedicated worker-9 serving frontend on its own server
- Separate Caddy instance with automatic HTTPS
- Independent scaling capability
- Isolated failure domains
- Git-based deployment workflow

**Maintenance Commands:**

```bash
# Update frontend
ssh dockeradmin@<worker-9-public-ip>
~/deploy-frontend.sh

# View frontend Caddy logs
docker service logs -f maplefile-frontend_frontend-caddy

# Restart frontend Caddy
docker service update --force maplefile-frontend_frontend-caddy

# Monitor frontend server
ssh dockeradmin@<worker-9-public-ip>
docker stats
df -h /var/www

# Scale frontend (add more replicas - requires load balancer)
docker service scale maplefile-frontend_frontend-caddy=2
```

**Future scaling options:**
- Add load balancer for multiple frontend replicas
- Set up CDN (Cloudflare) for static assets
- Implement caching layer (Varnish/Redis)
- Add monitoring (Prometheus/Grafana)

---

**Last Updated**: January 2025
**Maintained By**: Infrastructure Team

**Changelog:**
- January 2025: Alternative frontend deployment guide for dedicated server approach
