# Deploy MaplePress Frontend with Caddy

**Audience**: Junior DevOps Engineers, Infrastructure Team
**Time to Complete**: 45-60 minutes
**Prerequisites**:
- ✅ Completed guides 01-06 (Backend and backend Caddy deployed)
- ✅ Domain name `getmaplepress.com` registered
- ✅ Node.js 18+ installed on your local machine (for testing builds)
- ✅ Email address for Let's Encrypt SSL certificate notifications

---

## Overview

This guide sets up **worker-7** from scratch and deploys the MaplePress React frontend with Caddy reverse proxy serving static files.

**What you'll build:**
- Fresh worker-7 droplet with Docker and dockeradmin user
- Worker-7 joined to existing Docker Swarm
- Git repository cloned with automatic frontend builds
- Caddy serving React build with automatic HTTPS
- Independent domain: `getmaplepress.com` (separate from backend's `getmaplepress.ca`)

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

**Why separate worker and domain?**
- Frontend (`getmaplepress.com`) and backend (`getmaplepress.ca`) are separate domains
- Each domain needs its own SSL certificate
- Separate workers = better isolation and independent scaling
- Frontend can make API calls to backend via CORS or proxy

---
## Table of Contents

1. [Create Worker-7 Droplet](#step-1-create-worker-7-droplet)
2. [Initial Server Setup](#step-2-initial-server-setup)
3. [Configure Firewall](#step-3-configure-firewall)
4. [Join Worker-7 to Docker Swarm](#step-4-join-worker-7-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-7 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., Toronto)
   - **Image**: Ubuntu 24.04 LTS x64
   - **Size**: Basic shared CPU, 1 GB / 1 vCPU ($6/mo) - Frontend is lightweight
   - **Hostname**: `mapleopentech-swarm-worker-7-prod`
   - **VPC Network**: Select same VPC as your swarm (mapleopentech-vpc-prod)
   - **SSH Keys**: Add your SSH key
4. Click **Create Droplet**
5. Wait 1-2 minutes for droplet to provision

### 1.2 Record IP Addresses

Once created, copy both IPs:
- **Public IPv4**: `<worker-7-public-ip>` (e.g., 157.230.45.68)
- **Private IPv4**: `<worker-7-private-ip>` (e.g., 10.116.0.9)

**Update your local `.env` file:**

```bash
# On your local machine
cd ~/monorepo/cloud/infrastructure/production
vi .env

# Add these lines:
WORKER_7_PUBLIC_IP=157.230.45.68
WORKER_7_PRIVATE_IP=10.116.0.9
```

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

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

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

---

## Step 2: Initial Server Setup

### 2.1 SSH to Worker-7

```bash
# From your local machine
ssh root@<worker-7-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
```

This takes 2-5 minutes.

### 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-7-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 back to worker-7 as root:**

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

### 3.1 Setup UFW Firewall

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

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

# Allow HTTP and HTTPS (for Caddy)
sudo ufw allow 80/tcp
sudo 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

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

### 4.1 Get Swarm Join Token

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

```bash
ssh dockeradmin@<manager-public-ip>

# 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-7 to Swarm

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

```bash
# From your local machine
ssh dockeradmin@<worker-7-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-7 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-1-prod       Ready     Active
...
xyz789...                     mapleopentech-swarm-worker-7-prod       Ready     Active         <-- NEW!
```

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

### 4.4 Label Worker-7 for MaplePress Frontend

**On manager:**

```bash
# Add MaplePress frontend label (tells swarm to deploy MaplePress frontend here)
docker node update --label-add maplepress-frontend=true mapleopentech-swarm-worker-7-prod

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

**✅ Checkpoint:** Worker-7 has `maplepress-frontend=true` label

---

## Step 5: Configure DNS

Before deploying, point your frontend domain to worker-7.

### 5.1 Update DNS Records

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

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

Replace `<worker-7-public-ip>` with your actual public IP (e.g., 157.230.45.68)

Example:
- **@ record**: Points `getmaplepress.com` → 157.230.45.68
- **www record**: Points `www.getmaplepress.com` → 157.230.45.68

### 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 getmaplepress.com +short
# Should return: <worker-7-public-ip>

# Test www subdomain
dig www.getmaplepress.com +short
# Should return: <worker-7-public-ip>
```

**⚠️ Don't proceed until both commands return the correct IP!**

**✅ Checkpoint:** DNS resolves to worker-7 public IP

---

## Step 6: Clone Repository and Setup Build

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

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

```bash
ssh dockeradmin@<worker-7-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
```

### 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/maplepress-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'
# MaplePress Frontend - Production Environment

# Backend API URL (production)
VITE_API_BASE_URL=https://getmaplepress.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 (used by frontend to make API calls)
- Vite reads `.env.production` when building for production
- Without this file, frontend uses development default (`http://localhost:8000`)

### 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 "getmaplepress.ca" dist/assets/*.js | head -2
# Should show: getmaplepress.ca in compiled JavaScript
```

**Expected output:**

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

**⚠️ Important:** If you see `localhost:8000` in the grep output instead of `getmaplepress.ca`, the build used the wrong environment file. Make sure `.env.production` exists and rebuild.

### 6.5 Create Symlink for Caddy

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

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

**✅ Checkpoint:** React app built successfully at `/var/www/maplepress-frontend`

---
## Step 7: Deploy Caddy for Frontend

### 7.1 Create Caddyfile

**On manager node:**

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

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

### 7.2 Create Caddyfile

```bash
vi Caddyfile
```

**Paste this configuration:**

```caddy
{
    # Global options
    email YOUR_EMAIL@example.com

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

# Frontend domain configuration
getmaplepress.com www.getmaplepress.com {
    # Root directory for static files
    root * /var/www/maplepress-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
    @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 `YOUR_EMAIL@example.com` with your real email
2. Replace `getmaplepress.com` with your actual domain

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

**Understanding the config:**
- **`root * /var/www/maplepress-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
- **`encode gzip`**: Compresses responses for faster load times
- **Static asset caching**: JS/CSS/images cached for 1 year

### 7.3 Create Caddy Stack File

**On manager:**

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

**Paste this:**

```yaml
version: '3.8'

volumes:
  maplepress_frontend_caddy_data:
    # SSL certificates for getmaplepress.com
  maplepress_frontend_caddy_config:
    # Caddy config cache

configs:
  caddyfile:
    file: ./maplepress-frontend-caddy-config/Caddyfile

services:
  caddy:
    image: caddy:2-alpine
    hostname: frontend-caddy
    ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: udp
        mode: host
    configs:
      - source: caddyfile
        target: /etc/caddy/Caddyfile
    volumes:
      - maplepress_frontend_caddy_data:/data
      - maplepress_frontend_caddy_config:/config
      - /var/www/maplepress-frontend:/var/www/maplepress-frontend:ro
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.labels.maplepress-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
```

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

### 7.4 Verify Prerequisites

```bash
# On manager

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

# 2. Verify build exists on worker-7
ssh dockeradmin@<worker-7-public-ip> "ls -la /var/www/maplepress-frontend"
# Should show: index.html, assets/, etc.

# 3. Test DNS resolution
dig getmaplepress.com +short
# Should return: <worker-7-public-ip>
```

### 7.5 Deploy Caddy Stack

**If this is a re-deployment:**

```bash
# Remove old stack and config
docker stack rm maplepress-frontend
sleep 5
docker config rm maplepress-frontend_caddyfile 2>/dev/null || true
```

**Deploy fresh:**

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

# Deploy Caddy
docker stack deploy -c maplepress-frontend-stack.yml maplepress-frontend

# Expected output:
# Creating service maplepress-frontend_caddy
```

### 7.6 Watch Caddy Start

```bash
# Watch service come up
docker service ps maplepress-frontend_caddy

# Follow logs to see certificate acquisition
docker service logs -f maplepress-frontend_caddy

# You'll see:
# - "serving initial configuration"
# - "enabling automatic TLS certificate management"
# - "certificate obtained successfully"
# Press Ctrl+C when done
```

**✅ Checkpoint:** Service shows `1/1` replicas running

---
## Step 8: Verify Deployment

### 8.1 Test HTTP Redirect

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

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

### 8.2 Test HTTPS Access

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

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

### 8.3 Test in Browser

Open your browser and visit: `https://getmaplepress.com`

**You should see:**
- 🔒 Green padlock in address bar
- No security warnings
- Your React app loading correctly

**✅ Checkpoint:** Frontend loads successfully with valid SSL!

---

## Step 9: Update Process

When you make changes to your React frontend:

### 9.1 Update Frontend Code

**SSH to worker-7:**

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

# Navigate to repository
cd /var/www/monorepo

# Pull latest changes
git pull origin main

# Navigate to frontend
cd web/maplepress-frontend

# Ensure production environment is configured
cat > .env.production << 'EOF'
VITE_API_BASE_URL=https://getmaplepress.ca
NODE_ENV=production
EOF

# Install any new dependencies
npm install

# Build updated frontend
npm run build
```

**That's it!** No service restart needed.

### 9.2 Automated Deployment Script

Create a deployment script on worker-7:

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

**Paste:**

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

echo "🚀 Deploying MaplePress Frontend..."

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

cd web/maplepress-frontend

echo "🔧 Configuring production environment..."
cat > .env.production << 'EOF'
# MaplePress Frontend - Production Environment

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

NODE_ENV=production
EOF

echo "📦 Installing dependencies..."
npm install

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

echo "✅ Verifying production API URL in build..."
if grep -q "getmaplepress.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 "🌐 Visit: https://getmaplepress.com"
```

**Make executable:**

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

**Use it:**

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

---
## Troubleshooting

### Problem: Caddy Won't Start

**Symptom:** `docker service ps maplepress-frontend_caddy` shows service restarting

**Check:**

```bash
# View logs for errors
docker service logs maplepress-frontend_caddy --tail 100

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

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

### Problem: Config Immutability Error

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

**Solution:**

```bash
# Remove stack and config
docker stack rm maplepress-frontend
sleep 10
docker config rm maplepress-frontend_caddyfile

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

### Problem: 404 Not Found for React Routes

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

**Fix:** Ensure Caddyfile has:

```caddy
try_files {path} /index.html
```

This is critical for SPA routing.

### Problem: Frontend Calling localhost:8000 Instead of Production Backend

**Symptom:** Browser console shows errors like:
```
Fetch API cannot load http://localhost:8000/health due to access control checks
```

**Root Cause:** Frontend was built without `.env.production` file, so Vite used development defaults.

**Check:**

```bash
# On worker-7
ssh dockeradmin@<worker-7-public-ip>

# Check if .env.production exists
cat /var/www/monorepo/web/maplepress-frontend/.env.production
# Should show: VITE_API_BASE_URL=https://getmaplepress.ca

# Check if production URL is in build
grep -r "getmaplepress.ca" /var/www/maplepress-frontend/assets/*.js | head -2
# Should find the production URL in compiled JavaScript
```

**Solution:**

```bash
# On worker-7
cd /var/www/monorepo/web/maplepress-frontend

# Create production environment file
cat > .env.production << 'EOF'
# MaplePress Frontend - Production Environment

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

NODE_ENV=production
EOF

# Rebuild frontend
npm run build

# Verify production URL is in build
grep -r "getmaplepress.ca" dist/assets/*.js | head -2
# Should show matches
```

**Prevention:** The deployment script and manual deployment steps in this guide now include creating `.env.production` automatically. If you followed an older version of this guide, run the rebuild steps above.

### Problem: Certificate Acquisition Fails

**Check:**

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

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

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

### Problem: Frontend Shows Old Version

**Solution:**

```bash
# Verify build is recent
ssh dockeradmin@<worker-7-public-ip>
ls -la /var/www/maplepress-frontend/
# Check timestamps

# Rebuild if needed
cd /var/www/monorepo/web/maplepress-frontend

# Ensure production environment is configured
cat > .env.production << 'EOF'
VITE_API_BASE_URL=https://getmaplepress.ca
NODE_ENV=production
EOF

npm run build

# Hard refresh browser: Ctrl+Shift+R
```

---

## Next Steps

✅ **You now have:**
- Worker-7 with Caddy serving React frontend
- Independent domain with automatic HTTPS
- Git-based deployment workflow
- Easy updates via SSH + git pull + npm build

**CORS Configuration (if frontend calls backend API):**

If your React app makes API calls to `https://getmaplepress.ca/api/*`:

**Option 1: Configure CORS on backend** (in Go code)

**Option 2: Proxy through frontend Caddy:**

Edit Caddyfile:

```caddy
getmaplepress.com www.getmaplepress.com {
    root * /var/www/maplepress-frontend
    file_server
    try_files {path} /index.html

    # Proxy API calls to backend
    handle /api/* {
        reverse_proxy https://getmaplepress.ca {
            header_up Host {upstream_hostport}
        }
    }

    # ... rest of config
}
```

Now frontend can call `https://getmaplepress.com/api/*` - no CORS needed!

**Maintenance commands:**

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

# View Caddy logs
docker service logs -f maplepress-frontend_caddy

# Restart Caddy
docker service update --force maplepress-frontend_caddy

# Update Caddyfile
# 1. Edit: vi ~/stacks/maplepress-frontend-caddy-config/Caddyfile
# 2. Remove: docker stack rm maplepress-frontend
# 3. Remove config: docker config rm maplepress-frontend_caddyfile
# 4. Redeploy: docker stack deploy -c maplepress-frontend-stack.yml maplepress-frontend
```

---

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

**Changelog:**
- January 2025: Initial frontend deployment guide with Caddy, git-based workflow, and Docker Swarm integration
