Skip to content
Open
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
22 changes: 17 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -136,27 +136,39 @@ GITEA_ALLOWED_USERS=
PORT=3000
# HOST=0.0.0.0 # Bind address (default: 0.0.0.0). Set to 127.0.0.1 to restrict to localhost only.

# Cloud Deployment (for --profile cloud with Caddy reverse proxy)
# Set your domain and point DNS to your server — Caddy handles TLS automatically
# Cloud Deployment — reverse proxy with automatic HTTPS
# Pick ONE: --profile cloud (Caddy) or --profile traefik (Traefik)
# Set your domain and point DNS to your server.
# DOMAIN=archon.example.com

# ── Caddy-specific (--profile cloud) ──────────────────────────────────────
# Basic Auth (optional) — protects the Web UI and API when exposed to the internet
# Leave empty to disable (e.g. when using IP-based firewall rules instead).
# To enable:
# 1. Generate hash: docker run caddy caddy hash-password --plaintext 'YOUR_PASSWORD'
# 2. Set the variable below (replace admin and the hash):
# CADDY_BASIC_AUTH=basicauth @protected { admin $$2a$$14$$REPLACE_WITH_HASH }

# Form Auth (optional) — HTML login page via Caddy forward_auth + auth-service
# Alternative to CADDY_BASIC_AUTH. Requires: --profile auth in docker compose.
# ── Traefik-specific (--profile traefik) ──────────────────────────────────
# ACME email for Let's Encrypt certificate registration (required for Traefik)
# ACME_EMAIL=you@example.com
#
# Basic Auth (optional) — uncomment the middleware in traefik-dynamic.yml too
# Generate hash: htpasswd -nB admin
# TRAEFIK_BASIC_AUTH=admin:$$2y$$05$$REPLACE_WITH_HTPASSWD_HASH

# ── Form Auth (works with both Caddy and Traefik) ─────────────────────────
# HTML login page via forward auth + auth-service container.
# Requires: --profile auth in docker compose.
# To enable:
# 1. Generate bcrypt hash (requires auth-service container):
# docker compose --profile auth run --rm auth-service node -e \
# "require('bcryptjs').hash('YOUR_PASSWORD', 12).then(h => console.log(h))"
# 2. Generate a random cookie secret:
# docker run --rm node:22-alpine node -e \
# "console.log(require('crypto').randomBytes(32).toString('hex'))"
# 3. Set the variables below and uncomment Option A in Caddyfile
# 3. Caddy: uncomment Option A in Caddyfile
# Traefik: uncomment form-auth in traefik-dynamic.yml + add to app middleware labels
# AUTH_USERNAME=admin
# AUTH_PASSWORD_HASH=$$2b$$12$$REPLACE_WITH_BCRYPT_HASH
# ⚠ Escape every $ as $$ — Docker Compose interprets $ as variable substitution
Expand Down
10 changes: 8 additions & 2 deletions auth-service/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url, 'http://localhost');

