# Deploy Caddy Reverse Proxy with Automatic SSL: Part 2

**Audience**: Junior DevOps Engineers, Infrastructure Team
**Time to Complete**: 20-30 minutes
**Prerequisites**:
- ✅ Completed guide **09_maplefile_backend.md** (Backend deployed and running)
- ✅ Backend service accessible on `mapleopentech-public-prod` network
- ✅ Domain name `maplefile.ca` pointing to worker-8 public IP
- ✅ Email address for Let's Encrypt SSL certificate notifications

---

## Overview

This guide configures **Caddy** as a reverse proxy with automatic SSL/TLS certificate management for your MapleFile backend.

### What is a Reverse Proxy?

Think of a reverse proxy as a "receptionist" for your backend:

1. **Internet user** → Makes request to `https://maplefile.ca`
2. **Caddy (receptionist)** → Receives the request
   - Handles SSL/TLS (HTTPS encryption)
   - Checks rate limits
   - Adds security headers
3. **Caddy forwards** → Sends request to your backend at `http://maplefile-backend:8000`
4. **Backend** → Processes request, sends response back
5. **Caddy** → Returns response to user

**Why use a reverse proxy?**
- Your backend doesn't need to handle SSL certificates
- One place to manage security, rate limiting, and headers
- Can load balance across multiple backend instances
- Protects your backend from direct internet exposure

### Why Caddy Instead of NGINX?

**Caddy's killer feature: Automatic HTTPS**
- Caddy automatically gets SSL certificates from Let's Encrypt
- Automatically renews them before expiry (no cron jobs!)
- Zero manual certificate management
- Simpler configuration (10 lines vs 200+ for NGINX)

**What you'll build:**
- Caddy reverse proxy on worker-8
- Automatic SSL certificate from Let's Encrypt
- HTTP to HTTPS automatic redirection
- Security headers and rate limiting
- Zero-downtime certificate renewals (automatic)

**Architecture:**
```
Internet
  ↓ HTTPS (port 443)
Caddy (worker-8)
  ↓ HTTP (port 8000, internal network only)
Backend (worker-8)
  ↓ Private network
Databases (Cassandra, Redis on other workers)
```

**Key concept:** Caddy and Backend are both on worker-8, connected via the `mapleopentech-public-prod` Docker overlay network. Caddy can reach Backend by the hostname `maplefile-backend` - Docker's built-in DNS resolves this to the backend container's IP automatically.

---

## Step 1: Verify DNS Configuration

Before deploying Caddy, your domain must point to worker-8 (where Caddy will run).

### 1.1 Check Current DNS

**From your local machine:**

```bash
# Check where your domain currently points
dig maplefile.ca +short

# Should return worker-8's public IP (e.g., 143.110.212.253)
# If it returns nothing or wrong IP, continue to next step
```

### 1.2 Update DNS Records

**If DNS is not configured or points to wrong server:**

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

| Type | Name | Value | TTL |
|------|------|-------|-----|
| A | @ | `143.110.212.253` | 3600 |
| A | www | `143.110.212.253` | 3600 |

**Replace `143.110.212.253` with YOUR worker-8 public IP!**

**What this does:**
- `@` record: Makes `maplefile.ca` point to worker-8
- `www` record: Makes `www.maplefile.ca` point to worker-8
- Both domains will work with Caddy

### 1.3 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.ca +short
# Should return: 143.110.212.253 (your worker-8 IP)

# Test www subdomain
dig www.maplefile.ca +short
# Should return: 143.110.212.253 (your worker-8 IP)

# Alternative test
nslookup maplefile.ca
# Should show: Address: 143.110.212.253
```

**Keep testing every minute until both commands return worker-8's public IP.**

⚠️ **CRITICAL:** Do NOT proceed until DNS resolves correctly! Caddy cannot get SSL certificates if DNS doesn't point to the right server.

### 1.4 Verify Firewall Allows HTTP/HTTPS

**On worker-8, check firewall:**

```bash
# SSH to worker-8
ssh dockeradmin@143.110.212.253

# Check firewall rules
sudo ufw status | grep -E "80|443"

