refactor: заменить Caddy на Traefik v3

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shop
2026-05-16 20:27:54 +03:00
parent 11633fbe6e
commit 3419d90e61
11 changed files with 135 additions and 128 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
# Скопируйте в .env или запустите: go run ./cmd/install # Скопируйте в .env или запустите: ./install.sh
SITE_DOMAIN=localhost SITE_DOMAIN=localhost
CADDY_EMAIL=admin@localhost ACME_EMAIL=admin@localhost
HTTP_PORT=80 HTTP_PORT=80
HTTPS_PORT=443 HTTPS_PORT=443
+7
View File
@@ -1,5 +1,12 @@
# Changelog # Changelog
## [Unreleased]
### Изменено
- Caddy заменён на **Traefik v3** (маршруты в `traefik/dynamic/shop.yml`)
- `CADDY_EMAIL``ACME_EMAIL` в `.env`
## [0.20] — 2026-05-16 ## [0.20] — 2026-05-16
### Добавлено ### Добавлено
+3 -3
View File
@@ -2,7 +2,7 @@
**Версия:** `0.20` · [Релизы](https://git.evilfox.cc/test/shop3/releases) **Версия:** `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 Репозиторий: 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 git clone https://git.evilfox.cc/test/shop3.git
cd shop3 cd shop3
# 2. Установщик (домен + база данных → .env и caddy/Caddyfile) # 2. Установщик (домен + база данных → .env и traefik/dynamic/shop.yml)
chmod +x install.sh check.sh chmod +x install.sh check.sh
./install.sh ./install.sh
@@ -59,7 +59,7 @@ docker compose up --build -d
```bash ```bash
docker compose ps # статус контейнеров docker compose ps # статус контейнеров
docker compose logs -f # логи docker compose logs -f traefik app # логи прокси и приложения
curl -s http://localhost/health | jq curl -s http://localhost/health | jq
curl -s http://localhost/version | jq curl -s http://localhost/version | jq
``` ```
-32
View File
@@ -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
# }
+1 -1
View File
@@ -61,7 +61,7 @@ post_start_check() {
if curl -sf "http://127.0.0.1:${HTTP_PORT:-80}/health" >/dev/null 2>&1; then if curl -sf "http://127.0.0.1:${HTTP_PORT:-80}/health" >/dev/null 2>&1; then
echo " ✓ /health: OK" echo " ✓ /health: OK"
else else
echo " ✗ /health: не отвечает — docker compose logs app caddy" echo " ✗ /health: не отвечает — docker compose logs app traefik"
exit 1 exit 1
fi fi
elif docker compose exec -T app wget -qO- http://127.0.0.1:8080/health >/dev/null 2>&1; then elif docker compose exec -T app wget -qO- http://127.0.0.1:8080/health >/dev/null 2>&1; then
+17 -11
View File
@@ -53,26 +53,33 @@ services:
environment: environment:
APP_PORT: "8080" APP_PORT: "8080"
DATABASE_URL: postgres://${POSTGRES_USER:-shop}:${POSTGRES_PASSWORD:-shop_secret}@postgres:5432/${POSTGRES_DB:-shopdb}?sslmode=require DATABASE_URL: postgres://${POSTGRES_USER:-shop}:${POSTGRES_PASSWORD:-shop_secret}@postgres:5432/${POSTGRES_DB:-shopdb}?sslmode=require
COOKIE_SECURE: ${COOKIE_SECURE:-false}
networks: networks:
- backend - backend
- frontend - frontend
restart: unless-stopped restart: unless-stopped
caddy: traefik:
image: caddy:2-alpine image: traefik:v3.2
container_name: shop-caddy container_name: shop-traefik
depends_on: depends_on:
- app - app
environment: command:
SITE_DOMAIN: ${SITE_DOMAIN:-localhost} - --log.level=INFO
CADDY_EMAIL: ${CADDY_EMAIL:-admin@localhost} - --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: ports:
- "${HTTP_PORT:-80}:80" - "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443" - "${HTTPS_PORT:-443}:443"
volumes: volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro - ./traefik/dynamic:/etc/traefik/dynamic:ro
- caddy_data:/data - traefik_letsencrypt:/letsencrypt
- caddy_config:/config
networks: networks:
- frontend - frontend
restart: unless-stopped restart: unless-stopped
@@ -80,8 +87,7 @@ services:
volumes: volumes:
postgres_data: postgres_data:
postgres_ssl: postgres_ssl:
caddy_data: traefik_letsencrypt:
caddy_config:
networks: networks:
backend: backend:
+1 -1
View File
@@ -1,4 +1,4 @@
# Интерактивная установка: домен, база данных, .env, Caddyfile # Интерактивная установка: домен, база данных, .env, Traefik
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
Set-Location $PSScriptRoot Set-Location $PSScriptRoot
go run ./cmd/install go run ./cmd/install
+61 -67
View File
@@ -11,7 +11,7 @@ import (
type Config struct { type Config struct {
SiteDomain string SiteDomain string
CaddyEmail string AcmeEmail string
HTTPPort string HTTPPort string
HTTPSPort string HTTPSPort string
UseDockerDB bool UseDockerDB bool
@@ -31,7 +31,7 @@ func RunInteractive(root string) (Config, error) {
cfg := Config{} cfg := Config{}
cfg.SiteDomain = ask(in, "Домен сайта (например shop.example.com, Enter = localhost)", "localhost") 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.HTTPPort = ask(in, "HTTP порт", "80")
cfg.HTTPSPort = ask(in, "HTTPS порт", "443") cfg.HTTPSPort = ask(in, "HTTPS порт", "443")
@@ -58,7 +58,7 @@ func RunInteractive(root string) (Config, error) {
return cfg, err return cfg, err
} }
fmt.Println("\n✓ Созданы файлы: .env, caddy/Caddyfile") fmt.Println("\n✓ Созданы файлы: .env, traefik/dynamic/shop.yml")
fmt.Println("\nДальше (на сервере):") fmt.Println("\nДальше (на сервере):")
fmt.Println(" docker compose up --build -d") fmt.Println(" docker compose up --build -d")
fmt.Println(" ./check.sh --after-start") fmt.Println(" ./check.sh --after-start")
@@ -72,26 +72,30 @@ func RunInteractive(root string) (Config, error) {
func WriteFiles(root string, cfg Config) error { func WriteFiles(root string, cfg Config) error {
envPath := filepath.Join(root, ".env") 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 { if err := os.WriteFile(envPath, []byte(buildEnv(cfg)), 0o600); err != nil {
return fmt.Errorf("write .env: %w", err) 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 return err
} }
if err := os.WriteFile(caddyPath, []byte(buildCaddyfile(cfg)), 0o644); err != nil { if err := os.WriteFile(traefikPath, []byte(buildTraefikDynamic(cfg)), 0o644); err != nil {
return fmt.Errorf("write Caddyfile: %w", err) return fmt.Errorf("write traefik config: %w", err)
} }
return nil return nil
} }
func buildEnv(cfg Config) string { func buildEnv(cfg Config) string {
dbURL := DatabaseURL(cfg) dbURL := DatabaseURL(cfg)
cookieSecure := "false"
if !useLocalDomain(cfg.SiteDomain) {
cookieSecure = "true"
}
lines := []string{ lines := []string{
"# Сгенерировано установщиком ShopNova", "# Сгенерировано установщиком ShopNova",
fmt.Sprintf("SITE_DOMAIN=%s", cfg.SiteDomain), 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("HTTP_PORT=%s", cfg.HTTPPort),
fmt.Sprintf("HTTPS_PORT=%s", cfg.HTTPSPort), fmt.Sprintf("HTTPS_PORT=%s", cfg.HTTPSPort),
"", "",
@@ -101,6 +105,8 @@ func buildEnv(cfg Config) string {
"", "",
fmt.Sprintf("DATABASE_URL=%s", dbURL), fmt.Sprintf("DATABASE_URL=%s", dbURL),
"APP_PORT=8080", "APP_PORT=8080",
fmt.Sprintf("COOKIE_SECURE=%s", cookieSecure),
"SESSION_TTL_HOURS=168",
"", "",
fmt.Sprintf("DB_HOST=%s", cfg.DBHost), fmt.Sprintf("DB_HOST=%s", cfg.DBHost),
fmt.Sprintf("DB_PORT=%s", cfg.DBPort), fmt.Sprintf("DB_PORT=%s", cfg.DBPort),
@@ -122,68 +128,56 @@ func DatabaseURL(cfg Config) string {
return u.String() return u.String()
} }
func buildCaddyfile(cfg Config) string { func buildTraefikDynamic(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)
}
domain := strings.TrimSpace(cfg.SiteDomain) domain := strings.TrimSpace(cfg.SiteDomain)
return fmt.Sprintf(`{ if useLocalDomain(domain) {
email %s return fmt.Sprintf(`# Сгенерировано установщиком ShopNova (localhost)
} http:
routers:
%s { shop:
encode gzip zstd rule: "Host(`+"`%s`"+`)"
entryPoints: [web]
@api path /health /version middlewares: [gzip]
handle @api { service: shop
reverse_proxy app:8080 middlewares:
gzip:
compress: {}
services:
shop:
loadBalancer:
servers:
- url: "http://app:8080"
`, domain)
} }
handle /static/* { return fmt.Sprintf(`# Сгенерировано установщиком ShopNova
reverse_proxy app:8080 http:
} routers:
shop-http:
handle { rule: "Host(`+"`%s`"+`)"
reverse_proxy app:8080 entryPoints: [web]
} middlewares: [redirect-https]
service: shop
log { shop:
output stdout rule: "Host(`+"`%s`"+`)"
format console entryPoints: [websecure]
} middlewares: [gzip]
} service: shop
tls:
http://%s { certResolver: letsencrypt
redir https://{host}{uri} permanent middlewares:
} redirect-https:
`, email, domain, domain) redirectScheme:
scheme: https
permanent: true
gzip:
compress: {}
services:
shop:
loadBalancer:
servers:
- url: "http://app:8080"
`, domain, domain)
} }
func useLocalDomain(d string) bool { func useLocalDomain(d string) bool {
+1 -1
View File
@@ -42,7 +42,7 @@
<footer class="site-footer"> <footer class="site-footer">
<div class="container footer-inner"> <div class="container footer-inner">
<p class="footer-brand">ShopNova</p> <p class="footer-brand">ShopNova</p>
<p class="footer-copy">© 2026 Интернет-магазин. Go + PostgreSQL + Caddy.</p> <p class="footer-copy">© 2026 Интернет-магазин. Go + PostgreSQL + Traefik.</p>
</div> </div>
</footer> </footer>
</body> </body>
+16
View File
@@ -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"
+16
View File
@@ -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"