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
CADDY_EMAIL=admin@localhost
ACME_EMAIL=admin@localhost
HTTP_PORT=80
HTTPS_PORT=443
+7
View File
@@ -1,5 +1,12 @@
# Changelog
## [Unreleased]
### Изменено
- Caddy заменён на **Traefik v3** (маршруты в `traefik/dynamic/shop.yml`)
- `CADDY_EMAIL``ACME_EMAIL` в `.env`
## [0.20] — 2026-05-16
### Добавлено
+3 -3
View File
@@ -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
```
-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
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
+17 -11
View File
@@ -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:
+1 -1
View File
@@ -1,4 +1,4 @@
# Интерактивная установка: домен, база данных, .env, Caddyfile
# Интерактивная установка: домен, база данных, .env, Traefik
$ErrorActionPreference = "Stop"
Set-Location $PSScriptRoot
go run ./cmd/install
+71 -77
View File
@@ -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 {
+1 -1
View File
@@ -42,7 +42,7 @@
<footer class="site-footer">
<div class="container footer-inner">
<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>
</footer>
</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"