diff --git a/.gitignore b/.gitignore index 64f8ef0..57ab061 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,9 @@ # Ignore key files for decrypting credentials and more. /config/*.key +# Ignore Traefik certificate storage +acme.json + /app/assets/builds/* !/app/assets/builds/.keep diff --git a/README.md b/README.md index 3fdebb5..77ce489 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,40 @@ bin/dev ## Docker -### One-command deploy +### Production with SSL + +Use Docker Compose with Traefik for HTTPS. Create a `.env` file with SSL and app config: + +**Let's Encrypt (wildcard):** + +```bash +# .env +DOMAIN=uptime.example.com +WILDCARD_DOMAIN=*.example.com +DNS_PROVIDER=cloudflare +CF_DNS_API_TOKEN=your-token +LETSENCRYPT_EMAIL=you@example.com +# App config (see table above) +ADMIN_EMAILS=admin@example.com,manager@example.com + +docker compose up -d +``` + +**Cloudflare SSL (no cert management):** + +```bash +# .env +DOMAIN=uptime.example.com +ENTRYPOINT=web +# App config (see table above) +ADMIN_EMAILS=admin@example.com + +docker compose up -d +``` + +If `ENTRYPOINT` is not set, Traefik defaults to `websecure` (HTTPS) with automatic Let's Encrypt DNS challenge. [Supported DNS providers](https://doc.traefik.io/traefik/https/acme/#dnschallenge) + +### One-command deploy (local) ```bash docker run -d -p 3000:80 \ diff --git a/config/traefik/dynamic.yml b/config/traefik/dynamic.yml new file mode 100644 index 0000000..2d15733 --- /dev/null +++ b/config/traefik/dynamic.yml @@ -0,0 +1,20 @@ +# Dynamic configuration +http: + routers: + uptimer: + rule: 'Host(`{{ env "DOMAIN" }}`)' + entryPoints: + - '{{ env "ENTRYPOINT" "websecure" }}' + service: uptimer + tls: + certResolver: letsencrypt + domains: + - main: '{{ env "DOMAIN" }}' + sans: + - '{{ env "WILDCARD_DOMAIN" }}' + + services: + uptimer: + loadBalancer: + servers: + - url: "http://up-timer:80" diff --git a/config/traefik/traefik.yml b/config/traefik/traefik.yml new file mode 100644 index 0000000..d728497 --- /dev/null +++ b/config/traefik/traefik.yml @@ -0,0 +1,33 @@ +# Static configuration +global: + sendAnonymousUsage: false + +api: + dashboard: false + +entryPoints: + web: + address: ":80" + http: + redirections: + entryPoint: + to: websecure + scheme: https + websecure: + address: ":443" + +providers: + file: + filename: /etc/traefik/dynamic.yml + watch: true + +certificatesResolvers: + letsencrypt: + acme: + email: '{{ env "LETSENCRYPT_EMAIL" }}' + storage: /letsencrypt/acme.json + dnsChallenge: + provider: '{{ env "DNS_PROVIDER" "cloudflare" }}' + resolvers: + - "1.1.1.1:53" + - "8.8.8.8:53" diff --git a/docker-compose.yml b/docker-compose.yml index bcdeaaa..c3f9a54 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,32 @@ services: + traefik: + image: traefik:v3 + container_name: traefik + ports: + - "80:80" + - "443:443" + volumes: + - ./config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro + - ./config/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro + - traefik-certs:/letsencrypt + environment: + - ENTRYPOINT=${ENTRYPOINT:-websecure} + - DNS_PROVIDER=${DNS_PROVIDER:-cloudflare} + - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN} + - LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL} + - DOMAIN=${DOMAIN} + - WILDCARD_DOMAIN=${WILDCARD_DOMAIN} + restart: unless-stopped + up-timer: image: binilsn/up-timer:latest build: . - ports: - - "3000:80" environment: - RAILS_ENV=production - RAILS_MASTER_KEY=${RAILS_MASTER_KEY} - ADMIN_EMAILS=${ADMIN_EMAILS} - SOLID_QUEUE_IN_PUMA=true + - APP_HOST=${DOMAIN} volumes: - up-timer-storage:/rails/storage - up-timer-db:/rails/db @@ -17,7 +35,14 @@ services: interval: 10s timeout: 3s retries: 3 + labels: + - "traefik.enable=true" + - "traefik.http.routers.uptimer.rule=Host(`${DOMAIN}`)" + - "traefik.http.routers.uptimer.entrypoints=websecure" + - "traefik.http.routers.uptimer.tls.certresolver=letsencrypt" + restart: unless-stopped volumes: up-timer-storage: up-timer-db: + traefik-certs: