diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a0d45f8 --- /dev/null +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index df91287..43236ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,15 @@ -certs/ +# Environment +.env + +# Certificates and secrets +/nginx/certs/ +/ca/ +/acme/ + +# Service data +/services/*/data/ + +# IDE +.vscode/ +*.swp +*~ \ No newline at end of file diff --git a/README.md b/README.md index a40445b..23420c8 100644 --- a/README.md +++ b/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 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 ` +- **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 diff --git a/dnsmasq/dnsmasq.conf b/dnsmasq/dnsmasq.conf new file mode 100644 index 0000000..4cfc907 --- /dev/null +++ b/dnsmasq/dnsmasq.conf @@ -0,0 +1,2 @@ +# Этот файл будет перезаписан при запуске ./scripts/02-apply-config.sh +# Не редактируйте вручную \ No newline at end of file diff --git a/docker-compose.dnsmasq.yml b/docker-compose.dnsmasq.yml new file mode 100644 index 0000000..3c640f4 --- /dev/null +++ b/docker-compose.dnsmasq.yml @@ -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 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 669e4cd..efe709e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,55 +1,70 @@ -services: - 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 +version: '3.8' +services: nginx-proxy: image: nginxproxy/nginx-proxy:alpine container_name: nginx-proxy - restart: always + restart: unless-stopped ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./certs:/etc/nginx/certs:ro - - ./vhost.d:/etc/nginx/vhost.d:ro - - ./html:/usr/share/nginx/html:rw + - ./nginx/certs:/etc/nginx/certs:ro + - ./nginx/vhost.d:/etc/nginx/vhost.d:ro + - ./nginx/html:/usr/share/nginx/html:rw 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: - - "com.github.nginx-proxy.nginx" + - "com.github.nginx-proxy.nginx-proxy=true" nginx-proxy-companion: image: nginxproxy/acme-companion container_name: nginx-proxy-companion - restart: always + restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - - ./certs:/etc/nginx/certs:rw + - ./nginx/certs:/etc/nginx/certs:rw - ./acme:/etc/acme.sh - - ./vhost.d:/etc/nginx/vhost.d - - ./html:/usr/share/nginx/html + - ./nginx/vhost.d:/etc/nginx/vhost.d:rw networks: - devnet environment: + - DEFAULT_EMAIL=${LETSENCRYPT_EMAIL} - 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: devnet: - external: true + driver: bridge + ipam: + config: + - subnet: ${SUBNET} + gateway: ${GATEWAY} volumes: acme: vhost.d: - html: + html: \ No newline at end of file diff --git a/nginx/html/index.html b/nginx/html/index.html new file mode 100644 index 0000000..c54cefe --- /dev/null +++ b/nginx/html/index.html @@ -0,0 +1,7 @@ + + +Placeholder + +

Run ./scripts/00-init.sh to generate content

+ + \ No newline at end of file diff --git a/scripts/00-init.sh b/scripts/00-init.sh new file mode 100644 index 0000000..6d36416 --- /dev/null +++ b/scripts/00-init.sh @@ -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 \ No newline at end of file diff --git a/scripts/01-generate-cert.sh b/scripts/01-generate-cert.sh new file mode 100644 index 0000000..6580ae9 --- /dev/null +++ b/scripts/01-generate-cert.sh @@ -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" \ No newline at end of file diff --git a/scripts/02-apply-config.sh b/scripts/02-apply-config.sh new file mode 100644 index 0000000..364ae1a --- /dev/null +++ b/scripts/02-apply-config.sh @@ -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" < "$PROJECT_ROOT/nginx/html/index.html" < + +Добро пожаловать! + +

Сервисы .$LOCAL_DOMAIN

+

Добавьте свои сервисы в папку services/

+ + +EOF + +echo "Конфигурация применена для .$LOCAL_DOMAIN" \ No newline at end of file diff --git a/scripts/add-service.sh b/scripts/add-service.sh new file mode 100644 index 0000000..a023a62 --- /dev/null +++ b/scripts/add-service.sh @@ -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" <