Add Ubuntu deployment: scripts, SSL, systemd and setup.sh

This commit is contained in:
shop
2026-06-25 16:47:01 +03:00
parent ee5688f722
commit 8a7f79a70c
19 changed files with 387 additions and 58 deletions
+15
View File
@@ -0,0 +1,15 @@
# Git
.git
.gitignore
# Env and secrets
.env
# Build artifacts
*.exe
shop
/vendor/
# Docs and dev
README.md
scripts/
+10 -2
View File
@@ -10,5 +10,13 @@ ADMIN_PASSWORD=change_me
# Секрет для сессий (обязательно смените в продакшене) # Секрет для сессий (обязательно смените в продакшене)
SESSION_SECRET=your_random_secret_key_here SESSION_SECRET=your_random_secret_key_here
# Domain for Caddy SSL (production) # Часовой пояс сервера (Ubuntu)
DOMAIN=shop.example.com TZ=Europe/Moscow
# --- Продакшен на Ubuntu с доменом ---
# DOMAIN=shop.example.com
# CADDY_EMAIL=admin@example.com
# CADDYFILE=./Caddyfile.prod
# Для доступа по IP без домена оставьте DOMAIN закомментированным
# и используйте Caddyfile по умолчанию (HTTP на порту 80)
+2
View File
@@ -0,0 +1,2 @@
* text=auto eol=lf
*.sh text eol=lf
+2 -11
View File
@@ -1,14 +1,5 @@
{ # HTTP — доступ по IP или localhost (без домена)
admin localhost:2019
}
# Локальная разработка — HTTP на порту 80
:80 { :80 {
encode gzip
reverse_proxy app:8080 reverse_proxy app:8080
} }
# Продакшен — автоматический SSL от Let's Encrypt
# Раскомментируйте и закомментируйте блок :80 выше
# {$DOMAIN} {
# reverse_proxy app:8080
# }
+8
View File
@@ -0,0 +1,8 @@
{
email {$CADDY_EMAIL}
}
{$DOMAIN} {
encode gzip
reverse_proxy app:8080
}
+2
View File
@@ -16,6 +16,8 @@ FROM alpine:3.21
RUN apk add --no-cache ca-certificates tzdata RUN apk add --no-cache ca-certificates tzdata
ENV TZ=UTC
WORKDIR /app WORKDIR /app
COPY --from=builder /shop /app/shop COPY --from=builder /shop /app/shop
+39
View File
@@ -0,0 +1,39 @@
.PHONY: help install deploy up down logs restart ssl update
help:
@echo "Shop — команды для Ubuntu"
@echo ""
@echo " make install — установить Docker (sudo)"
@echo " make deploy — первый запуск проекта"
@echo " make up — поднять контейнеры"
@echo " make down — остановить контейнеры"
@echo " make logs — логи всех сервисов"
@echo " make restart — перезапуск"
@echo " make ssl — включить SSL (DOMAIN=... EMAIL=...)"
@echo " make update — git pull + пересборка"
install:
sudo ./scripts/install-ubuntu.sh
deploy:
chmod +x scripts/*.sh
./scripts/deploy.sh
up:
docker compose up -d
down:
docker compose down
logs:
docker compose logs -f
restart:
docker compose restart
ssl:
@test -n "$(DOMAIN)" || (echo "Укажите DOMAIN=shop.example.com EMAIL=admin@example.com"; exit 1)
./scripts/enable-ssl.sh "$(DOMAIN)" "$(EMAIL)"
update:
./scripts/update.sh
+97 -32
View File
@@ -2,6 +2,8 @@
Главная страница интернет-магазина на **Go**, **PostgreSQL 17**, **Docker Compose** и **Caddy** (обратный прокси + SSL). Главная страница интернет-магазина на **Go**, **PostgreSQL 17**, **Docker Compose** и **Caddy** (обратный прокси + SSL).
Проект настроен для развёртывания на **Ubuntu 22.04 / 24.04**.
## Стек ## Стек
| Компонент | Технология | | Компонент | Технология |
@@ -9,20 +11,74 @@
| Backend | Go 1.23, `net/http`, `pgx/v5` | | Backend | Go 1.23, `net/http`, `pgx/v5` |
| База данных | PostgreSQL 17 | | База данных | PostgreSQL 17 |
| Прокси / SSL | Caddy 2 | | Прокси / SSL | Caddy 2 |
| ОС | Ubuntu 22.04 / 24.04 |
| Контейнеризация | Docker Compose | | Контейнеризация | Docker Compose |
## Быстрый старт ## Быстрый старт на Ubuntu
```bash ```bash
# Скопировать переменные окружения # Клонировать репозиторий
cp .env.example .env git clone https://git.evilfox.cc/test2/shop-go.git /opt/shop
cd /opt/shop
# Запустить все сервисы # 1. Установить Docker (один раз, с sudo)
docker compose up --build -d sudo ./scripts/install-ubuntu.sh
# Открыть в браузере # Перелогиниться или:
# http://localhost newgrp docker
# http://localhost/admin/login — админка
# 2. Запустить магазин
chmod +x scripts/*.sh
./scripts/deploy.sh
```
Откройте в браузере:
- `http://IP_СЕРВЕРА` — магазин
- `http://IP_СЕРВЕРА/admin/login` — админка
Или через Make:
```bash
make install # sudo, установка Docker
make deploy # первый запуск
make logs # просмотр логов
```
## Продакшен с доменом и SSL
1. Настройте DNS: A-запись домена → IP сервера Ubuntu
2. Откройте порты 80 и 443 (скрипт `install-ubuntu.sh` настраивает ufw)
3. Включите SSL:
```bash
./scripts/enable-ssl.sh shop.example.com admin@example.com
```
Или:
```bash
make ssl DOMAIN=shop.example.com EMAIL=admin@example.com
```
Caddy автоматически получит сертификат Let's Encrypt.
Вручную через `.env`:
```env
DOMAIN=shop.example.com
CADDY_EMAIL=admin@example.com
CADDYFILE=./Caddyfile.prod
```
Затем: `docker compose up -d --force-recreate caddy`
## Автозапуск при загрузке Ubuntu
```bash
sudo cp systemd/shop.service /etc/systemd/system/
# Отредактируйте WorkingDirectory, если проект не в /opt/shop
sudo systemctl daemon-reload
sudo systemctl enable --now shop
``` ```
## Админ ## Админ
@@ -35,7 +91,9 @@ ADMIN_PASSWORD=change_me
SESSION_SECRET=your_random_secret_key_here SESSION_SECRET=your_random_secret_key_here
``` ```
Вход: **http://localhost/admin/login** Скрипт `deploy.sh` генерирует пароли автоматически при первом запуске.
Вход: `/admin/login`
При изменении `ADMIN_PASSWORD` пароль обновится в БД при следующем запуске. При изменении `ADMIN_PASSWORD` пароль обновится в БД при следующем запуске.
@@ -46,29 +104,26 @@ SESSION_SECRET=your_random_secret_key_here
| Caddy | 80, 443 | Обратный прокси, SSL | | Caddy | 80, 443 | Обратный прокси, SSL |
| App | 8080 (внутри) | Go-приложение | | App | 8080 (внутри) | Go-приложение |
| PostgreSQL | 5432 (внутри) | База данных | | PostgreSQL | 5432 (внутри) | База данных |
| Caddy Admin | 2019 | Панель управления Caddy |
## SSL в продакшене ## Обновление на сервере
1. Укажите домен в `.env`:
```
DOMAIN=shop.example.com
```
2. Обновите `Caddyfile` — раскомментируйте блок с `{$DOMAIN}` и закомментируйте `:80`.
3. Убедитесь, что порты 80 и 443 открыты. Caddy автоматически получит сертификат Let's Encrypt.
## Локальная разработка без Docker
```bash ```bash
# Запустить только PostgreSQL ./scripts/update.sh
# или
make update
```
## Локальная разработка (Ubuntu)
```bash
# Только PostgreSQL в Docker
docker compose up db -d docker compose up db -d
# Установить зависимости и запустить приложение export DB_HOST=localhost DB_USER=shop DB_PASSWORD=shop_secret DB_NAME=shop
export ADMIN_EMAIL=admin@shop.local ADMIN_PASSWORD=change_me
go mod tidy go mod tidy
go run ./cmd/shop go run ./cmd/shop
# http://localhost:8080 # http://localhost:8080
``` ```
@@ -76,15 +131,19 @@ go run ./cmd/shop
``` ```
shop/ shop/
├── cmd/shop/main.go # Точка входа ├── cmd/shop/main.go
├── internal/ ├── internal/
│ ├── database/ # Подключение к PostgreSQL ├── migrations/
│ ├── handlers/ # HTTP-обработчики, шаблоны, статика ├── scripts/
│ ├── models/ # Модели данных │ ├── install-ubuntu.sh # установка Docker + ufw
── repository/ # Запросы к БД ── deploy.sh # первый запуск
├── migrations/ # SQL-миграции │ ├── enable-ssl.sh # SSL для домена
├── Caddyfile # Конфигурация Caddy │ └── update.sh # обновление
├── systemd/shop.service # автозапуск
├── Caddyfile # HTTP (IP / localhost)
├── Caddyfile.prod # HTTPS (домен)
├── docker-compose.yml ├── docker-compose.yml
├── Makefile
└── Dockerfile └── Dockerfile
``` ```
@@ -108,3 +167,9 @@ docker compose down
# С удалением данных БД # С удалением данных БД
docker compose down -v docker compose down -v
``` ```
## Требования к серверу Ubuntu
- 1 CPU, 1 GB RAM (минимум)
- Ubuntu 22.04 или 24.04 LTS
- Открытые порты 22 (SSH), 80, 443
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"shop/internal/config" "shop/internal/config"
"shop/internal/database" "shop/internal/database"
"shop/internal/handlers" "shop/internal/handlers"
"shop/internal/middleware"
"shop/internal/repository" "shop/internal/repository"
) )
@@ -63,7 +64,7 @@ func main() {
addr := ":" + getEnv("APP_PORT", "8080") addr := ":" + getEnv("APP_PORT", "8080")
srv := &http.Server{ srv := &http.Server{
Addr: addr, Addr: addr,
Handler: mux, Handler: middleware.TrustProxy(mux),
ReadTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second,
+7 -8
View File
@@ -7,6 +7,7 @@ services:
POSTGRES_USER: ${DB_USER:-shop} POSTGRES_USER: ${DB_USER:-shop}
POSTGRES_PASSWORD: ${DB_PASSWORD:-shop_secret} POSTGRES_PASSWORD: ${DB_PASSWORD:-shop_secret}
POSTGRES_DB: ${DB_NAME:-shop} POSTGRES_DB: ${DB_NAME:-shop}
TZ: ${TZ:-UTC}
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
- ./migrations/001_init.sql:/docker-entrypoint-initdb.d/001_init.sql:ro - ./migrations/001_init.sql:/docker-entrypoint-initdb.d/001_init.sql:ro
@@ -25,21 +26,17 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: shop-app container_name: shop-app
restart: unless-stopped restart: unless-stopped
env_file: .env
environment: environment:
DB_HOST: db DB_HOST: db
DB_PORT: "5432" DB_PORT: "5432"
DB_USER: ${DB_USER:-shop}
DB_PASSWORD: ${DB_PASSWORD:-shop_secret}
DB_NAME: ${DB_NAME:-shop}
APP_PORT: "8080" APP_PORT: "8080"
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@shop.local}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change_me}
SESSION_SECRET: ${SESSION_SECRET:-dev_session_secret}
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
networks: networks:
- shop-net - shop-net
init: true
caddy: caddy:
image: caddy:2-alpine image: caddy:2-alpine
@@ -48,17 +45,19 @@ services:
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
- "2019:2019" env_file: .env
environment: environment:
DOMAIN: ${DOMAIN:-localhost} DOMAIN: ${DOMAIN:-localhost}
CADDY_EMAIL: ${CADDY_EMAIL:-}
volumes: volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro - ${CADDYFILE:-./Caddyfile}:/etc/caddy/Caddyfile:ro
- caddy_data:/data - caddy_data:/data
- caddy_config:/config - caddy_config:/config
depends_on: depends_on:
- app - app
networks: networks:
- shop-net - shop-net
init: true
volumes: volumes:
pgdata: pgdata:
+11 -2
View File
@@ -58,27 +58,36 @@ func (s *Session) Validate(token string) (string, bool) {
return email, true return email, true
} }
func (s *Session) SetCookie(w http.ResponseWriter, token string) { func (s *Session) SetCookie(w http.ResponseWriter, r *http.Request, token string) {
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: cookieName, Name: cookieName,
Value: token, Value: token,
Path: "/admin", Path: "/admin",
HttpOnly: true, HttpOnly: true,
Secure: s.isSecure(r),
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
MaxAge: int(sessionTTL.Seconds()), MaxAge: int(sessionTTL.Seconds()),
}) })
} }
func (s *Session) ClearCookie(w http.ResponseWriter) { func (s *Session) ClearCookie(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: cookieName, Name: cookieName,
Value: "", Value: "",
Path: "/admin", Path: "/admin",
HttpOnly: true, HttpOnly: true,
Secure: s.isSecure(r),
MaxAge: -1, MaxAge: -1,
}) })
} }
func (s *Session) isSecure(r *http.Request) bool {
if r.TLS != nil {
return true
}
return strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https")
}
func (s *Session) FromRequest(r *http.Request) (string, bool) { func (s *Session) FromRequest(r *http.Request) (string, bool) {
c, err := r.Cookie(cookieName) c, err := r.Cookie(cookieName)
if err != nil { if err != nil {
+2 -2
View File
@@ -81,12 +81,12 @@ func (h *AdminHandler) Login(w http.ResponseWriter, r *http.Request) {
return return
} }
h.session.SetCookie(w, token) h.session.SetCookie(w, r, token)
http.Redirect(w, r, "/admin/", http.StatusSeeOther) http.Redirect(w, r, "/admin/", http.StatusSeeOther)
} }
func (h *AdminHandler) Logout(w http.ResponseWriter, r *http.Request) { func (h *AdminHandler) Logout(w http.ResponseWriter, r *http.Request) {
h.session.ClearCookie(w) h.session.ClearCookie(w, r)
http.Redirect(w, r, "/admin/login", http.StatusSeeOther) http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
} }
+19
View File
@@ -0,0 +1,19 @@
package middleware
import (
"net/http"
"strings"
)
// TrustProxy восстанавливает схему HTTPS за Caddy/nginx.
func TrustProxy(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if proto := r.Header.Get("X-Forwarded-Proto"); strings.EqualFold(proto, "https") {
r.URL.Scheme = "https"
}
if host := r.Header.Get("X-Forwarded-Host"); host != "" {
r.Host = host
}
next.ServeHTTP(w, r)
})
}
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT_DIR}"
if ! command -v docker &>/dev/null; then
echo "Docker не найден. Сначала: sudo ./scripts/install-ubuntu.sh"
exit 1
fi
if [[ ! -f .env ]]; then
cp .env.example .env
SECRET=$(openssl rand -hex 32)
DB_PASS=$(openssl rand -hex 16)
sed -i "s/your_random_secret_key_here/${SECRET}/" .env
sed -i "s/DB_PASSWORD=shop_secret/DB_PASSWORD=${DB_PASS}/" .env
sed -i "s/ADMIN_PASSWORD=change_me/ADMIN_PASSWORD=$(openssl rand -base64 18 | tr -d '/+=')/" .env
echo "Создан .env с автоматически сгенерированными паролями."
echo "Проверьте ADMIN_EMAIL и DOMAIN перед продакшеном."
fi
docker compose pull --ignore-buildable 2>/dev/null || true
docker compose up --build -d
echo ""
echo "Сервисы запущены."
echo " Магазин: http://$(hostname -I | awk '{print $1}')"
echo " Админка: http://$(hostname -I | awk '{print $1}')/admin/login"
echo ""
echo "Логи: docker compose logs -f"
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Переключение Caddy на продакшен-режим с SSL (Ubuntu + домен)
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT_DIR}"
if [[ ! -f .env ]]; then
echo "Сначала запустите: ./scripts/deploy.sh"
exit 1
fi
DOMAIN="${1:-}"
EMAIL="${2:-}"
if [[ -z "${DOMAIN}" || -z "${EMAIL}" ]]; then
echo "Использование: ./scripts/enable-ssl.sh <домен> <email>"
echo "Пример: ./scripts/enable-ssl.sh shop.example.com admin@example.com"
exit 1
fi
set_env() {
local key="$1" val="$2"
if grep -q "^${key}=" .env; then
sed -i "s|^${key}=.*|${key}=${val}|" .env
else
echo "${key}=${val}" >> .env
fi
}
set_env DOMAIN "${DOMAIN}"
set_env CADDY_EMAIL "${EMAIL}"
set_env CADDYFILE "./Caddyfile.prod"
docker compose up -d --force-recreate caddy
echo "SSL включён для https://${DOMAIN}"
echo "Убедитесь, что DNS A-запись указывает на этот сервер."
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail
# Установка Docker и Docker Compose на Ubuntu 22.04 / 24.04
if [[ "${EUID}" -ne 0 ]]; then
echo "Запустите с sudo: sudo ./scripts/install-ubuntu.sh"
exit 1
fi
if ! grep -qi ubuntu /etc/os-release 2>/dev/null; then
echo "Скрипт предназначен для Ubuntu."
exit 1
fi
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y ca-certificates curl gnupg ufw
# Docker official repo
install -m 0755 -d /etc/apt/keyrings
if [[ ! -f /etc/apt/keyrings/docker.gpg ]]; then
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
fi
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${VERSION_CODENAME}") stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
# Firewall
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
SUDO_USER_NAME="${SUDO_USER:-$USER}"
if id "${SUDO_USER_NAME}" &>/dev/null; then
usermod -aG docker "${SUDO_USER_NAME}"
echo "Пользователь ${SUDO_USER_NAME} добавлен в группу docker."
echo "Перелогиньтесь или выполните: newgrp docker"
fi
echo ""
echo "Docker установлен: $(docker --version)"
echo "Compose: $(docker compose version)"
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT_DIR}"
git pull origin main
docker compose up --build -d
docker image prune -f
echo "Обновление завершено."
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Быстрая установка на Ubuntu — запуск: sudo bash setup.sh
set -euo pipefail
cd "$(dirname "$0")"
if [[ "${EUID}" -ne 0 ]]; then
echo "Запустите: sudo bash setup.sh"
exit 1
fi
bash scripts/install-ubuntu.sh
REAL_USER="${SUDO_USER:-ubuntu}"
if id "${REAL_USER}" &>/dev/null; then
sudo -u "${REAL_USER}" bash scripts/deploy.sh
else
bash scripts/deploy.sh
fi
echo ""
echo "Готово! Откройте http://$(hostname -I | awk '{print $1}')"
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=Shop internet store (Docker Compose)
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/shop
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=300
[Install]
WantedBy=multi-user.target