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
|
SITE_DOMAIN=localhost
|
||||||
CADDY_EMAIL=admin@localhost
|
ACME_EMAIL=admin@localhost
|
||||||
HTTP_PORT=80
|
HTTP_PORT=80
|
||||||
HTTPS_PORT=443
|
HTTPS_PORT=443
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
### Добавлено
|
### Добавлено
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
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
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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