# Should show:
# 80/tcp                     ALLOW       Anywhere
# 443/tcp                    ALLOW       Anywhere
```

**If ports are NOT open:**

```bash
# Allow HTTP (needed for Let's Encrypt)
sudo ufw allow 80/tcp

# Allow HTTPS (needed for encrypted traffic)
sudo ufw allow 443/tcp

# Verify
sudo ufw status | grep -E "80|443"

# Exit back to local machine
exit
```

**✅ Checkpoint:** DNS resolves to worker-8, ports 80 and 443 are open

---

## Step 2: Prepare Caddy Configuration

### 2.1 Create Caddy Config Directory

**On manager node:**

```bash
# SSH to manager
ssh dockeradmin@143.110.210.162

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

### 2.2 Create Caddyfile

The **Caddyfile** is Caddy's configuration file. It's much simpler than NGINX config.

```bash
vi Caddyfile
```

**Paste this configuration:**

```caddy
{
    # Global options
    email your-email@example.com

    # Use Let's Encrypt production (not staging)
    # Staging is for testing - production is for real certificates
    acme_ca https://acme-v02.api.letsencrypt.org/directory
}

# Your domain configuration
maplefile.ca www.maplefile.ca {
    # Reverse proxy all requests to backend
    reverse_proxy maplefile-backend:8000 {
        # Forward real client IP to backend
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
        header_up X-Forwarded-Host {host}

        # Preserve Origin header for CORS (required for frontend)
        header_up Origin {http.request.header.Origin}
    }

    # Logging
    log {
        output stdout
        format json
        level INFO
    }

    # Security headers (Caddy adds many by default)
    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 (security by obscurity)
        -Server
    }

    # Rate limiting (requires Caddy plugin - see note below)
    # For basic setups, you can skip this or add later
}
```

