Files
portfolio-hub/docs/deployment.md
T

4.2 KiB

Production Deployment: HTTPS with Caddy

This guide describes how to deploy PortfolioHub behind Caddy, a reverse proxy that automatically provisions and renews TLS certificates from Let's Encrypt or ZeroSSL.

Prerequisites

  • A Linux/Ubuntu server with a public IP address
  • A domain name (e.g., api.example.com) with a DNS A record pointing to that IP
  • Ports 80 and 443 open on your firewall (see Firewall Setup)

1. Install Caddy (Ubuntu/Debian)

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
  | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
  | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

Verify the installation:

caddy version

2. Configure the Caddyfile

Create or edit /etc/caddy/Caddyfile:

your-domain.com {
    reverse_proxy localhost:8000
}

Replace your-domain.com with your actual domain. Caddy automatically:

  • Obtains and renews TLS certificates from Let's Encrypt/ZeroSSL
  • Redirects plain HTTP (port 80) to HTTPS (port 443)
  • Forwards all requests to the PortfolioHub uvicorn process on port 8000

Reload Caddy after editing the Caddyfile:

sudo systemctl reload caddy

3. CORS Configuration

When PortfolioHub is accessed from a browser-based frontend over HTTPS, the PORTFOLIOHUB_ALLOWED_ORIGINS environment variable must list the exact HTTPS origin(s) of your frontend.

Set the variable in your environment or .env file:

PORTFOLIOHUB_ALLOWED_ORIGINS=https://app.example.com,https://www.example.com

Multiple origins are comma-separated. For the CORS policy details see section 12 of docs/portfoliohub-spec.md.


4. Start PortfolioHub for Production

Run uvicorn without --reload and bind only to localhost so that external traffic is always routed through Caddy:

uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2

For a systemd service, create /etc/systemd/system/portfoliohub.service:

[Unit]
Description=PortfolioHub API
After=network.target

[Service]
User=www-data
WorkingDirectory=/opt/portfoliohub
EnvironmentFile=/opt/portfoliohub/.env
ExecStart=/opt/portfoliohub/.venv/bin/uvicorn app.main:app \
    --host 127.0.0.1 --port 8000 --workers 2
Restart=on-failure

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable portfoliohub
sudo systemctl start portfoliohub

5. Docker Compose Setup (Optional)

If you run PortfolioHub in Docker, you can add Caddy as a sidecar container.

docker-compose.yml:

services:
  app:
    build: .
    expose:
      - "8000"
    environment:
      - PORTFOLIOHUB_ALLOWED_ORIGINS=https://your-domain.com
      # add other env vars or reference an env_file
    restart: unless-stopped

  caddy:
    image: caddy:2-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - app
    restart: unless-stopped

volumes:
  caddy_data:
  caddy_config:

Caddyfile (placed next to docker-compose.yml):

your-domain.com {
    reverse_proxy app:8000
}

Note: inside a Docker Compose network, Caddy reaches the app container via its service name (app) instead of localhost.

Start everything:

docker compose up -d

6. Firewall Setup

Open ports 80 (HTTP, needed for ACME certificate challenge) and 443 (HTTPS):

# UFW (Ubuntu)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload
# firewalld (RHEL/CentOS/Fedora)
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

7. Verify the Deployment

# Health check over HTTPS
curl https://your-domain.com/health
# Expected: {"status":"ok"}

Check Caddy's status and certificate renewal:

sudo systemctl status caddy
sudo journalctl -u caddy -f