refactor: заменить Caddy на Traefik v3
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Изменено
|
||||
|
||||
- Caddy заменён на **Traefik v3** (маршруты в `traefik/dynamic/shop.yml`)
|
||||
- `CADDY_EMAIL` → `ACME_EMAIL` в `.env`
|
||||
|
||||
## [0.20] — 2026-05-16
|
||||
|
||||
### Добавлено
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
# }
|
||||
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
# Интерактивная установка: домен, база данных, .env, Caddyfile
|
||||
# Интерактивная установка: домен, база данных, .env, Traefik
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-Location $PSScriptRoot
|
||||
go run ./cmd/install
|
||||
|
||||
+71
-77
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user