Skip to content

Commit 312964e

Browse files
authored
SSL proxy with Caddy and mkcert
SSL proxy with Caddy and mkcert
2 parents 3105f56 + 659df54 commit 312964e

File tree

10 files changed

+296
-1
lines changed

10 files changed

+296
-1
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DOMAIN=local.example.com
2+
UPSTREAM_URL=http://host.docker.internal:3000

.github/workflows/test.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
DOMAIN: sslproxy.stackpop.com
11+
12+
jobs:
13+
test:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Start mock upstream
20+
run: |
21+
docker run -d --name upstream -p 3000:80 nginx:alpine
22+
sleep 2
23+
24+
- name: Create .env
25+
run: |
26+
echo "DOMAIN=${{ env.DOMAIN }}" > .env
27+
echo "UPSTREAM_URL=http://host.docker.internal:3000" >> .env
28+
29+
- name: Add test domain to hosts
30+
run: echo "127.0.0.1 ${{ env.DOMAIN }}" | sudo tee -a /etc/hosts
31+
32+
- name: Build images
33+
run: docker compose build
34+
35+
- name: Generate certificates
36+
run: docker compose --profile setup run --rm mkcert
37+
38+
- name: Verify certificates exist
39+
run: |
40+
test -f certs/${{ env.DOMAIN }}.pem
41+
test -f certs/${{ env.DOMAIN }}.key.pem
42+
test -f certs/${{ env.DOMAIN }}.rootCA.pem
43+
44+
- name: Start proxy
45+
run: docker compose up -d
46+
47+
- name: Wait for Caddy to start
48+
run: sleep 3
49+
50+
- name: Check Caddy is running
51+
run: docker compose ps --status running --services | grep -q '^caddy$'
52+
53+
- name: Test HTTP redirect
54+
run: |
55+
curl -s -o /dev/null -w "%{http_code}" http://${{ env.DOMAIN }}:8080 | grep -q "301\|308"
56+
57+
- name: Test HTTPS proxies to upstream
58+
run: |
59+
curl -s --cacert certs/${{ env.DOMAIN }}.rootCA.pem https://${{ env.DOMAIN }}:8443 | grep -q "nginx"
60+
61+
- name: Show logs on failure
62+
if: failure()
63+
run: docker compose logs
64+
65+
- name: Stop proxy
66+
if: always()
67+
run: |
68+
docker compose down
69+
docker rm -f upstream || true

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ certs/
88
.env
99

1010
# AI agents
11-
.claude/
11+
.claude/

CLAUDE.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
A Caddy-based reverse proxy for local development with automatic SSL certificate generation.
8+
9+
## Architecture
10+
11+
- **mkcert container**: Generates SSL certificates on first run, stores in `./certs/`
12+
- **Caddy container**: Reverse proxy with HTTPS, depends on mkcert completing first
13+
- **Bind mount**: Certs stored locally in `./certs/` for easy access
14+
15+
## Key Commands
16+
17+
```bash
18+
# Generate certificates (first time only)
19+
docker-compose --profile setup run --rm mkcert
20+
21+
# Start the proxy
22+
docker-compose up -d --build
23+
24+
# View logs
25+
docker-compose logs -f caddy
26+
27+
# Stop the proxy
28+
docker-compose down
29+
30+
# Regenerate certificates
31+
rm -rf certs/* && docker-compose --profile setup run --rm mkcert
32+
33+
# Install CA on macOS
34+
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./certs/${DOMAIN}.rootCA.pem
35+
```
36+
37+
## Configuration
38+
39+
Environment variables (set in `.env`):
40+
- `DOMAIN` - Domain name for SSL cert (default: `localhost`)
41+
- `UPSTREAM_URL` - URL for your local app (default: `http://host.docker.internal:3000`)
42+
43+
## Files
44+
45+
- **config/Caddyfile**: Proxy rules, TLS config, CSP header removal
46+
- **scripts/mkcert/entrypoint.sh**: Script that generates certs if they don't exist
47+
- **docker-compose.yml**: Service definitions with mkcert → caddy dependency
48+
- **Dockerfile.mkcert**: Alpine image with mkcert for cert generation
49+
- **Dockerfile.caddy**: Minimal Caddy image
50+
51+
## Ports
52+
53+
- `8080` → HTTP (redirects to HTTPS on 8443)
54+
- `8443` → HTTPS (proxies to `${UPSTREAM_URL}`)

Dockerfile.caddy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM caddy:2-alpine
2+
3+
EXPOSE 80 443
4+
5+
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

Dockerfile.mkcert

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM alpine:3.19
2+
3+
ARG MKCERT_VERSION=1.4.4
4+
5+
RUN apk add --no-cache ca-certificates nss-tools curl \
6+
&& ARCH=$(uname -m) \
7+
&& case "$ARCH" in \
8+
aarch64) MKCERT_ARCH="linux-arm64" ;; \
9+
x86_64) MKCERT_ARCH="linux-amd64" ;; \
10+
*) echo "Unsupported architecture: $ARCH" && exit 1 ;; \
11+
esac \
12+
&& curl -L "https://github.com/FiloSottile/mkcert/releases/download/v${MKCERT_VERSION}/mkcert-v${MKCERT_VERSION}-${MKCERT_ARCH}" -o /usr/local/bin/mkcert \
13+
&& chmod +x /usr/local/bin/mkcert
14+
15+
COPY scripts/mkcert/entrypoint.sh /entrypoint.sh
16+
RUN chmod +x /entrypoint.sh
17+
18+
ENTRYPOINT ["/entrypoint.sh"]

