diff --git a/.gitignore b/.gitignore index 43236ab..905d5be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Environment .env +domains.txt # Certificates and secrets /nginx/certs/ diff --git a/README.md b/README.md index 23420c8..8d9e0ef 100644 --- a/README.md +++ b/README.md @@ -1,130 +1,117 @@ -# Local DNS Services with Docker +# Локальная сеть с доменами `.mikan` и HTTPS -This project provides a local development infrastructure with automatic DNS resolution for custom domains (e.g., `.mikan` or `.local`) and valid HTTPS certificates. +Система для запуска сервисов в Docker с автоматическим HTTPS, локальными доменами (`service.mikan`, `service.local`) и возможностью расширения (Tailscale, публичные домены). -## 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 +- Docker + Docker Compose +- `mkcert` — [инструкция по установке](https://github.com/FiloSottile/mkcert#installation) +- `bash`, `coreutils` -## Requirements +## Быстрый старт -- Docker and Docker Compose -- `mkcert` installed on the host system - -## Quick Start - -1. Clone this repository and enter the directory: +1. **Клонируй репозиторий** ```bash - git clone local-dns - cd local-dns + git clone ... && cd local-dns ``` -2. Initialize the project: +2. **Настрой домен (опционально)** ```bash - ./scripts/00-init.sh + echo "LOCAL_DOMAIN=mikan" > .env ``` - This creates a `.env` file from `.env.example` and sets up the directory structure. + Замени `mikan` на желаемый суффикс (например, `home`). -3. Generate TLS certificates: +3. **Инициализируй CA и базовый сертификат** ```bash - ./scripts/01-generate-cert.sh + ./scripts/01-init-ca.sh ``` - This creates trusted certificates for your local domain. + - Создаст доверенный корневой сертификат (`ca/rootCA.pem`) + - Сгенерирует `default.crt` для nginx-proxy -4. Start the core infrastructure: +4. **Установи корневой сертификат в систему** + Импортируй файл `ca/rootCA.pem` в хранилище **доверенных корневых центров сертификации**: + - **Linux**: + ```bash + sudo cp ca/rootCA.pem /usr/local/share/ca-certificates/mkcert-local.crt + sudo update-ca-certificates + ``` + - **Windows/macOS**: двойной клик по файлу → «Доверять всегда» (macOS) или импорт в «Доверенные корневые центры» (Windows) + +5. **Запусти инфраструктуру** ```bash docker compose up -d ``` -5. Add your first service (example: Gitea on port 3000): +6. **Добавь сервис** ```bash - ./scripts/add-service.sh gitea 3000 + ./scripts/add-service.sh gitea ``` - Then edit `services/gitea/docker-compose.yml` to set the correct image and volumes. + Сервис будет доступен по: + - `https://gitea.mikan` + - `https://gitea.local` -6. Start the new service: - ```bash - docker compose -f docker-compose.yml -f services/gitea/docker-compose.yml up -d + \+ может пригодиться: + + ```bash + Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value "10.0.0.3 youtrack.mikan" + ``` + +7. **Настрой сервис в Docker Compose** + Убедись, что в `docker-compose.yml` сервиса указано: + ```yaml + services: + gitea: + environment: + - VIRTUAL_HOST=gitea.mikan,gitea.local + - VIRTUAL_PORT=3000 # порт внутри контейнера + networks: + - devnet + dns: 10.0.0.2 # обязательно для резолвинга доменов из контейнеров ``` -7. Visit `https://gitea.yourdomain` in your browser (replace `yourdomain` with your configured domain). +## Важные замечания -## Configuration +### DNS из контейнеров +Контейнеры **не могут** резолвить `*.mikan` без явного указания DNS: +```yaml +dns: 10.0.0.2 # IP dnsmasq +``` +Добавляй эту строку во все сервисы, которым нужен доступ к другим по домену. -Edit the `.env` file to customize your setup: +### Обновление сертификатов +При добавлении сервиса через `add-service.sh`: +- Автоматически обновляется `domains.txt` +- Генерируется новый `default.crt` +- Перезагружается `nginx-proxy` -- `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.) +### Пути +- Сертификаты: `nginx/certs/default.crt` +- Домены: `domains.txt` +- Корневой CA: `ca/rootCA.pem` -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 +### Tailscale +Добавь в `docker-compose.yml` сервис `tailscale` и объявляй дополнительные домены: +```yaml +environment: + - VIRTUAL_HOST=gitea.mikan,gitea.local,gitea.yourname.ts.net ``` -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" +### Публичный домен +1. Купи домен (например, `example.com`) +2. В `.env` добавь: + ```env + PUBLIC_DOMAIN=example.com ``` - -## 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 +3. В сервисах укажи: + ```yaml + environment: + - VIRTUAL_HOST=gitea.mikan,gitea.local,gitea.example.com + - LETSENCRYPT_HOST=gitea.example.com ``` + Сертификат для `example.com` получит `acme-companion`. -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 ` -- **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. \ No newline at end of file +Готово. Теперь все сервисы работают по HTTPS с валидными сертификатами. \ No newline at end of file diff --git a/docker-compose.dnsmasq.yml b/docker-compose.dnsmasq.yml deleted file mode 100644 index 3c640f4..0000000 --- a/docker-compose.dnsmasq.yml +++ /dev/null @@ -1,26 +0,0 @@ -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 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index efe709e..c3e3c14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,13 +56,30 @@ services: depends_on: - nginx-proxy + 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: - driver: bridge - ipam: - config: - - subnet: ${SUBNET} - gateway: ${GATEWAY} + external: true + name: devnet volumes: acme: diff --git a/scripts/00-init.sh b/scripts/00-init.sh index 6d36416..2e7af30 100644 --- a/scripts/00-init.sh +++ b/scripts/00-init.sh @@ -24,7 +24,6 @@ echo "Проверка сети devnet..." if ! docker network inspect devnet &>/dev/null; then echo "Создаём сеть devnet..." docker network create \ - --driver=bridge \ --subnet="${SUBNET}" \ --gateway="${GATEWAY}" \ devnet diff --git a/scripts/01-generate-cert.sh b/scripts/01-generate-cert.sh deleted file mode 100644 index 6580ae9..0000000 --- a/scripts/01-generate-cert.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/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" \ No newline at end of file diff --git a/scripts/01-init-ca.sh b/scripts/01-init-ca.sh new file mode 100644 index 0000000..f366c96 --- /dev/null +++ b/scripts/01-init-ca.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$SCRIPT_DIR/.." + +if [ -f "$ROOT_DIR/.env" ]; then + export $(grep -v '^#' "$ROOT_DIR/.env" | xargs) +fi + +LOCAL_DOMAIN=${LOCAL_DOMAIN:-mikan} +CA_DIR="$ROOT_DIR/ca" +CERTS_DIR="$ROOT_DIR/nginx/certs" +DOMAINS_FILE="$ROOT_DIR/domains.txt" + +mkdir -p "$CA_DIR" "$CERTS_DIR" + +if [ ! -f "$DOMAINS_FILE" ]; then + touch "$DOMAINS_FILE" +fi + +if [ ! -f "$CA_DIR/rootCA.pem" ]; then + mkcert -install + cp "$(mkcert -CAROOT)/rootCA.pem" "$CA_DIR/rootCA.pem" + cp "$(mkcert -CAROOT)/rootCA-key.pem" "$CA_DIR/rootCA-key.pem" + + cat > "$DOMAINS_FILE" < <порт>" - echo "Пример: $0 gitea 3000" - exit 1 -fi - -NAME=$1 -PORT=$2 - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$SCRIPT_DIR/.." +ROOT_DIR="$SCRIPT_DIR/.." -if [ ! -f "$PROJECT_ROOT/.env" ]; then - echo "Файл .env не найден. Запустите сначала ./scripts/00-init.sh" +if [ -f "$ROOT_DIR/.env" ]; then + export $(grep -v '^#' "$ROOT_DIR/.env" | xargs) +fi + +LOCAL_DOMAIN=${LOCAL_DOMAIN:-mikan} + +if [ $# -lt 1 ]; then + echo "Usage: $0 [extra_domains...]" exit 1 fi -export $(grep -v '^#' "$PROJECT_ROOT/.env" | xargs) +SERVICE_NAME=$1 +shift +EXTRA_DOMAINS=("$@") -SERVICE_DIR="$PROJECT_ROOT/services/$NAME" +DOMAINS_FILE="$ROOT_DIR/domains.txt" -if [ -d "$SERVICE_DIR" ]; then - echo "Сервис '$NAME' уже существует!" - exit 1 +if [ ! -f "$DOMAINS_FILE" ]; then + touch "$DOMAINS_FILE" fi -mkdir -p "$SERVICE_DIR/data" +PRIMARY="${SERVICE_NAME}.${LOCAL_DOMAIN}" +ALIAS="${SERVICE_NAME}.local" -cat > "$SERVICE_DIR/docker-compose.yml" <> "$DOMAINS_FILE" -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 +sort -u "$DOMAINS_FILE" | grep -v '^$' > "$DOMAINS_FILE.tmp" && mv "$DOMAINS_FILE.tmp" "$DOMAINS_FILE" -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" \ No newline at end of file +exec "$SCRIPT_DIR/regenerate-cert.sh" \ No newline at end of file diff --git a/scripts/regenerate-cert.sh b/scripts/regenerate-cert.sh new file mode 100644 index 0000000..abf333e --- /dev/null +++ b/scripts/regenerate-cert.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$SCRIPT_DIR/.." + +if [ -f "$ROOT_DIR/.env" ]; then + export $(grep -v '^#' "$ROOT_DIR/.env" | xargs) +fi + +DOMAINS_FILE="$ROOT_DIR/domains.txt" +CERTS_DIR="$ROOT_DIR/nginx/certs" + +if [ ! -f "$DOMAINS_FILE" ]; then + echo "Error: $DOMAINS_FILE not found" >&2 + exit 1 +fi + +mapfile -t DOMAINS < <(grep -v '^$' "$DOMAINS_FILE") + +if [ ${#DOMAINS[@]} -eq 0 ]; then + echo "Error: no domains found in $DOMAINS_FILE" >&2 + exit 1 +fi + +mkcert \ + -cert-file "$CERTS_DIR/default.crt.new" \ + -key-file "$CERTS_DIR/default.key.new" \ + "${DOMAINS[@]}" + +mv "$CERTS_DIR/default.crt.new" "$CERTS_DIR/default.crt" +mv "$CERTS_DIR/default.key.new" "$CERTS_DIR/default.key" \ No newline at end of file diff --git a/scripts/reload-nginx.sh b/scripts/reload-nginx.sh new file mode 100644 index 0000000..2ee811d --- /dev/null +++ b/scripts/reload-nginx.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$SCRIPT_DIR/.." + +CERTS_DIR="$ROOT_DIR/nginx/certs" +cp "$CERTS_DIR/default.crt" "$CERTS_DIR/default.key" "$CERTS_DIR/" + +docker compose -f "$ROOT_DIR/docker-compose.yml" exec nginx-proxy nginx -s reload 2>/dev/null || \ +docker compose -f "$ROOT_DIR/docker-compose.yml" restart nginx-proxy \ No newline at end of file