Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DOMAIN=local.example.com
UPSTREAM_URL=http://host.docker.internal:3000
69 changes: 69 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Test

on:
push:
branches: [main]
pull_request:
branches: [main]

env:
DOMAIN: sslproxy.stackpop.com

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Start mock upstream
run: |
docker run -d --name upstream -p 3000:80 nginx:alpine
sleep 2

- name: Create .env
run: |
echo "DOMAIN=${{ env.DOMAIN }}" > .env
echo "UPSTREAM_URL=http://host.docker.internal:3000" >> .env

- name: Add test domain to hosts
run: echo "127.0.0.1 ${{ env.DOMAIN }}" | sudo tee -a /etc/hosts

- name: Build images
run: docker compose build

- name: Generate certificates
run: docker compose --profile setup run --rm mkcert

- name: Verify certificates exist
run: |
test -f certs/${{ env.DOMAIN }}.pem
test -f certs/${{ env.DOMAIN }}.key.pem
test -f certs/${{ env.DOMAIN }}.rootCA.pem

- name: Start proxy
run: docker compose up -d

- name: Wait for Caddy to start
run: sleep 3

- name: Check Caddy is running
run: docker compose ps --status running --services | grep -q '^caddy$'

- name: Test HTTP redirect
run: |
curl -s -o /dev/null -w "%{http_code}" http://${{ env.DOMAIN }}:8080 | grep -q "301\|308"

- name: Test HTTPS proxies to upstream
run: |
curl -s --cacert certs/${{ env.DOMAIN }}.rootCA.pem https://${{ env.DOMAIN }}:8443 | grep -q "nginx"

- name: Show logs on failure
if: failure()
run: docker compose logs

- name: Stop proxy
if: always()
run: |
docker compose down
docker rm -f upstream || true
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ certs/
.env

# AI agents
.claude/
.claude/
54 changes: 54 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

A Caddy-based reverse proxy for local development with automatic SSL certificate generation.

## Architecture

- **mkcert container**: Generates SSL certificates on first run, stores in `./certs/`
- **Caddy container**: Reverse proxy with HTTPS, depends on mkcert completing first
- **Bind mount**: Certs stored locally in `./certs/` for easy access

## Key Commands

```bash
# Generate certificates (first time only)
docker-compose --profile setup run --rm mkcert

# Start the proxy
docker-compose up -d --build

# View logs
docker-compose logs -f caddy

# Stop the proxy
docker-compose down

# Regenerate certificates
rm -rf certs/* && docker-compose --profile setup run --rm mkcert

# Install CA on macOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./certs/${DOMAIN}.rootCA.pem
```

## Configuration

Environment variables (set in `.env`):
- `DOMAIN` - Domain name for SSL cert (default: `localhost`)
- `UPSTREAM_URL` - URL for your local app (default: `http://host.docker.internal:3000`)

## Files

- **config/Caddyfile**: Proxy rules, TLS config, CSP header removal
- **scripts/mkcert/entrypoint.sh**: Script that generates certs if they don't exist
- **docker-compose.yml**: Service definitions with mkcert → caddy dependency
- **Dockerfile.mkcert**: Alpine image with mkcert for cert generation
- **Dockerfile.caddy**: Minimal Caddy image

## Ports

- `8080` → HTTP (redirects to HTTPS on 8443)
- `8443` → HTTPS (proxies to `${UPSTREAM_URL}`)
5 changes: 5 additions & 0 deletions Dockerfile.caddy
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM caddy:2-alpine

EXPOSE 80 443

CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
18 changes: 18 additions & 0 deletions Dockerfile.mkcert
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM alpine:3.19

ARG MKCERT_VERSION=1.4.4

RUN apk add --no-cache ca-certificates nss-tools curl \
&& ARCH=$(uname -m) \
&& case "$ARCH" in \
aarch64) MKCERT_ARCH="linux-arm64" ;; \
x86_64) MKCERT_ARCH="linux-amd64" ;; \
*) echo "Unsupported architecture: $ARCH" && exit 1 ;; \
esac \
&& curl -L "https://github.com/FiloSottile/mkcert/releases/download/v${MKCERT_VERSION}/mkcert-v${MKCERT_VERSION}-${MKCERT_ARCH}" -o /usr/local/bin/mkcert \
&& chmod +x /usr/local/bin/mkcert