README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# SSL Proxy
2+
3+
A Dockerized Caddy reverse proxy with automatic SSL certificate generation for local development.
4+
5+
## Features
6+
7+
- Automatic SSL certificate generation via mkcert
8+
- Strips Content-Security-Policy headers
9+
- HTTP to HTTPS redirect
10+
- Configurable domain and upstream URL
11+
12+
## Quick Start
13+
14+
1. Configure your domain in `.env`:
15+
16+
```
17+
DOMAIN=local.example.com
18+
UPSTREAM_URL=http://host.docker.internal:3000
19+
```
20+
21+
`UPSTREAM_URL` must include the scheme and port.
22+
23+
2. Add to `/etc/hosts`:
24+
25+
```
26+
127.0.0.1 local.example.com
27+
```
28+
29+
3. Generate certificates (first time only):
30+
31+
```bash
32+
docker-compose --profile setup run --rm mkcert
33+
```
34+
35+
4. Install the CA certificate (one-time):
36+
37+
```bash
38+
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./certs/local.example.com.rootCA.pem
39+
```
40+
41+
5. Start the proxy:
42+
43+
```bash
44+
docker-compose up -d
45+
```
46+
47+
6. Visit: `https://local.example.com:8443`
48+
49+
Note (Linux): Requires Docker Engine 20.10+ for `host-gateway` support.
50+
51+
## Configuration
52+
53+
| Variable | Default | Description |
54+
| --------------- | ----------- | ---------------------- |
55+
| `DOMAIN` | `localhost` | Domain for SSL cert |
56+
| `UPSTREAM_URL` | `http://host.docker.internal:3000` | URL for your local app |
57+
58+
## Ports
59+
60+
- `8080` - HTTP (redirects to HTTPS)
61+
- `8443` - HTTPS
62+
63+
## Layout
64+
65+
```
66+
├── config/Caddyfile # Caddy configuration
67+
├── scripts/mkcert/entrypoint.sh # Cert generation script
68+
├── docker-compose.yml # Service definitions
69+
├── Dockerfile.caddy # Caddy image
70+
├── Dockerfile.mkcert # Certificate generator
71+
└── .env # Your configuration
72+
```

config/Caddyfile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
:80 {
2+
redir https://{$DOMAIN:localhost}:8443{uri} permanent
3+
}
4+
5+
{$DOMAIN:localhost} {
6+
tls /etc/caddy/certs/{$DOMAIN:localhost}.pem /etc/caddy/certs/{$DOMAIN:localhost}.key.pem
7+
8+
reverse_proxy {$UPSTREAM_URL:http://host.docker.internal:3000} {
9+
header_down -Content-Security-Policy
10+
header_down -Content-Security-Policy-Report-Only
11+
}
12+
13+
log {
14+
output stdout
15+
format console
16+
}
17+
}

docker-compose.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
services:
2+
mkcert:
3+
build:
4+
context: .
5+
dockerfile: Dockerfile.mkcert
6+
container_name: mkcert
7+
profiles:
8+
- setup
9+
environment:
10+
- DOMAIN=${DOMAIN:-localhost}
11+
volumes:
12+
- ./certs:/certs
13+
14+
caddy:
15+
build:
16+
context: .
17+
dockerfile: Dockerfile.caddy
18+
container_name: ssl-proxy
19+
ports:
20+
- "8080:80"
21+
- "8443:443"
22+
environment:
23+
- DOMAIN=${DOMAIN:-localhost}
24+
- UPSTREAM_URL=${UPSTREAM_URL:-http://host.docker.internal:3000}
25+
extra_hosts:
26+
- "host.docker.internal:host-gateway"
27+
volumes:
28+
- ./config/Caddyfile:/etc/caddy/Caddyfile:ro
29+
- ./certs:/etc/caddy/certs:ro
30+
- caddy_data:/data
31+
- caddy_config:/config
32+
restart: unless-stopped
33+
34+
volumes:
35+
caddy_data:
36+
caddy_config:

scripts/mkcert/entrypoint.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/bin/sh
2+
set -e
3+
4+
DOMAIN="${DOMAIN:-localhost}"
5+
CERT_FILE="/certs/${DOMAIN}.pem"
6+
KEY_FILE="/certs/${DOMAIN}.key.pem"
7+
CA_FILE="/certs/${DOMAIN}.rootCA.pem"
8+
9+
if [ ! -f "$CERT_FILE" ]; then
10+
echo "Generating SSL certificate for ${DOMAIN}..."
11+
mkcert -install
12+
mkcert -cert-file "$CERT_FILE" \
13+
-key-file "$KEY_FILE" \
14+
"$DOMAIN"
15+
cp "$(mkcert -CAROOT)/rootCA.pem" "$CA_FILE"
16+
echo "=== Certificate generated ==="
17+
else
18+
echo "Certificate already exists for ${DOMAIN}, skipping generation."
19+
fi
20+
21+
echo "Install CA on macOS:"
22+
echo " sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./certs/${DOMAIN}.rootCA.pem"

0 commit comments

Comments
 (0)