From 0e603790c6add4469b2f78123f750c571ce1dd57 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:44:20 -0800 Subject: [PATCH 1/5] SSL proxy with Caddy and mkcert --- .env.example | 2 + .gitignore | 2 +- CLAUDE.md | 54 +++++++++++++++++++++++++++ Dockerfile.caddy | 5 +++ Dockerfile.mkcert | 18 +++++++++ README.md | 72 ++++++++++++++++++++++++++++++++++++ config/Caddyfile | 17 +++++++++ docker-compose.yml | 36 ++++++++++++++++++ scripts/mkcert/entrypoint.sh | 22 +++++++++++ 9 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 CLAUDE.md create mode 100644 Dockerfile.caddy create mode 100644 Dockerfile.mkcert create mode 100644 README.md create mode 100644 config/Caddyfile create mode 100644 docker-compose.yml create mode 100644 scripts/mkcert/entrypoint.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0bcf871 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +DOMAIN=local.example.com +UPSTREAM_URL=http://host.docker.internal:3000 diff --git a/.gitignore b/.gitignore index daa21fa..eb353ba 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ certs/ .env # AI agents -.claude/ \ No newline at end of file +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..03359dd --- /dev/null +++ b/CLAUDE.md @@ -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}`) diff --git a/Dockerfile.caddy b/Dockerfile.caddy new file mode 100644 index 0000000..4205ab1 --- /dev/null +++ b/Dockerfile.caddy @@ -0,0 +1,5 @@ +FROM caddy:2-alpine + +EXPOSE 80 443 + +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/Dockerfile.mkcert b/Dockerfile.mkcert new file mode 100644 index 0000000..36a6c21 --- /dev/null +++ b/Dockerfile.mkcert @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ecaef1 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/config/Caddyfile b/config/Caddyfile new file mode 100644 index 0000000..e551ffa --- /dev/null +++ b/config/Caddyfile @@ -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 + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..340f49a --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/scripts/mkcert/entrypoint.sh b/scripts/mkcert/entrypoint.sh new file mode 100644 index 0000000..3fa4f7f --- /dev/null +++ b/scripts/mkcert/entrypoint.sh @@ -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" From cbde3f3e8ffd76ff2b3b75dbc93fa2d42c0a0beb Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:49:51 -0800 Subject: [PATCH 2/5] Test with CI workflow --- .github/workflows/test.yml | 63 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ab0fb80 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,63 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +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=localhost" > .env + echo "UPSTREAM_URL=http://host.docker.internal:3000" >> .env + + - 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/localhost.pem + test -f certs/localhost.key.pem + test -f certs/localhost.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 caddy | grep -q "running" + + - name: Test HTTP redirect + run: | + curl -s -o /dev/null -w "%{http_code}" http://localhost:8080 | grep -q "301\|308" + + - name: Test HTTPS proxies to upstream + run: | + curl -s --cacert certs/localhost.rootCA.pem https://localhost: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 From 20c1c04b44ea18b03f5ee766dfa2e96d289c13b1 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:51:06 -0800 Subject: [PATCH 3/5] Fixed CI workflow --- .github/workflows/test.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab0fb80..9d48938 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,10 +24,10 @@ jobs: echo "UPSTREAM_URL=http://host.docker.internal:3000" >> .env - name: Build images - run: docker-compose build + run: docker compose build - name: Generate certificates - run: docker-compose --profile setup run --rm mkcert + run: docker compose --profile setup run --rm mkcert - name: Verify certificates exist run: | @@ -36,13 +36,13 @@ jobs: test -f certs/localhost.rootCA.pem - name: Start proxy - run: docker-compose up -d + run: docker compose up -d - name: Wait for Caddy to start run: sleep 3 - name: Check Caddy is running - run: docker-compose ps caddy | grep -q "running" + run: docker compose ps caddy | grep -q "running" - name: Test HTTP redirect run: | @@ -54,10 +54,10 @@ jobs: - name: Show logs on failure if: failure() - run: docker-compose logs + run: docker compose logs - name: Stop proxy if: always() run: | - docker-compose down + docker compose down docker rm -f upstream || true From 28940fc2c73ae1a6469736886a6a379173aa1e6e Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:52:52 -0800 Subject: [PATCH 4/5] Fixed checking status of Caddy container in GitHub Actions workflow --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d48938..0d9359b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,7 @@ jobs: run: sleep 3 - name: Check Caddy is running - run: docker compose ps caddy | grep -q "running" + run: docker compose ps --format json | jq -e '.[] | select(.Name == "ssl-proxy") | .State == "running"' - name: Test HTTP redirect run: | From 659df545a6039850ec8e4ae5d27aa306b9ee908b Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:56:23 -0800 Subject: [PATCH 5/5] Check again --- .github/workflows/test.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d9359b..eb01982 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +env: + DOMAIN: sslproxy.stackpop.com + jobs: test: runs-on: ubuntu-latest @@ -20,9 +23,12 @@ jobs: - name: Create .env run: | - echo "DOMAIN=localhost" > .env + 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 @@ -31,9 +37,9 @@ jobs: - name: Verify certificates exist run: | - test -f certs/localhost.pem - test -f certs/localhost.key.pem - test -f certs/localhost.rootCA.pem + 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 @@ -42,15 +48,15 @@ jobs: run: sleep 3 - name: Check Caddy is running - run: docker compose ps --format json | jq -e '.[] | select(.Name == "ssl-proxy") | .State == "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://localhost:8080 | grep -q "301\|308" + 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/localhost.rootCA.pem https://localhost:8443 | grep -q "nginx" + curl -s --cacert certs/${{ env.DOMAIN }}.rootCA.pem https://${{ env.DOMAIN }}:8443 | grep -q "nginx" - name: Show logs on failure if: failure()