new arch
This commit is contained in:
7
.env.example
Normal file
7
.env.example
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
LOCAL_DOMAIN=mikan
|
||||||
|
PUBLIC_DOMAIN=
|
||||||
|
LETSENCRYPT_EMAIL=admin@${LOCAL_DOMAIN}
|
||||||
|
NGINX_IP=10.0.0.3
|
||||||
|
DNSMASQ_IP=10.0.0.2
|
||||||
|
SUBNET=10.0.0.0/24
|
||||||
|
GATEWAY=10.0.0.1
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1 +1,15 @@
|
|||||||
certs/
|
# Environment
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Certificates and secrets
|
||||||
|
/nginx/certs/
|
||||||
|
/ca/
|
||||||
|
/acme/
|
||||||
|
|
||||||
|
# Service data
|
||||||
|
/services/*/data/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
130
README.md
130
README.md
@@ -1,2 +1,130 @@
|
|||||||
# local-dns
|
# Local DNS Services with Docker
|
||||||
|
|
||||||
|
This project provides a local development infrastructure with automatic DNS resolution for custom domains (e.g., `.mikan` or `.local`) and valid HTTPS certificates.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Automatic DNS resolution for `*.yourdomain` and `*.yourdomain.local`
|
||||||
|
- Valid HTTPS certificates for local domains using `mkcert`
|
||||||
|
- Easy service deployment with `VIRTUAL_HOST` environment variables
|
||||||
|
- Ready for future expansion: public domains, Let's Encrypt, Tailscale
|
||||||
|
- Single-command service creation
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Docker and Docker Compose
|
||||||
|
- `mkcert` installed on the host system
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Clone this repository and enter the directory:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url> local-dns
|
||||||
|
cd local-dns
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Initialize the project:
|
||||||
|
```bash
|
||||||
|
./scripts/00-init.sh
|
||||||
|
```
|
||||||
|
This creates a `.env` file from `.env.example` and sets up the directory structure.
|
||||||
|
|
||||||
|
3. Generate TLS certificates:
|
||||||
|
```bash
|
||||||
|
./scripts/01-generate-cert.sh
|
||||||
|
```
|
||||||
|
This creates trusted certificates for your local domain.
|
||||||
|
|
||||||
|
4. Start the core infrastructure:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Add your first service (example: Gitea on port 3000):
|
||||||
|
```bash
|
||||||
|
./scripts/add-service.sh gitea 3000
|
||||||
|
```
|
||||||
|
Then edit `services/gitea/docker-compose.yml` to set the correct image and volumes.
|
||||||
|
|
||||||
|
6. Start the new service:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f services/gitea/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Visit `https://gitea.yourdomain` in your browser (replace `yourdomain` with your configured domain).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Edit the `.env` file to customize your setup:
|
||||||
|
|
||||||
|
- `LOCAL_DOMAIN`: Your local domain suffix (default: `mikan`)
|
||||||
|
- `PUBLIC_DOMAIN`: Optional public domain for Let's Encrypt certificates
|
||||||
|
- `LETSENCRYPT_EMAIL`: Email for Let's Encrypt notifications
|
||||||
|
- Network settings (`NGINX_IP`, `DNSMASQ_IP`, etc.)
|
||||||
|
|
||||||
|
After changing `.env`, re-run the initialization and certificate scripts.
|
||||||
|
|
||||||
|
## DNS Setup
|
||||||
|
|
||||||
|
### Linux/macOS
|
||||||
|
|
||||||
|
Use the included `dnsmasq` service for automatic DNS resolution:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dnsmasq.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Then configure your system to use `10.0.0.2` as the primary DNS server.
|
||||||
|
|
||||||
|
### Windows (WSL2)
|
||||||
|
|
||||||
|
Windows reserves port 53, so `dnsmasq` cannot run on the host. Instead:
|
||||||
|
|
||||||
|
1. Work inside WSL2
|
||||||
|
2. Manually add entries to the Windows hosts file:
|
||||||
|
```powershell
|
||||||
|
# Run PowerShell as Administrator
|
||||||
|
Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value "127.0.0.1 gitea.mikan"
|
||||||
|
Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value "127.0.0.1 gitea.mikan.local"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Certificate Trust
|
||||||
|
|
||||||
|
After running `./scripts/01-generate-cert.sh`, install the root certificate on your devices:
|
||||||
|
|
||||||
|
- **Linux**: Depends on distribution (usually via `update-ca-certificates`)
|
||||||
|
- **macOS**:
|
||||||
|
```bash
|
||||||
|
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca/rootCA.pem
|
||||||
|
```
|
||||||
|
- **Windows**:
|
||||||
|
1. Copy `ca/rootCA.pem` to Windows
|
||||||
|
2. Open `certmgr.msc`
|
||||||
|
3. Import into "Trusted Root Certification Authorities"
|
||||||
|
|
||||||
|
## Adding Public Domain Support
|
||||||
|
|
||||||
|
To enable Let's Encrypt certificates for a public domain:
|
||||||
|
|
||||||
|
1. Set in `.env`:
|
||||||
|
```ini
|
||||||
|
PUBLIC_DOMAIN=yourdomain.com
|
||||||
|
LETSENCRYPT_EMAIL=you@yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Re-run initialization:
|
||||||
|
```bash
|
||||||
|
./scripts/00-init.sh
|
||||||
|
./scripts/01-generate-cert.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. New services will automatically request Let's Encrypt certificates for the public domain.
|
||||||
|
|
||||||
|
## Service Management
|
||||||
|
|
||||||
|
- **Add a service**: `./scripts/add-service.sh <name> <port>`
|
||||||
|
- **Start all**: `docker compose up -d`
|
||||||
|
- **Start with dnsmasq**: `docker compose -f docker-compose.yml -f docker-compose.dnsmasq.yml up -d`
|
||||||
|
- **Stop all**: `docker compose down`
|
||||||
|
|
||||||
|
All service data is stored in the `services/` directory and persists between restarts.
|
||||||
2
dnsmasq/dnsmasq.conf
Normal file
2
dnsmasq/dnsmasq.conf
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Этот файл будет перезаписан при запуске ./scripts/02-apply-config.sh
|
||||||
|
# Не редактируйте вручную
|
||||||
26
docker-compose.dnsmasq.yml
Normal file
26
docker-compose.dnsmasq.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
dnsmasq:
|
||||||
|
image: jpillora/dnsmasq
|
||||||
|
container_name: dnsmasq
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "53:53/udp"
|
||||||
|
- "53:53/tcp"
|
||||||
|
volumes:
|
||||||
|
- ./dnsmasq/dnsmasq.conf:/etc/dnsmasq.conf:ro
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
networks:
|
||||||
|
devnet:
|
||||||
|
ipv4_address: ${DNSMASQ_IP}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "nslookup", "localhost", "127.0.0.1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
networks:
|
||||||
|
devnet:
|
||||||
|
external: true
|
||||||
@@ -1,55 +1,70 @@
|
|||||||
services:
|
version: '3.8'
|
||||||
dnsmasq:
|
|
||||||
image: jpillora/dnsmasq
|
|
||||||
container_name: dnsmasq
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "53:53/udp"
|
|
||||||
- "53:53/tcp"
|
|
||||||
volumes:
|
|
||||||
- ./dnsmasq.conf:/etc/dnsmasq.conf:ro
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
networks:
|
|
||||||
- devnet
|
|
||||||
|
|
||||||
|
services:
|
||||||
nginx-proxy:
|
nginx-proxy:
|
||||||
image: nginxproxy/nginx-proxy:alpine
|
image: nginxproxy/nginx-proxy:alpine
|
||||||
container_name: nginx-proxy
|
container_name: nginx-proxy
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||||
- ./certs:/etc/nginx/certs:ro
|
- ./nginx/certs:/etc/nginx/certs:ro
|
||||||
- ./vhost.d:/etc/nginx/vhost.d:ro
|
- ./nginx/vhost.d:/etc/nginx/vhost.d:ro
|
||||||
- ./html:/usr/share/nginx/html:rw
|
- ./nginx/html:/usr/share/nginx/html:rw
|
||||||
networks:
|
networks:
|
||||||
- devnet
|
devnet:
|
||||||
|
ipv4_address: ${NGINX_IP}
|
||||||
|
environment:
|
||||||
|
- DEFAULT_HOST=welcome.${LOCAL_DOMAIN}
|
||||||
|
- HTTPS_METHOD=redirect
|
||||||
|
- SSL_POLICY=Mozilla-Modern
|
||||||
|
- DEFAULT_SSL_CERT=/etc/nginx/certs/default.crt
|
||||||
|
- DEFAULT_SSL_KEY=/etc/nginx/certs/default.key
|
||||||
labels:
|
labels:
|
||||||
- "com.github.nginx-proxy.nginx"
|
- "com.github.nginx-proxy.nginx-proxy=true"
|
||||||
|
|
||||||
nginx-proxy-companion:
|
nginx-proxy-companion:
|
||||||
image: nginxproxy/acme-companion
|
image: nginxproxy/acme-companion
|
||||||
container_name: nginx-proxy-companion
|
container_name: nginx-proxy-companion
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- ./certs:/etc/nginx/certs:rw
|
- ./nginx/certs:/etc/nginx/certs:rw
|
||||||
- ./acme:/etc/acme.sh
|
- ./acme:/etc/acme.sh
|
||||||
- ./vhost.d:/etc/nginx/vhost.d
|
- ./nginx/vhost.d:/etc/nginx/vhost.d:rw
|
||||||
- ./html:/usr/share/nginx/html
|
|
||||||
networks:
|
networks:
|
||||||
- devnet
|
- devnet
|
||||||
environment:
|
environment:
|
||||||
|
- DEFAULT_EMAIL=${LETSENCRYPT_EMAIL}
|
||||||
- NGINX_PROXY_CONTAINER=nginx-proxy
|
- NGINX_PROXY_CONTAINER=nginx-proxy
|
||||||
|
- ACME_CA_URI=https://acme-staging-v02.api.letsencrypt.org/directory
|
||||||
|
depends_on:
|
||||||
|
- nginx-proxy
|
||||||
|
|
||||||
|
welcome:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: welcome
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./nginx/html:/usr/share/nginx/html:ro
|
||||||
|
environment:
|
||||||
|
- VIRTUAL_HOST=welcome.${LOCAL_DOMAIN},welcome.${LOCAL_DOMAIN}.local
|
||||||
|
networks:
|
||||||
|
- devnet
|
||||||
|
depends_on:
|
||||||
|
- nginx-proxy
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
devnet:
|
devnet:
|
||||||
external: true
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: ${SUBNET}
|
||||||
|
gateway: ${GATEWAY}
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
acme:
|
acme:
|
||||||
vhost.d:
|
vhost.d:
|
||||||
html:
|
html:
|
||||||
7
nginx/html/index.html
Normal file
7
nginx/html/index.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>Placeholder</title></head>
|
||||||
|
<body>
|
||||||
|
<p>Run ./scripts/00-init.sh to generate content</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
41
scripts/00-init.sh
Normal file
41
scripts/00-init.sh
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$SCRIPT_DIR/.."
|
||||||
|
|
||||||
|
if [ ! -f "$PROJECT_ROOT/.env" ]; then
|
||||||
|
echo "Файл .env не найден. Копирую из .env.example..."
|
||||||
|
cp "$PROJECT_ROOT/.env.example" "$PROJECT_ROOT/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export $(grep -v '^#' "$PROJECT_ROOT/.env" | xargs)
|
||||||
|
|
||||||
|
# Создаём каталоги
|
||||||
|
mkdir -p \
|
||||||
|
"$PROJECT_ROOT/dnsmasq" \
|
||||||
|
"$PROJECT_ROOT/nginx/{certs,vhost.d,html,conf.d}" \
|
||||||
|
"$PROJECT_ROOT/acme" \
|
||||||
|
"$PROJECT_ROOT/ca" \
|
||||||
|
"$PROJECT_ROOT/services"
|
||||||
|
|
||||||
|
# Создаём общую Docker-сеть с фиксированными настройками
|
||||||
|
echo "Проверка сети devnet..."
|
||||||
|
if ! docker network inspect devnet &>/dev/null; then
|
||||||
|
echo "Создаём сеть devnet..."
|
||||||
|
docker network create \
|
||||||
|
--driver=bridge \
|
||||||
|
--subnet="${SUBNET}" \
|
||||||
|
--gateway="${GATEWAY}" \
|
||||||
|
devnet
|
||||||
|
else
|
||||||
|
echo "Сеть devnet уже существует."
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$SCRIPT_DIR/02-apply-config.sh"
|
||||||
|
|
||||||
|
echo "Проект инициализирован."
|
||||||
|
echo "Домен: .$LOCAL_DOMAIN"
|
||||||
|
if [ -n "$PUBLIC_DOMAIN" ]; then
|
||||||
|
echo "Публичный домен: $PUBLIC_DOMAIN"
|
||||||
|
fi
|
||||||
49
scripts/01-generate-cert.sh
Normal file
49
scripts/01-generate-cert.sh
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$SCRIPT_DIR/.."
|
||||||
|
|
||||||
|
if [ ! -f "$PROJECT_ROOT/.env" ]; then
|
||||||
|
echo "Файл .env не найден. Запустите сначала ./scripts/00-init.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export $(grep -v '^#' "$PROJECT_ROOT/.env" | xargs)
|
||||||
|
|
||||||
|
CERTS_DIR="$PROJECT_ROOT/nginx/certs"
|
||||||
|
CA_DIR="$PROJECT_ROOT/ca"
|
||||||
|
|
||||||
|
if ! command -v mkcert &> /dev/null; then
|
||||||
|
echo "mkcert не установлен."
|
||||||
|
echo "Linux: sudo apt install mkcert"
|
||||||
|
echo "macOS: brew install mkcert nss"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$CERTS_DIR" "$CA_DIR"
|
||||||
|
|
||||||
|
DOMAINS=(
|
||||||
|
"*.$LOCAL_DOMAIN"
|
||||||
|
"*.$LOCAL_DOMAIN.local"
|
||||||
|
"$LOCAL_DOMAIN"
|
||||||
|
"localhost"
|
||||||
|
"127.0.0.1"
|
||||||
|
"::1"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -n "$PUBLIC_DOMAIN" ]; then
|
||||||
|
DOMAINS+=("*.$PUBLIC_DOMAIN" "$PUBLIC_DOMAIN")
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkcert -install
|
||||||
|
mkcert \
|
||||||
|
-cert-file "$CERTS_DIR/default.crt" \
|
||||||
|
-key-file "$CERTS_DIR/default.key" \
|
||||||
|
"${DOMAINS[@]}"
|
||||||
|
|
||||||
|
cp "$(mkcert -CAROOT)/rootCA.pem" "$CA_DIR/rootCA.pem"
|
||||||
|
|
||||||
|
echo "Сертификаты созданы в $CERTS_DIR"
|
||||||
|
echo "Установите корневой сертификат на устройствах:"
|
||||||
|
echo " $CA_DIR/rootCA.pem"
|
||||||
38
scripts/02-apply-config.sh
Normal file
38
scripts/02-apply-config.sh
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$SCRIPT_DIR/.."
|
||||||
|
|
||||||
|
if [ ! -f "$PROJECT_ROOT/.env" ]; then
|
||||||
|
echo "Файл .env не найден. Запустите сначала ./scripts/00-init.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export $(grep -v '^#' "$PROJECT_ROOT/.env" | xargs)
|
||||||
|
|
||||||
|
cat > "$PROJECT_ROOT/dnsmasq/dnsmasq.conf" <<EOF
|
||||||
|
address=/.$LOCAL_DOMAIN/$NGINX_IP
|
||||||
|
address=/.$LOCAL_DOMAIN.local/$NGINX_IP
|
||||||
|
local=/.$LOCAL_DOMAIN/
|
||||||
|
local=/.$LOCAL_DOMAIN.local/
|
||||||
|
no-resolv
|
||||||
|
server=1.1.1.1
|
||||||
|
server=8.8.8.8
|
||||||
|
log-queries
|
||||||
|
log-facility=/dev/stdout
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mkdir -p "$PROJECT_ROOT/nginx/html"
|
||||||
|
cat > "$PROJECT_ROOT/nginx/html/index.html" <<EOF
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>Добро пожаловать!</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Сервисы .$LOCAL_DOMAIN</h1>
|
||||||
|
<p>Добавьте свои сервисы в папку <code>services/</code></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Конфигурация применена для .$LOCAL_DOMAIN"
|
||||||
65
scripts/add-service.sh
Normal file
65
scripts/add-service.sh
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ $# -ne 2 ]; then
|
||||||
|
echo "Использование: $0 <название> <порт>"
|
||||||
|
echo "Пример: $0 gitea 3000"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NAME=$1
|
||||||
|
PORT=$2
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$SCRIPT_DIR/.."
|
||||||
|
|
||||||
|
if [ ! -f "$PROJECT_ROOT/.env" ]; then
|
||||||
|
echo "Файл .env не найден. Запустите сначала ./scripts/00-init.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export $(grep -v '^#' "$PROJECT_ROOT/.env" | xargs)
|
||||||
|
|
||||||
|
SERVICE_DIR="$PROJECT_ROOT/services/$NAME"
|
||||||
|
|
||||||
|
if [ -d "$SERVICE_DIR" ]; then
|
||||||
|
echo "Сервис '$NAME' уже существует!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$SERVICE_DIR/data"
|
||||||
|
|
||||||
|
cat > "$SERVICE_DIR/docker-compose.yml" <<EOF
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
$NAME:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: $NAME
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- VIRTUAL_HOST=$NAME.$LOCAL_DOMAIN,$NAME.$LOCAL_DOMAIN.local
|
||||||
|
$(if [ -n "$PUBLIC_DOMAIN" ]; then
|
||||||
|
echo " - LETSENCRYPT_HOST=$NAME.$PUBLIC_DOMAIN"
|
||||||
|
echo " - LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL"
|
||||||
|
else
|
||||||
|
echo " # LETSENCRYPT_HOST="
|
||||||
|
echo " # LETSENCRYPT_EMAIL="
|
||||||
|
fi)
|
||||||
|
- VIRTUAL_PORT=$PORT
|
||||||
|
volumes:
|
||||||
|
- .//data
|
||||||
|
networks:
|
||||||
|
- devnet
|
||||||
|
depends_on:
|
||||||
|
- nginx-proxy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
devnet:
|
||||||
|
external: true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Сервис '$NAME' создан в $SERVICE_DIR"
|
||||||
|
echo "Запустите:"
|
||||||
|
echo " cd $PROJECT_ROOT"
|
||||||
|
echo " docker compose -f docker-compose.yml -f services/$NAME/docker-compose.yml up -d"
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
# cert_renew_restart.py
|
|
||||||
import subprocess
|
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
|
||||||
from common import *
|
|
||||||
|
|
||||||
def generate_mkcert_command():
|
|
||||||
"""Генерирует команду для mkcert с указанием всех доменов"""
|
|
||||||
project_path = get_project_root()
|
|
||||||
services = read_services_config(project_path)
|
|
||||||
domains = [f"{service['name']}.local" for service in services['services']]
|
|
||||||
domains.append("*.local")
|
|
||||||
|
|
||||||
# Создаем PowerShell команду
|
|
||||||
domains_str = " ".join([f'"{domain}"' for domain in domains])
|
|
||||||
powershell_cmd = f'mkcert {domains_str}'
|
|
||||||
return powershell_cmd
|
|
||||||
|
|
||||||
def copy_and_rename_certificates(project_path):
|
|
||||||
"""Копирует и переименовывает сертификаты из Windows в WSL в директорию проекта."""
|
|
||||||
# Определяем пути
|
|
||||||
windows_certs_path = Path("C:/store/certs")
|
|
||||||
# Путь к директории certs внутри проекта local-dns
|
|
||||||
wsl_certs_path = project_path / "certs"
|
|
||||||
|
|
||||||
# Создаем директорию для сертификатов в WSL в корне проекта
|
|
||||||
wsl_certs_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Находим сгенерированные mkcert файлы (обычно именуются как _wildcard.local+*.pem)
|
|
||||||
# Предполагаем, что первый найденный файл - это сертификат
|
|
||||||
cert_files = list(windows_certs_path.glob("_wildcard.local*.pem"))
|
|
||||||
if not cert_files:
|
|
||||||
print(f"Сертификаты не найдены в {windows_certs_path} по шаблону _wildcard.local*.pem")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Сортируем, чтобы упростить выбор (хотя mkcert обычно создает пару)
|
|
||||||
cert_files.sort()
|
|
||||||
cert_file = cert_files[0]
|
|
||||||
|
|
||||||
# Пытаемся найти соответствующий ключ (обычно имя файла сертификата с -key.pem)
|
|
||||||
# Например, если сертификат _wildcard.local+2.pem, ключ будет _wildcard.local+2-key.pem
|
|
||||||
expected_key_name = cert_file.with_name(cert_file.name.replace('.pem', '-key.pem'))
|
|
||||||
if expected_key_name.exists():
|
|
||||||
key_file = expected_key_name
|
|
||||||
else:
|
|
||||||
print(f"Файл ключа не найден по ожидаемому пути: {expected_key_name}")
|
|
||||||
# Альтернатива: ищем файл с -key.pem в имени среди найденных
|
|
||||||
key_candidates = [f for f in cert_files if '-key.pem' in f.name]
|
|
||||||
if key_candidates:
|
|
||||||
key_file = key_candidates[0] # Берем первый найденный ключ
|
|
||||||
print(f"Найден ключ по альтернативному поиску: {key_file}")
|
|
||||||
else:
|
|
||||||
print(f"Файл ключа не найден в {windows_certs_path}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# Копируем и переименовываем в директорию проекта
|
|
||||||
target_cert = wsl_certs_path / "wildcard.local.pem"
|
|
||||||
target_key = wsl_certs_path / "wildcard.local.key"
|
|
||||||
|
|
||||||
try:
|
|
||||||
shutil.copy2(cert_file, target_cert)
|
|
||||||
shutil.copy2(key_file, target_key)
|
|
||||||
print(f"Сертификат скопирован в: {target_cert}")
|
|
||||||
print(f"Ключ скопирован в: {target_key}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Ошибка копирования сертификатов: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Создаем копии для default (если требуются контейнером)
|
|
||||||
default_cert = wsl_certs_path / "default.crt"
|
|
||||||
default_key = wsl_certs_path / "default.key"
|
|
||||||
|
|
||||||
try:
|
|
||||||
shutil.copy2(target_cert, default_cert)
|
|
||||||
shutil.copy2(target_key, default_key)
|
|
||||||
print(f"Копии сертификатов созданы как default.crt и default.key")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Ошибка создания копий сертификатов по умолчанию: {e}")
|
|
||||||
# Это может быть не критичной ошибкой, в зависимости от настройки контейнера
|
|
||||||
|
|
||||||
print(f"Сертификаты скопированы в {wsl_certs_path}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("Обновление сертификатов и перезапуск сервисов...")
|
|
||||||
|
|
||||||
# Получаем путь к проекту
|
|
||||||
project_path = get_project_root()
|
|
||||||
|
|
||||||
# Генерируем конфигурацию dnsmasq перед обновлением
|
|
||||||
generate_dnsmasq_config(project_path)
|
|
||||||
|
|
||||||
# Выводим команду для генерации сертификатов
|
|
||||||
mkcert_cmd = generate_mkcert_command()
|
|
||||||
print("\nДля генерации сертификатов выполните в PowerShell (от администратора):")
|
|
||||||
print(f"cd C:\\store\\certs")
|
|
||||||
print(mkcert_cmd)
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Ждем подтверждения пользователя
|
|
||||||
input("Нажмите Enter после генерации сертификатов в PowerShell...")
|
|
||||||
|
|
||||||
# Копируем сертификаты
|
|
||||||
if not copy_and_rename_certificates(project_path):
|
|
||||||
print("Не удалось скопировать сертификаты")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Перезапускаем контейнеры Docker Compose
|
|
||||||
print("Перезапуск контейнеров Docker Compose...")
|
|
||||||
if not run_docker_command("docker-compose restart"):
|
|
||||||
print("Команда 'docker-compose restart' не удалась.")
|
|
||||||
print("Попытка поднять контейнеры с помощью 'docker-compose up -d'...")
|
|
||||||
if not run_docker_command("docker-compose up -d --force-recreate"):
|
|
||||||
print("Не удалось поднять контейнеры с помощью 'docker-compose up -d'.")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
print("Контейнеры подняты с помощью 'docker-compose up -d'.")
|
|
||||||
else:
|
|
||||||
print("Контейнеры успешно перезапущены!")
|
|
||||||
|
|
||||||
print("Сертификаты обновлены и сервисы перезапущены!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
# common.py
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
def get_host_ip():
|
|
||||||
"""Получает IP-адрес хоста в локальной сети"""
|
|
||||||
try:
|
|
||||||
# Для WSL2 получаем IP хоста (Windows)
|
|
||||||
if 'microsoft' in platform.uname().release.lower():
|
|
||||||
# В WSL2 получаем IP хоста из файла resolv.conf
|
|
||||||
with open('/etc/resolv.conf', 'r') as f:
|
|
||||||
for line in f:
|
|
||||||
if line.startswith('nameserver'):
|
|
||||||
# Возвращаем первый найденный nameserver как IP хоста
|
|
||||||
# (Это может быть не всегда точно, но подходит для базового сценария)
|
|
||||||
return line.split()[1]
|
|
||||||
# Для других систем используем подключение к внешнему серверу
|
|
||||||
else:
|
|
||||||
import socket
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
s.connect(("8.8.8.8", 80))
|
|
||||||
ip = s.getsockname()[0]
|
|
||||||
s.close()
|
|
||||||
return ip
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Не удалось получить IP-адрес хоста: {e}")
|
|
||||||
# Возвращаем IP по умолчанию, если не удалось получить
|
|
||||||
# Используем IP из памяти, если доступен
|
|
||||||
return "192.168.31.60"
|
|
||||||
|
|
||||||
def get_project_root():
|
|
||||||
"""Определяет корневую директорию проекта 'local-dns'."""
|
|
||||||
# Начинаем с домашней директории
|
|
||||||
current_path = Path.home() / "projects" / "docker"
|
|
||||||
project_name = "local-dns" # Используем фиксированное имя для этого репозитория
|
|
||||||
project_path = current_path / project_name
|
|
||||||
|
|
||||||
if not project_path.exists() or not (project_path / "docker-compose.yml").exists():
|
|
||||||
print(f"Директория проекта '{project_name}' или 'docker-compose.yml' не найдена в {current_path}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return project_path
|
|
||||||
|
|
||||||
def read_services_config(project_root):
|
|
||||||
"""Читает конфигурацию сервисов из файла в корне проекта."""
|
|
||||||
config_path = project_root / "services.json"
|
|
||||||
default_config = {
|
|
||||||
"services": [
|
|
||||||
{"name": "gitea", "port": 3000},
|
|
||||||
{"name": "youtrack", "port": 8080},
|
|
||||||
{"name": "n8n", "port": 5678}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if config_path.exists():
|
|
||||||
try:
|
|
||||||
with open(config_path, 'r', encoding='utf-8') as f:
|
|
||||||
return json.load(f)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
print(f"Ошибка чтения JSON из {config_path}: {e}")
|
|
||||||
print("Используем конфигурацию по умолчанию.")
|
|
||||||
return default_config
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Не удалось открыть файл конфигурации: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
# Создаем файл конфигурации по умолчанию в корне проекта
|
|
||||||
try:
|
|
||||||
with open(config_path, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(default_config, f, indent=2, ensure_ascii=False)
|
|
||||||
print(f"Файл конфигурации создан по умолчанию: {config_path}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Не удалось создать файл конфигурации: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
return default_config
|
|
||||||
|
|
||||||
def generate_dnsmasq_config(project_root):
|
|
||||||
"""Генерирует конфигурационный файл dnsmasq в директории проекта."""
|
|
||||||
host_ip = get_host_ip()
|
|
||||||
config_path = project_root / "dnsmasq.conf"
|
|
||||||
services = read_services_config(project_root)
|
|
||||||
|
|
||||||
config_content = f"""# Локальные домены → твой ПК
|
|
||||||
"""
|
|
||||||
for service in services['services']:
|
|
||||||
config_content += f"address=/{service['name']}.local/{host_ip}\n"
|
|
||||||
|
|
||||||
config_content += f"""address=/.local/{host_ip}
|
|
||||||
|
|
||||||
# Кэш DNS
|
|
||||||
cache-size=500
|
|
||||||
|
|
||||||
# Лог (опционально)
|
|
||||||
log-queries
|
|
||||||
log-facility=/var/log/dnsmasq.log
|
|
||||||
|
|
||||||
# Не перехватывать внешние запросы — делегируем
|
|
||||||
no-resolv
|
|
||||||
server=8.8.8.8
|
|
||||||
server=1.1.1.1
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(config_path, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(config_content.strip())
|
|
||||||
print(f"Конфигурационный файл dnsmasq обновлен: {config_path}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Не удалось записать файл конфигурации dnsmasq: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def run_docker_command(cmd):
|
|
||||||
"""Выполняет команду Docker и возвращает результат"""
|
|
||||||
try:
|
|
||||||
print(f"Выполняю команду: {cmd}")
|
|
||||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"Ошибка выполнения команды: {cmd}")
|
|
||||||
print(f"stderr: {result.stderr}")
|
|
||||||
print(f"stdout: {result.stdout}")
|
|
||||||
return False
|
|
||||||
print(f"Команда выполнена успешно: {cmd}")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Ошибка выполнения команды: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def setup_project():
|
|
||||||
"""Настройка проекта: генерация конфигов и переход в директорию проекта."""
|
|
||||||
project_path = get_project_root()
|
|
||||||
print(f"Работаем в директории проекта: {project_path}")
|
|
||||||
|
|
||||||
# Генерируем конфигурационный файл dnsmasq
|
|
||||||
generate_dnsmasq_config(project_path)
|
|
||||||
|
|
||||||
# Переходим в директорию проекта
|
|
||||||
os.chdir(project_path)
|
|
||||||
print(f"Текущая директория изменена на: {os.getcwd()}")
|
|
||||||
return project_path
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# restart_services.py
|
|
||||||
from common import *
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("Перезапуск сервисов Docker Compose...")
|
|
||||||
|
|
||||||
# Настройка проекта (включает генерацию конфига dnsmasq)
|
|
||||||
project_path = setup_project() # Вызывает setup_project, который включает generate_dnsmasq_config
|
|
||||||
|
|
||||||
# Перезапускаем контейнеры
|
|
||||||
print("Перезапуск контейнеров Docker Compose...")
|
|
||||||
if not run_docker_command("docker-compose restart"):
|
|
||||||
print("Команда 'docker-compose restart' не удалась.")
|
|
||||||
print("Попытка поднять контейнеры с помощью 'docker-compose up -d'...")
|
|
||||||
if not run_docker_command("docker-compose up -d"):
|
|
||||||
print("Не удалось поднять контейнеры с помощью 'docker-compose up -d'.")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
print("Контейнеры подняты с помощью 'docker-compose up -d'.")
|
|
||||||
else:
|
|
||||||
print("Контейнеры успешно перезапущены!")
|
|
||||||
|
|
||||||
print("Сервисы перезапущены!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ssl_certificate /etc/nginx/certs/wildcard.local.pem;
|
|
||||||
ssl_certificate_key /etc/nginx/certs/wildcard.local.key;
|
|
||||||
Reference in New Issue
Block a user