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
+71 -77
View File
@@ -10,17 +10,17 @@ 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
DBHost string DBHost string
DBPort string DBPort string
DBUser string DBUser string
DBPassword string DBPassword string
DBName string DBName string
DBSSLMode string DBSSLMode string
} }
func RunInteractive(root string) (Config, error) { func RunInteractive(root string) (Config, error) {
@@ -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"