// GET /verify — Caddy forward_auth calls this for every protected request
// GET /verify — Caddy forward_auth / Traefik forwardAuth calls this for every protected request
if (req.method === 'GET' && url.pathname === '/verify') {
const cookies = parseCookies(req.headers['cookie']);
const session = verifyCookie(cookies[COOKIE_NAME] ?? '');
Expand All @@ -153,7 +153,13 @@ const server = http.createServer(async (req, res) => {
}
const originalUri = req.headers['x-forwarded-uri'] ?? '/';
const safeRd = isSafeRedirect(originalUri) ? originalUri : '/';
res.writeHead(302, { Location: `/login?rd=${encodeURIComponent(safeRd)}` });
// Build absolute redirect so it works behind both Caddy and Traefik.
// Traefik resolves relative Location against the auth-service origin,
// which is an internal Docker address the browser cannot reach.
const proto = req.headers['x-forwarded-proto'] ?? 'https';
const host = req.headers['x-forwarded-host'] ?? req.headers['host'] ?? '';
const base = host ? `${proto}://${host}` : '';
res.writeHead(302, { Location: `${base}/login?rd=${encodeURIComponent(safeRd)}` });
return res.end();
}

Expand Down
62 changes: 57 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
# docker compose up -d # App with SQLite (default)
# docker compose --profile with-db up -d # App + local PostgreSQL
# docker compose --profile cloud up -d # App + Caddy HTTPS reverse proxy
# docker compose --profile with-db --profile cloud up -d # All three
# docker compose --profile traefik up -d # App + Traefik HTTPS reverse proxy
# docker compose --profile with-db --profile cloud up -d # All three (with Caddy)
#
# Reverse Proxy (pick ONE — they both bind ports 80/443):
# --profile cloud Caddy — simple config, automatic HTTPS. Set DOMAIN in .env.
# --profile traefik Traefik — Docker-native, label-based routing. Set DOMAIN + ACME_EMAIL.
#
# Database:
# SQLite is the default (zero config). For PostgreSQL, either:
Expand All @@ -22,7 +27,7 @@
# Cloud (HTTPS):
# 1. Set DOMAIN=archon.example.com in .env
# 2. Point DNS A record to your server
# 3. Add --profile cloud Caddy handles TLS automatically via Let's Encrypt
# 3. Add --profile cloud (Caddy) OR --profile traefik
#

services:
Expand Down Expand Up @@ -53,6 +58,16 @@ services:
- 8.8.4.4
sysctls:
- net.ipv6.conf.all.disable_ipv6=1
labels:
# -- Traefik labels (ignored unless the traefik container is running) ----
traefik.enable: "true"
traefik.http.routers.archon.rule: "Host(`${DOMAIN}`)"
traefik.http.routers.archon.entrypoints: "websecure"
traefik.http.routers.archon.tls.certresolver: "letsencrypt"
traefik.http.routers.archon.middlewares: "security-headers@file,compress@file"
traefik.http.services.archon.loadbalancer.server.port: "${PORT:-3000}"
# SSE: disable response buffering for streaming endpoints
traefik.http.services.archon.loadbalancer.responseForwarding.flushInterval: "-1"

# -------------------------------------------------------------------------
# PostgreSQL (optional: --profile with-db)
Expand Down Expand Up @@ -81,7 +96,7 @@ services:

# -------------------------------------------------------------------------
# Caddy reverse proxy with automatic HTTPS (optional: --profile cloud)
# Requires DOMAIN set in .env. See Caddyfile for configuration.
# Requires DOMAIN set in .env. See Caddyfile.example for configuration.
# -------------------------------------------------------------------------
caddy:
image: caddy:2-alpine
Expand All @@ -103,8 +118,35 @@ services:
condition: service_healthy

# -------------------------------------------------------------------------
# Auth service — form-based login for Caddy forward_auth (optional: --profile auth)
# Use alongside --profile cloud: docker compose --profile cloud --profile auth up -d
# Traefik reverse proxy with automatic HTTPS (optional: --profile traefik)
# Requires DOMAIN and ACME_EMAIL set in .env. See traefik.yml for config.
# -------------------------------------------------------------------------
traefik:
image: traefik:v3
profiles: ["traefik"]
restart: unless-stopped
environment:
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL: "${ACME_EMAIL}"
TRAEFIK_BASIC_AUTH: "${TRAEFIK_BASIC_AUTH:-}"
ports:
- "80:80"
- "443:443"
volumes:
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
- traefik_letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- archon-network
depends_on:
app:
condition: service_healthy

# -------------------------------------------------------------------------
# Auth service — form-based login via forward auth (optional: --profile auth)
# Works with both Caddy (forward_auth) and Traefik (forwardAuth middleware).
# Use alongside --profile cloud or --profile traefik:
# docker compose --profile cloud --profile auth up -d
# Requires AUTH_USERNAME, AUTH_PASSWORD_HASH, COOKIE_SECRET in .env.
# See docs/docker.md for setup instructions.
# -------------------------------------------------------------------------
Expand All @@ -119,12 +161,22 @@ services:
- "${AUTH_SERVICE_PORT:-9000}"
networks:
- archon-network
labels:
# -- Traefik labels for /login and /logout routing (ignored without traefik) -
traefik.enable: "true"
traefik.http.routers.auth-login.rule: "Host(`${DOMAIN}`) && (Path(`/login`) || Path(`/logout`))"
traefik.http.routers.auth-login.entrypoints: "websecure"
traefik.http.routers.auth-login.tls.certresolver: "letsencrypt"
traefik.http.routers.auth-login.priority: "100"
traefik.http.routers.auth-login.middlewares: "security-headers@file,compress@file"
traefik.http.services.auth-login.loadbalancer.server.port: "${AUTH_SERVICE_PORT:-9000}"

volumes:
archon_data:
postgres_data:
caddy_data:
caddy_config:
traefik_letsencrypt:

networks:
archon-network:
Expand Down
41 changes: 41 additions & 0 deletions traefik-dynamic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Traefik dynamic configuration for Archon.
# Middlewares for security headers, compression, and authentication.

http:
middlewares:
# -- Security Headers -------------------------------------------------------
security-headers:
headers:
customResponseHeaders:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: "max-age=31536000; includeSubDomains"
Server: ""

# -- Compression ------------------------------------------------------------
compress:
compress: {}

# -- Option A: Form-based auth (forward auth to auth-service) ---------------
# Requires: docker compose --profile traefik --profile auth up -d
# Setup: Set AUTH_USERNAME, AUTH_PASSWORD_HASH, COOKIE_SECRET in .env
# See docs/docker.md for hash generation instructions.
# To enable: uncomment this block AND add "form-auth@file" to the app
# router middlewares label in docker-compose.yml.
#
# form-auth:
# forwardAuth:
# address: "http://auth-service:9000/verify"
# authResponseHeaders:
# - X-Auth-User

# -- Option B: Basic auth (browser popup, no extra container) ---------------
# Generate hash: htpasswd -nB admin
# Then set in .env: TRAEFIK_BASIC_AUTH=admin:$$2y$$...
# To enable: uncomment this block AND add "basic-auth@file" to the app
# router middlewares label in docker-compose.yml.
# basic-auth:
# basicAuth:
# users:
# - "{{ env "TRAEFIK_BASIC_AUTH" }}"
43 changes: 43 additions & 0 deletions traefik.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Traefik static configuration for Archon.
# Replaces Caddyfile.example — Traefik handles TLS via Let's Encrypt.
#
# For local testing, comment out certificatesResolvers and set
# entryPoints.websecure to port 80 without TLS.

api:
dashboard: false

entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt

certificatesResolvers:
letsencrypt:
acme:
# email is set via TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL env var
# email: ""
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web

providers:
docker:
exposedByDefault: false
file:
filename: /etc/traefik/dynamic.yml

log:
level: INFO
format: common

accessLog: {}