COPY scripts/mkcert/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# SSL Proxy

A Dockerized Caddy reverse proxy with automatic SSL certificate generation for local development.

## Features

- Automatic SSL certificate generation via mkcert
- Strips Content-Security-Policy headers
- HTTP to HTTPS redirect
- Configurable domain and upstream URL

## Quick Start

1. Configure your domain in `.env`:

```
DOMAIN=local.example.com
UPSTREAM_URL=http://host.docker.internal:3000
```

`UPSTREAM_URL` must include the scheme and port.

2. Add to `/etc/hosts`:

```
127.0.0.1 local.example.com
```

3. Generate certificates (first time only):

```bash
docker-compose --profile setup run --rm mkcert
```

4. Install the CA certificate (one-time):

```bash
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./certs/local.example.com.rootCA.pem
```

5. Start the proxy:

```bash
docker-compose up -d
```

6. Visit: `https://local.example.com:8443`

Note (Linux): Requires Docker Engine 20.10+ for `host-gateway` support.

## Configuration

| Variable | Default | Description |
| --------------- | ----------- | ---------------------- |
| `DOMAIN` | `localhost` | Domain for SSL cert |
| `UPSTREAM_URL` | `http://host.docker.internal:3000` | URL for your local app |

## Ports

- `8080` - HTTP (redirects to HTTPS)
- `8443` - HTTPS

## Layout

```
├── config/Caddyfile # Caddy configuration
├── scripts/mkcert/entrypoint.sh # Cert generation script
├── docker-compose.yml # Service definitions
├── Dockerfile.caddy # Caddy image
├── Dockerfile.mkcert # Certificate generator
└── .env # Your configuration
```
17 changes: 17 additions & 0 deletions config/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
:80 {
redir https://{$DOMAIN:localhost}:8443{uri} permanent
}

{$DOMAIN:localhost} {
tls /etc/caddy/certs/{$DOMAIN:localhost}.pem /etc/caddy/certs/{$DOMAIN:localhost}.key.pem

reverse_proxy {$UPSTREAM_URL:http://host.docker.internal:3000} {
header_down -Content-Security-Policy
header_down -Content-Security-Policy-Report-Only
}

log {
output stdout
format console
}
}
36 changes: 36 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
services:
mkcert:
build:
context: .
dockerfile: Dockerfile.mkcert
container_name: mkcert
profiles:
- setup
environment:
- DOMAIN=${DOMAIN:-localhost}
volumes:
- ./certs:/certs

caddy:
build:
context: .
dockerfile: Dockerfile.caddy
container_name: ssl-proxy
ports:
- "8080:80"
- "8443:443"
environment:
- DOMAIN=${DOMAIN:-localhost}
- UPSTREAM_URL=${UPSTREAM_URL:-http://host.docker.internal:3000}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./config/Caddyfile:/etc/caddy/Caddyfile:ro
- ./certs:/etc/caddy/certs:ro
- caddy_data:/data
- caddy_config:/config
restart: unless-stopped

volumes:
caddy_data:
caddy_config:
22 changes: 22 additions & 0 deletions scripts/mkcert/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/sh
set -e

DOMAIN="${DOMAIN:-localhost}"
CERT_FILE="/certs/${DOMAIN}.pem"
KEY_FILE="/certs/${DOMAIN}.key.pem"
CA_FILE="/certs/${DOMAIN}.rootCA.pem"

if [ ! -f "$CERT_FILE" ]; then
echo "Generating SSL certificate for ${DOMAIN}..."
mkcert -install
mkcert -cert-file "$CERT_FILE" \
-key-file "$KEY_FILE" \
"$DOMAIN"
cp "$(mkcert -CAROOT)/rootCA.pem" "$CA_FILE"
echo "=== Certificate generated ==="
else
echo "Certificate already exists for ${DOMAIN}, skipping generation."
fi

echo "Install CA on macOS:"
echo " sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./certs/${DOMAIN}.rootCA.pem"