**Important replacements:**
1. Replace `your-email@example.com` with your real email (Let's Encrypt sends expiry warnings here)
2. Domain names are already set to `maplefile.ca` and `www.maplefile.ca`
3. Backend hostname is already set to `maplefile-backend:8000`

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

**Understanding the config:**

- **`maplefile-backend:8000`** - This is how Caddy reaches your backend
  - `maplefile-backend` = hostname of your backend service (Docker DNS resolves this)
  - `8000` = port your backend listens on
  - No IP address needed - Docker overlay network handles it!

**Important: Service Name vs Hostname**

When you run `docker service ls`, you see:
```
maplefile_backend   1/1   registry.digitalocean.com/ssp/maplefile-backend:prod
```

But in the Caddyfile, we use `maplefile-backend:8000`, not `maplefile_backend:8000`. Why?

- **Service name** (`maplefile_backend`): How Docker Swarm identifies the service
  - Used in: `docker service ls`, `docker service logs maplefile_backend`
  - Format: `{stack-name}_{service-name}`

- **Hostname** (`maplefile-backend`): How containers reach each other on the network
  - Used in: Caddyfile, application configs, container-to-container communication
  - Defined in the stack file: `hostname: maplefile-backend`

**Think of it like this:**
- Service name = The employee's official HR name (full legal name)
- Hostname = The nickname everyone uses in the office

Other containers don't care about the service name - they use the hostname for DNS resolution.

- **`header_up`** - Passes information to your backend about the real client
  - Without this, backend would think all requests come from Caddy
  - Your backend can log real client IPs for security/debugging

- **Security headers** - Tell browsers how to handle your site securely
  - HSTS: Forces browsers to always use HTTPS
  - X-Frame-Options: Prevents your site being embedded in iframes (clickjacking protection)
  - X-Content-Type-Options: Prevents MIME confusion attacks

### 2.3 Understanding the Automatic SSL Magic

**What happens when Caddy starts:**

1. Caddy sees `maplefile.ca` in the Caddyfile
2. Caddy checks if domain points to this server (DNS check)
3. Caddy requests SSL certificate from Let's Encrypt
4. Let's Encrypt does a challenge (HTTP-01 via port 80)
5. Caddy receives certificate and stores it in `/data/caddy`
6. Caddy automatically serves HTTPS on port 443
7. Caddy automatically redirects HTTP → HTTPS

**You don't have to:**
- Manually run certbot commands
- Stop the server to renew certificates
- Set up cron jobs
- Mount certificate directories

**Caddy handles ALL of this automatically!**

---

## Step 3: Deploy Caddy Service

### 3.1 Update Stack File to Add Caddy

We need to UPDATE the existing `maplefile-stack.yml` file to add the `backend-caddy` service.

**On manager node:**

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

**Add the following sections to your existing stack file:**

**First, add volumes section after networks (if not already there):**

```yaml
volumes:
  caddy_data:
    # Caddy stores certificates here
  caddy_config:
    # Caddy stores config cache here
```

**Then, add configs section after volumes:**

```yaml
configs:
  caddyfile:
    file: ./maplefile-caddy-config/Caddyfile
```

**Finally, add the backend-caddy service after the backend service:**

```yaml
  backend-caddy:
    image: caddy:2.9.1-alpine
    hostname: maplefile-caddy
    networks:
      - mapleopentech-public-prod
    ports:
      # Port 80 - HTTP (for Let's Encrypt challenges and HTTP→HTTPS redirect)
      # Using mode: host to bind directly to worker-8's network interface
      - 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 (optional, modern protocol)
      - target: 443
        published: 443
        protocol: udp
        mode: host
    configs:
      # Docker config - automatically distributed to worker-8
      - source: caddyfile
        target: /etc/caddy/Caddyfile
    volumes:
      # Persistent storage for certificates
      - caddy_data:/data
      # Persistent storage for config cache
      - caddy_config:/config
    deploy:
      replicas: 1
      placement:
        constraints:
          # Deploy on same node as backend (worker-8)
          - node.labels.maplefile-backend == true
      restart_policy:
        condition: on-failure
        delay: 5s
        # Note: No max_attempts - Docker will keep trying indefinitely
        # This prevents the service from scaling to 0 after a few failures
      update_config:
        # Rolling updates (zero downtime)
        parallelism: 1
        delay: 10s
        order: start-first
      resources:
        limits:
          # Caddy is lightweight - 256MB is plenty
          memory: 256M
        reservations:
          memory: 128M
    # Note: No healthcheck - Caddy's built-in health monitoring is sufficient
    # Docker healthchecks can cause SIGTERM shutdowns during startup or cert renewal
```

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

**Understanding the stack file:**

- **`mapleopentech-public-prod` network**: Shared network with backend
  - Both Caddy and Backend are connected here
  - Allows Caddy to reach Backend by hostname
  - `external: true` means we created this network earlier (in 09_maplefile_backend.md)

- **Ports** (using `mode: host`):
  - Port 80 (HTTP) - Needed for Let's Encrypt certificate challenges
  - Port 443 (HTTPS TCP) - Encrypted traffic
  - Port 443 (HTTPS UDP) - HTTP/3 support
  - **Why `mode: host`?** Binds directly to worker-8's network interface
    - `mode: ingress` (default) uses Docker Swarm routing mesh (any node can accept traffic)
    - `mode: host` binds only on the specific node running Caddy
    - Since we're pinning Caddy to worker-8 anyway, `host` mode is more reliable
    - Prevents potential routing issues with Let's Encrypt challenges

- **Configs** (not volumes for Caddyfile):
  - `caddyfile` - Docker config that's automatically distributed to worker-8
  - Why not a volume mount? Because the file is on the manager, but Caddy runs on worker-8
  - Docker configs solve this: they're stored in the swarm and sent to the right node
  - Configs are immutable - to update, you must redeploy the stack

- **Volumes**:
  - `caddy_data` - Stores SSL certificates (persists across restarts)
  - `caddy_config` - Stores runtime config cache (persists across restarts)
  - Why separate from backend data? So certificate renewals don't affect backend storage
  - Volumes persist even if Caddy container is recreated

- **Placement constraint**:
  - `node.labels.maplefile-backend == true` - Same as backend (worker-8)
  - Caddy and Backend MUST be on the same node to share `mapleopentech-public-prod` network
  - Docker overlay networks work best when services are colocated

### 3.2 Deploy Updated Stack

**On manager node:**

```bash
# Deploy the updated stack
docker stack deploy -c ~/stacks/maplefile-stack.yml maplefile

# Check both services are running
docker service ls | grep maplefile
# Should show:
# maplefile_backend         1/1   registry.digitalocean.com/ssp/maplefile-backend:prod
# maplefile_backend-caddy   1/1   caddy:2.9.1-alpine
```

**Expected output:**
```
yexoj87lb67j   maplefile_backend         replicated   1/1   registry.digitalocean.com/ssp/maplefile-backend:prod
abc123xyz456   maplefile_backend-caddy   replicated   1/1   caddy:2.9.1-alpine
```

### 3.3 Watch Caddy Start and Get SSL Certificate

**This is the exciting part - watch Caddy automatically get your SSL certificate!**

```bash
# Watch Caddy logs (real-time)
docker service logs -f maplefile_backend-caddy

# You'll see something like this:
# {"level":"info","msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile"}
# {"level":"info","msg":"obtaining certificate","domain":"maplefile.ca"}
# {"level":"info","msg":"validating authorization","domain":"maplefile.ca","challenge":"http-01"}
# {"level":"info","msg":"authorization finalized","domain":"maplefile.ca"}
# {"level":"info","msg":"certificate obtained successfully","domain":"maplefile.ca"}
# {"level":"info","msg":"serving initial configuration"}
```

**Press `Ctrl+C` to exit log streaming when you see "certificate obtained successfully"**

**What just happened?**
1. Caddy loaded the Caddyfile
2. Caddy saw `maplefile.ca` and checked DNS
3. Caddy requested a certificate from Let's Encrypt
4. Let's Encrypt sent an HTTP challenge to port 80
5. Caddy responded to the challenge
6. Let's Encrypt verified ownership and issued the certificate
7. Caddy stored the certificate in the `caddy_data` volume
8. Caddy started serving HTTPS on port 443

**All of this happened in ~10-30 seconds, completely automatically!**

---

## Step 4: Test Your HTTPS Site

### 4.1 Test HTTP to HTTPS Redirect

**From your local machine:**

```bash
# Test HTTP (port 80) - should redirect to HTTPS
curl -I http://maplefile.ca

# Expected response:
# HTTP/1.1 308 Permanent Redirect
# Location: https://maplefile.ca/
```

**What this means:**
- Caddy received HTTP request
- Caddy automatically redirected to HTTPS
- Browser will follow redirect and use HTTPS

### 4.2 Test HTTPS Connection

```bash
# Test HTTPS (port 443)
curl -I https://maplefile.ca/health

# Expected response:
# HTTP/2 200
# Content-Type: application/json
# (Your backend's response)
```

**If you see HTTP/2 200, congratulations! Your site is:**
- ✅ Serving over HTTPS
- ✅ Using HTTP/2 (faster than HTTP/1.1)
- ✅ Protected by a valid Let's Encrypt SSL certificate
- ✅ Automatically redirecting HTTP to HTTPS

### 4.3 Test in Browser

**Open your browser and visit:**

1. `http://maplefile.ca/version` - Should redirect to HTTPS
2. `https://maplefile./version` - Should show your backend's response
3. `https://www.maplefile.ca/version` - Should also work (www subdomain)

**Click the padlock icon in your browser address bar:**
- Should show "Connection is secure"
- Certificate issued by: Let's Encrypt
- Valid for: `maplefile.ca` and `www.maplefile.ca`
- Expires in: ~90 days (Caddy will auto-renew at 60 days)

### 4.4 Test SSL Certificate

**Use SSL Labs to test your certificate (optional but recommended):**

1. Visit: https://www.ssllabs.com/ssltest/
2. Enter: `maplefile.ca`
3. Click "Submit"
4. Wait 2-3 minutes for the test

**Expected grade: A or A+**

If you get less than A, check:
- Security headers in Caddyfile
- HSTS header is present
- No insecure protocols enabled

---

## Step 5: Verify Services

### 5.1 Check All Services Running

```bash
# List all maplefile services
docker service ls | grep maplefile

# Expected output:
# maplefile_backend         1/1   registry.digitalocean.com/ssp/maplefile-backend:prod
# maplefile_backend-caddy   1/1   caddy:2.9.1-alpine
```

### 5.2 Check Service Tasks

```bash
# Check backend tasks
docker service ps maplefile_backend

# Check caddy tasks
docker service ps maplefile_backend-caddy

# Both should show:
# CURRENT STATE: Running X minutes ago
# No ERROR messages
```

### 5.3 Test Backend Health

```bash
# Test backend health endpoint directly (from manager)
curl http://maplefile.ca/health

# Expected: {"status":"healthy"} or similar

# Test through Caddy (HTTPS)
curl https://maplefile.ca/health

# Should return the same response
```

---

## Troubleshooting

### Problem: Caddy Can't Get SSL Certificate

**Symptom:** Caddy logs show "failed to obtain certificate" or "challenge failed"

**Causes and fixes:**

1. **DNS not pointing to worker-8**
   ```bash
   # Test DNS
   dig maplefile.ca +short
   # Should return worker-8's public IP (143.110.212.253)

   # If wrong, update DNS records and wait for propagation (5-60 min)
   ```

2. **Port 80 not accessible**
   ```bash
   # Test from outside
   curl -I http://maplefile.ca

   # If connection refused, check firewall
   ssh dockeradmin@143.110.212.253
   sudo ufw allow 80/tcp
   sudo ufw allow 443/tcp
   ```

3. **Caddyfile has wrong domain**
   ```bash
   # Check Caddyfile on manager
   cat ~/stacks/maplefile-caddy-config/Caddyfile

   # Should show: maplefile.ca www.maplefile.ca
   # If wrong, edit and redeploy
   vi ~/stacks/maplefile-caddy-config/Caddyfile
   docker stack deploy -c ~/stacks/maplefile-stack.yml maplefile
   ```

4. **Let's Encrypt rate limit (5 certificates per week)**
   ```bash
   # Check Caddy logs for rate limit message
   docker service logs maplefile_backend-caddy | grep -i "rate limit"

   # If rate limited, wait 7 days or use staging for testing
   # Edit Caddyfile to use staging:
   # acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
   ```

### Problem: HTTP Not Redirecting to HTTPS

**Symptom:** `http://maplefile.ca` doesn't redirect

**Fix:**

```bash
# Check Caddy is running
docker service ps maplefile_backend-caddy

# Check Caddy logs
docker service logs maplefile_backend-caddy --tail 50

# Caddy should automatically redirect HTTP to HTTPS
# If not, check Caddyfile syntax
```

### Problem: Backend Not Reachable Through Caddy

**Symptom:** HTTPS works but returns 502 Bad Gateway

**Causes:**

1. **Backend not running**
   ```bash
   docker service ps maplefile_backend
   # Should show: Running
   ```

2. **Backend not on mapleopentech-public-prod network**
   ```bash
   # Check backend networks
   docker service inspect maplefile_backend --format '{{json .Spec.TaskTemplate.Networks}}'

   # Should include mapleopentech-public-prod
   ```

3. **Wrong hostname in Caddyfile**
   ```bash
   # Check Caddyfile
   cat ~/stacks/maplefile-caddy-config/Caddyfile | grep reverse_proxy

   # Should show: reverse_proxy maplefile-backend:8000
   # NOT: maplefile_backend:8000 (wrong - underscore instead of hyphen)
   ```

### Problem: Certificate Renewal Fails

**Symptom:** Certificate expires or renewal warnings in logs

**Fix:**

```bash
# Check Caddy logs for renewal attempts
docker service logs maplefile_backend-caddy | grep -i renew

# Caddy renews at 60 days (certificate valid for 90 days)
# If renewal fails, check:
# 1. DNS still points to worker-8
# 2. Port 80 still open
# 3. Caddy service still running

# Force renewal (if needed)
# Restart Caddy service
docker service update --force maplefile_backend-caddy
```

---

## Maintenance

### Updating Caddyfile

When you need to change Caddy configuration:

```bash
# 1. Edit Caddyfile on manager
ssh dockeradmin@143.110.210.162
vi ~/stacks/maplefile-caddy-config/Caddyfile

# 2. Redeploy stack
docker stack deploy -c ~/stacks/maplefile-stack.yml maplefile

# 3. Watch Caddy reload
docker service logs -f maplefile_backend-caddy

# Caddy will gracefully reload with zero downtime
```

### Monitoring SSL Certificate Expiry

```bash
# Check certificate expiry
echo | openssl s_client -servername maplefile.ca -connect maplefile.ca:443 2>/dev/null | openssl x509 -noout -dates

# Returns:
# notBefore=Jan 15 12:00:00 2025 GMT
# notAfter=Apr 15 12:00:00 2025 GMT

# Caddy automatically renews at 60 days (30 days before expiry)
```

### Viewing Caddy Access Logs

```bash
# Real-time logs
docker service logs -f maplefile_backend-caddy

# Last 100 lines
docker service logs maplefile_backend-caddy --tail 100

# Filter for errors
docker service logs maplefile_backend-caddy | grep -i error
```

---

## Security Best Practices

### 1. Keep Caddy Updated

```bash
# Check current version
docker service inspect maplefile_backend-caddy --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'

# Update to latest (in stack file)
vi ~/stacks/maplefile-stack.yml
# Change: image: caddy:2.9.1-alpine
# To: image: caddy:2.10.0-alpine (or latest version)

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

### 2. Monitor Certificate Health

Set up monitoring to alert before certificate expiry:
- Let's Encrypt certificates expire in 90 days
- Caddy renews at 60 days
- Monitor renewal attempts in logs
- Set up alerts if renewal fails

### 3. Review Access Logs Regularly

```bash
# Check for suspicious access patterns
docker service logs maplefile_backend-caddy | grep -E "404|403|500"

# Look for unusual traffic spikes
docker service logs maplefile_backend-caddy | grep -i "POST\|PUT\|DELETE"
```

---

## Summary

**What you've accomplished:**

✅ Deployed Caddy reverse proxy on worker-8
✅ Obtained automatic SSL certificate from Let's Encrypt
✅ Configured HTTPS with HTTP/2 support
✅ Set up automatic HTTP → HTTPS redirects
✅ Added security headers (HSTS, X-Frame-Options, etc.)
✅ Configured Caddy to forward client IPs to backend
✅ Set up automatic certificate renewal (every 60 days)

**Your MapleFile backend is now:**
- Publicly accessible at `https://maplefile.ca`
- Protected by SSL/TLS encryption
- Behind a reverse proxy for security
- Automatically renewing certificates
- Serving HTTP/2 for better performance

**Next steps:**
- Deploy MapleFile frontend (connects to this backend)
- Set up monitoring and alerting
- Configure backups for Caddy volumes
- Review and tune security headers
- Set up rate limiting (if needed)

**Important URLs:**
- Backend API: `https://maplefile.ca`
- Health check: `https://maplefile.ca/health`
- SSL Labs test: https://www.ssllabs.com/ssltest/analyze.html?d=maplefile.ca

---

## Quick Reference

### Common Commands

```bash
# View Caddy logs
docker service logs -f maplefile_backend-caddy

# Restart Caddy (zero downtime)
docker service update --force maplefile_backend-caddy

# Update Caddyfile and reload
vi ~/stacks/maplefile-caddy-config/Caddyfile
docker stack deploy -c ~/stacks/maplefile-stack.yml maplefile

# Check SSL certificate
echo | openssl s_client -servername maplefile.ca -connect maplefile.ca:443 2>/dev/null | openssl x509 -noout -dates

# Test HTTPS
curl -I https://maplefile.ca

# Check service status
docker service ps maplefile_backend-caddy
```

### File Locations

- Caddyfile: `~/stacks/maplefile-caddy-config/Caddyfile`
- Stack file: `~/stacks/maplefile-stack.yml`
- Certificates: Stored in `caddy_data` Docker volume
- Config cache: Stored in `caddy_config` Docker volume

---

**🎉 Congratulations!** Your MapleFile backend is now securely accessible over HTTPS with automatic SSL certificate management!
