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
+61 -67
View File
@@ -11,7 +11,7 @@ import (
type Config struct {
SiteDomain string
CaddyEmail string
AcmeEmail string
HTTPPort string
HTTPSPort string
UseDockerDB bool
@@ -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"