From 3419d90e617ec30b1a114437c3a253e7881f9179 Mon Sep 17 00:00:00 2001 From: shop Date: Sat, 16 May 2026 20:27:54 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20Caddy=20=D0=BD=D0=B0=20Traefik=20v3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .env.example | 4 +- CHANGELOG.md | 7 ++ README.md | 6 +- caddy/Caddyfile | 32 ------- check.sh | 2 +- docker-compose.yml | 28 +++--- install.ps1 | 2 +- internal/setup/setup.go | 148 ++++++++++++++--------------- internal/web/templates/layout.html | 2 +- traefik/dynamic/shop.yml | 16 ++++ traefik/dynamic/shop.yml.example | 16 ++++ 11 files changed, 135 insertions(+), 128 deletions(-) delete mode 100644 caddy/Caddyfile create mode 100644 traefik/dynamic/shop.yml create mode 100644 traefik/dynamic/shop.yml.example diff --git a/.env.example b/.env.example index 200886d..c26a206 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ -# Скопируйте в .env или запустите: go run ./cmd/install +# Скопируйте в .env или запустите: ./install.sh SITE_DOMAIN=localhost -CADDY_EMAIL=admin@localhost +ACME_EMAIL=admin@localhost HTTP_PORT=80 HTTPS_PORT=443 diff --git a/CHANGELOG.md b/CHANGELOG.md index a580583..e6a70f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [Unreleased] + +### Изменено + +- Caddy заменён на **Traefik v3** (маршруты в `traefik/dynamic/shop.yml`) +- `CADDY_EMAIL` → `ACME_EMAIL` в `.env` + ## [0.20] — 2026-05-16 ### Добавлено diff --git a/README.md b/README.md index 33116b4..16adb6e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Версия:** `0.20` · [Релизы](https://git.evilfox.cc/test/shop3/releases) -Главная страница интернет-магазина на Go с PostgreSQL 17 (SSL), reverse proxy Caddy и Docker Compose. +Главная страница интернет-магазина на Go с PostgreSQL 17 (SSL), reverse proxy **Traefik** и Docker Compose. Репозиторий: https://git.evilfox.cc/test/shop3.git @@ -21,7 +21,7 @@ git clone --branch v0.20 https://git.evilfox.cc/test/shop3.git git clone https://git.evilfox.cc/test/shop3.git cd shop3 -# 2. Установщик (домен + база данных → .env и caddy/Caddyfile) +# 2. Установщик (домен + база данных → .env и traefik/dynamic/shop.yml) chmod +x install.sh check.sh ./install.sh @@ -59,7 +59,7 @@ docker compose up --build -d ```bash docker compose ps # статус контейнеров -docker compose logs -f # логи +docker compose logs -f traefik app # логи прокси и приложения curl -s http://localhost/health | jq curl -s http://localhost/version | jq ``` diff --git a/caddy/Caddyfile b/caddy/Caddyfile deleted file mode 100644 index 4b7d88b..0000000 --- a/caddy/Caddyfile +++ /dev/null @@ -1,32 +0,0 @@ -{ - email {$CADDY_EMAIL:admin@localhost} -} - -# HTTP → приложение (для локальной разработки) -:80 { - encode gzip zstd - - @api path /health /version - handle @api { - reverse_proxy app:8080 - } - - handle /static/* { - reverse_proxy app:8080 - } - - handle { - reverse_proxy app:8080 - } - - log { - output stdout - format console - } -} - -# HTTPS (раскомментируйте домен для продакшена) -# {$SITE_DOMAIN} { -# encode gzip zstd -# reverse_proxy app:8080 -# } diff --git a/check.sh b/check.sh index 09638ed..4bbc5a4 100755 --- a/check.sh +++ b/check.sh @@ -61,7 +61,7 @@ post_start_check() { if curl -sf "http://127.0.0.1:${HTTP_PORT:-80}/health" >/dev/null 2>&1; then echo " ✓ /health: OK" else - echo " ✗ /health: не отвечает — docker compose logs app caddy" + echo " ✗ /health: не отвечает — docker compose logs app traefik" exit 1 fi elif docker compose exec -T app wget -qO- http://127.0.0.1:8080/health >/dev/null 2>&1; then diff --git a/docker-compose.yml b/docker-compose.yml index e62a905..8161360 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,26 +53,33 @@ services: environment: APP_PORT: "8080" DATABASE_URL: postgres://${POSTGRES_USER:-shop}:${POSTGRES_PASSWORD:-shop_secret}@postgres:5432/${POSTGRES_DB:-shopdb}?sslmode=require + COOKIE_SECURE: ${COOKIE_SECURE:-false} networks: - backend - frontend restart: unless-stopped - caddy: - image: caddy:2-alpine - container_name: shop-caddy + traefik: + image: traefik:v3.2 + container_name: shop-traefik depends_on: - app - environment: - SITE_DOMAIN: ${SITE_DOMAIN:-localhost} - CADDY_EMAIL: ${CADDY_EMAIL:-admin@localhost} + command: + - --log.level=INFO + - --accesslog=true + - --providers.file.directory=/etc/traefik/dynamic + - --providers.file.watch=true + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL:-admin@localhost} + - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json + - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web ports: - "${HTTP_PORT:-80}:80" - "${HTTPS_PORT:-443}:443" volumes: - - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config + - ./traefik/dynamic:/etc/traefik/dynamic:ro + - traefik_letsencrypt:/letsencrypt networks: - frontend restart: unless-stopped @@ -80,8 +87,7 @@ services: volumes: postgres_data: postgres_ssl: - caddy_data: - caddy_config: + traefik_letsencrypt: networks: backend: diff --git a/install.ps1 b/install.ps1 index ba6391f..58e7764 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,4 +1,4 @@ -# Интерактивная установка: домен, база данных, .env, Caddyfile +# Интерактивная установка: домен, база данных, .env, Traefik $ErrorActionPreference = "Stop" Set-Location $PSScriptRoot go run ./cmd/install diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 749dee3..0ba425d 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -10,17 +10,17 @@ import ( ) type Config struct { - SiteDomain string - CaddyEmail string - HTTPPort string - HTTPSPort string - UseDockerDB bool - DBHost string - DBPort string - DBUser string - DBPassword string - DBName string - DBSSLMode string + SiteDomain string + AcmeEmail string + HTTPPort string + HTTPSPort string + UseDockerDB bool + DBHost string + DBPort string + DBUser string + DBPassword string + DBName string + DBSSLMode string } func RunInteractive(root string) (Config, error) { @@ -31,7 +31,7 @@ func RunInteractive(root string) (Config, error) { cfg := Config{} cfg.SiteDomain = ask(in, "Домен сайта (например shop.example.com, Enter = localhost)", "localhost") - cfg.CaddyEmail = ask(in, "Email для Let's Encrypt (Caddy)", "admin@localhost") + cfg.AcmeEmail = ask(in, "Email для Let's Encrypt (Traefik)", "admin@localhost") cfg.HTTPPort = ask(in, "HTTP порт", "80") cfg.HTTPSPort = ask(in, "HTTPS порт", "443") @@ -58,7 +58,7 @@ func RunInteractive(root string) (Config, error) { return cfg, err } - fmt.Println("\n✓ Созданы файлы: .env, caddy/Caddyfile") + fmt.Println("\n✓ Созданы файлы: .env, traefik/dynamic/shop.yml") fmt.Println("\nДальше (на сервере):") fmt.Println(" docker compose up --build -d") fmt.Println(" ./check.sh --after-start") @@ -72,26 +72,30 @@ func RunInteractive(root string) (Config, error) { func WriteFiles(root string, cfg Config) error { envPath := filepath.Join(root, ".env") - caddyPath := filepath.Join(root, "caddy", "Caddyfile") + traefikPath := filepath.Join(root, "traefik", "dynamic", "shop.yml") if err := os.WriteFile(envPath, []byte(buildEnv(cfg)), 0o600); err != nil { return fmt.Errorf("write .env: %w", err) } - if err := os.MkdirAll(filepath.Dir(caddyPath), 0o755); err != nil { + if err := os.MkdirAll(filepath.Dir(traefikPath), 0o755); err != nil { return err } - if err := os.WriteFile(caddyPath, []byte(buildCaddyfile(cfg)), 0o644); err != nil { - return fmt.Errorf("write Caddyfile: %w", err) + if err := os.WriteFile(traefikPath, []byte(buildTraefikDynamic(cfg)), 0o644); err != nil { + return fmt.Errorf("write traefik config: %w", err) } return nil } func buildEnv(cfg Config) string { dbURL := DatabaseURL(cfg) + cookieSecure := "false" + if !useLocalDomain(cfg.SiteDomain) { + cookieSecure = "true" + } lines := []string{ "# Сгенерировано установщиком ShopNova", fmt.Sprintf("SITE_DOMAIN=%s", cfg.SiteDomain), - fmt.Sprintf("CADDY_EMAIL=%s", cfg.CaddyEmail), + fmt.Sprintf("ACME_EMAIL=%s", cfg.AcmeEmail), fmt.Sprintf("HTTP_PORT=%s", cfg.HTTPPort), fmt.Sprintf("HTTPS_PORT=%s", cfg.HTTPSPort), "", @@ -101,6 +105,8 @@ func buildEnv(cfg Config) string { "", fmt.Sprintf("DATABASE_URL=%s", dbURL), "APP_PORT=8080", + fmt.Sprintf("COOKIE_SECURE=%s", cookieSecure), + "SESSION_TTL_HOURS=168", "", fmt.Sprintf("DB_HOST=%s", cfg.DBHost), fmt.Sprintf("DB_PORT=%s", cfg.DBPort), @@ -122,68 +128,56 @@ func DatabaseURL(cfg Config) string { return u.String() } -func buildCaddyfile(cfg Config) string { - email := cfg.CaddyEmail - if useLocalDomain(cfg.SiteDomain) { - return fmt.Sprintf(`{ - email %s -} - -:80 { - encode gzip zstd - - @api path /health /version - handle @api { - reverse_proxy app:8080 - } - - handle /static/* { - reverse_proxy app:8080 - } - - handle { - reverse_proxy app:8080 - } - - log { - output stdout - format console - } -} -`, email) - } - +func buildTraefikDynamic(cfg Config) string { domain := strings.TrimSpace(cfg.SiteDomain) - return fmt.Sprintf(`{ - email %s -} - -%s { - encode gzip zstd - - @api path /health /version - handle @api { - reverse_proxy app:8080 + if useLocalDomain(domain) { + return fmt.Sprintf(`# Сгенерировано установщиком ShopNova (localhost) +http: + routers: + shop: + rule: "Host(`+"`%s`"+`)" + entryPoints: [web] + middlewares: [gzip] + service: shop + middlewares: + gzip: + compress: {} + services: + shop: + loadBalancer: + servers: + - url: "http://app:8080" +`, domain) } - handle /static/* { - reverse_proxy app:8080 - } - - handle { - reverse_proxy app:8080 - } - - log { - output stdout - format console - } -} - -http://%s { - redir https://{host}{uri} permanent -} -`, email, domain, domain) + return fmt.Sprintf(`# Сгенерировано установщиком ShopNova +http: + routers: + shop-http: + rule: "Host(`+"`%s`"+`)" + entryPoints: [web] + middlewares: [redirect-https] + service: shop + shop: + rule: "Host(`+"`%s`"+`)" + entryPoints: [websecure] + middlewares: [gzip] + service: shop + tls: + certResolver: letsencrypt + middlewares: + redirect-https: + redirectScheme: + scheme: https + permanent: true + gzip: + compress: {} + services: + shop: + loadBalancer: + servers: + - url: "http://app:8080" +`, domain, domain) } func useLocalDomain(d string) bool { diff --git a/internal/web/templates/layout.html b/internal/web/templates/layout.html index c292063..abe8f57 100644 --- a/internal/web/templates/layout.html +++ b/internal/web/templates/layout.html @@ -42,7 +42,7 @@ diff --git a/traefik/dynamic/shop.yml b/traefik/dynamic/shop.yml new file mode 100644 index 0000000..92eaca8 --- /dev/null +++ b/traefik/dynamic/shop.yml @@ -0,0 +1,16 @@ +# Локальная разработка (перезаписывается установщиком ./install.sh) +http: + routers: + shop: + rule: "Host(`localhost`)" + entryPoints: [web] + middlewares: [gzip] + service: shop + middlewares: + gzip: + compress: {} + services: + shop: + loadBalancer: + servers: + - url: "http://app:8080" diff --git a/traefik/dynamic/shop.yml.example b/traefik/dynamic/shop.yml.example new file mode 100644 index 0000000..145949a --- /dev/null +++ b/traefik/dynamic/shop.yml.example @@ -0,0 +1,16 @@ +# Пример — установщик создаёт shop.yml автоматически +http: + routers: + shop: + rule: "Host(`localhost`)" + entryPoints: [web] + middlewares: [gzip] + service: shop + middlewares: + gzip: + compress: {} + services: + shop: + loadBalancer: + servers: + - url: "http://app:8080"