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
# Domain for Caddy SSL (production)
DOMAIN=shop.example.com
# Часовой пояс сервера (Ubuntu)
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 @@
{
admin localhost:2019
}
# Локальная разработка — HTTP на порту 80
# HTTP — доступ по IP или localhost (без домена)
:80 {
encode gzip
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
ENV TZ=UTC
WORKDIR /app
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).
Проект настроен для развёртывания на **Ubuntu 22.04 / 24.04**.
## Стек
| Компонент | Технология |
@@ -9,20 +11,74 @@
| Backend | Go 1.23, `net/http`, `pgx/v5` |
| База данных | PostgreSQL 17 |
| Прокси / SSL | Caddy 2 |
| ОС | Ubuntu 22.04 / 24.04 |
| Контейнеризация | Docker Compose |
## Быстрый старт
## Быстрый старт на Ubuntu
```bash
# Скопировать переменные окружения
cp .env.example .env
# Клонировать репозиторий
git clone https://git.evilfox.cc/test2/shop-go.git /opt/shop
cd /opt/shop
# Запустить все сервисы
docker compose up --build -d
# 1. Установить Docker (один раз, с sudo)
sudo ./scripts/install-ubuntu.sh
# Открыть в браузере
# http://localhost
# http://localhost/admin/login — админка
# Перелогиниться или:
newgrp docker
# 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
```
Вход: **http://localhost/admin/login**
Скрипт `deploy.sh` генерирует пароли автоматически при первом запуске.
Вход: `/admin/login`
При изменении `ADMIN_PASSWORD` пароль обновится в БД при следующем запуске.
@@ -46,29 +104,26 @@ SESSION_SECRET=your_random_secret_key_here
| Caddy | 80, 443 | Обратный прокси, SSL |
| App | 8080 (внутри) | Go-приложение |
| 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
# Запустить только PostgreSQL
./scripts/update.sh
# или
make update
```
## Локальная разработка (Ubuntu)
```bash
# Только PostgreSQL в Docker
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 run ./cmd/shop
# http://localhost:8080
```
@@ -76,15 +131,19 @@ go run ./cmd/shop
```
shop/
├── cmd/shop/main.go # Точка входа
├── cmd/shop/main.go
├── internal/
│ ├── database/ # Подключение к PostgreSQL
│ ├── handlers/ # HTTP-обработчики, шаблоны, статика
│ ├── models/ # Модели данных
── repository/ # Запросы к БД
├── migrations/ # SQL-миграции
├── Caddyfile # Конфигурация Caddy
├── migrations/
├── scripts/
│ ├── install-ubuntu.sh # установка Docker + ufw
── deploy.sh # первый запуск
│ ├── enable-ssl.sh # SSL для домена
│ └── update.sh # обновление
├── systemd/shop.service # автозапуск
├── Caddyfile # HTTP (IP / localhost)
├── Caddyfile.prod # HTTPS (домен)
├── docker-compose.yml
├── Makefile
└── Dockerfile
```
@@ -108,3 +167,9 @@ docker compose down
# С удалением данных БД
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/database"
"shop/internal/handlers"
"shop/internal/middleware"
"shop/internal/repository"
)
@@ -63,7 +64,7 @@ func main() {
addr := ":" + getEnv("APP_PORT", "8080")
srv := &http.Server{
Addr: addr,
Handler: mux,
Handler: middleware.TrustProxy(mux),
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
+7 -8
View File
@@ -7,6 +7,7 @@ services:
POSTGRES_USER: ${DB_USER:-shop}
POSTGRES_PASSWORD: ${DB_PASSWORD:-shop_secret}
POSTGRES_DB: ${DB_NAME:-shop}
TZ: ${TZ:-UTC}
volumes:
- pgdata:/var/lib/postgresql/data
- ./migrations/001_init.sql:/docker-entrypoint-initdb.d/001_init.sql:ro
@@ -25,21 +26,17 @@ services:
dockerfile: Dockerfile
container_name: shop-app
restart: unless-stopped
env_file: .env
environment:
DB_HOST: db
DB_PORT: "5432"
DB_USER: ${DB_USER:-shop}
DB_PASSWORD: ${DB_PASSWORD:-shop_secret}
DB_NAME: ${DB_NAME:-shop}
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:
db:
condition: service_healthy
networks:
- shop-net
init: true
caddy:
image: caddy:2-alpine
@@ -48,17 +45,19 @@ services:
ports:
- "80:80"
- "443:443"
- "2019:2019"
env_file: .env
environment:
DOMAIN: ${DOMAIN:-localhost}
CADDY_EMAIL: ${CADDY_EMAIL:-}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ${CADDYFILE:-./Caddyfile}:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- app
networks:
- shop-net
init: true
volumes:
pgdata:
+11 -2
View File
@@ -58,27 +58,36 @@ func (s *Session) Validate(token string) (string, bool) {
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{
Name: cookieName,
Value: token,
Path: "/admin",
HttpOnly: true,
Secure: s.isSecure(r),
SameSite: http.SameSiteLaxMode,
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{
Name: cookieName,
Value: "",
Path: "/admin",
HttpOnly: true,
Secure: s.isSecure(r),
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) {
c, err := r.Cookie(cookieName)
if err != nil {
+2 -2
View File
@@ -81,12 +81,12 @@ func (h *AdminHandler) Login(w http.ResponseWriter, r *http.Request) {
return
}
h.session.SetCookie(w, token)
h.session.SetCookie(w, r, token)
http.Redirect(w, r, "/admin/", http.StatusSeeOther)
}
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)
}
+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