diff --git a/.dockerignore b/.dockerignore index 3fee6d0..e62bef4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,7 @@ node_modules/ .env .venv/ docker-compose-example/ +docker-compose*.yml +dev.sh +index.html +site.webmanifest diff --git a/.env.example b/.env.example index 47a5178..064f97f 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ # # Po zmianie wartości uruchom: ./dev.sh config (lub ./dev.sh restart) -# --- Frontend Server --- +# --- Frontend Server (TYLKO lokalny dev — nie używaj w Dokploy) --- # Port na hoście (localhost). W kontenerze nginx zawsze nasłuchuje na 80. FRONTEND_PORT=8082 diff --git a/Dockerfile b/Dockerfile index 8dab85c..ad5e792 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,8 @@ RUN apk add --no-cache gettext # Remove default nginx config RUN rm /etc/nginx/conf.d/default.conf -# Copy nginx config from project root +# Copy nginx configs from project root +COPY nginx.conf /etc/nginx/nginx.conf COPY default.conf /etc/nginx/conf.d/default.conf # Copy static assets diff --git a/default.conf b/default.conf index e8cb034..4b5a82f 100644 --- a/default.conf +++ b/default.conf @@ -10,9 +10,24 @@ server { gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; gzip_min_length 1024; + # Block hidden files and sensitive extensions + location ~ /\. { + deny all; + return 404; + } + + location ~* \.(sh|template|env|yml|yaml|md|git|bak|sql|log)$ { + deny all; + return 404; + } + # Main page - location / { - try_files $uri $uri/ /index.html; + location = / { + try_files /index.html =404; + } + + location = /index.html { + add_header Cache-Control "no-cache"; } # Web app manifest @@ -29,7 +44,15 @@ server { try_files $uri =404; } + # Everything else → 404 (no fallback to index.html) + location / { + return 404; + } + # Security headers add_header X-Frame-Options SAMEORIGIN always; add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://www.youtube.com https://www.gstatic.com; style-src 'self' 'unsafe-inline'; img-src 'self' https://img.youtube.com data:; frame-src https://www.youtube.com; connect-src 'self' https://www.youtube.com; base-uri 'self'; form-action 'none'; object-src 'none'; upgrade-insecure-requests;" always; } diff --git a/docker-compose-example/.env.example b/docker-compose-example/.env.example index 162ba1c..c11eddb 100644 --- a/docker-compose-example/.env.example +++ b/docker-compose-example/.env.example @@ -1,12 +1,9 @@ -# Slavic Pulse — Environment Configuration (Dokploy) +# Slavic Pulse — Environment Configuration (Dokploy / produkcja) # Copy to .env before deploy if needed. # Never commit .env with real credentials to version control. - -# --- Gitea (opcjonalnie — tylko gdy repo jest prywatne) --- -# Domyślnie clone używa publicznego URL bez auth. -# Dokploy Gitea Provider (OAuth) NIE autoryzuje git clone wewnątrz kontenera — -# nadal potrzebujesz publicznego repo LUB tokena poniżej. -# GITEA_REPO_URL=https://oauth2:TWÓJ_TOKEN@gitea.expert-it.agency-ai.dev/tomasz-syn-grzegorza/site-slavic-pulse-com.git +# +# Tylko zmienne runtime strony — bez GITEA_REPO_URL, bez FRONTEND_PORT. +# Dokploy buduje obraz z Dockerfile; repo klonowane jest na hoście przez Dokploy. # --- Strona (meta, SEO, PWA) --- SITE_TITLE="Slavic Pulse" diff --git a/docker-compose-example/README.md b/docker-compose-example/README.md index dfba16d..d656386 100644 --- a/docker-compose-example/README.md +++ b/docker-compose-example/README.md @@ -9,6 +9,7 @@ site-slavic-pulse-com/ ├── docker-compose.yml # Lokalny dev (build z Dockerfile) ├── Dockerfile ├── default.conf +├── nginx.conf ├── index.html.template # Szablon strony ├── generate-from-env.sh # Generator HTML z .env ├── assets/imgs/favico/ # Favicony @@ -29,19 +30,18 @@ site-slavic-pulse-com/ - `www.slavic-pulse.com` ← **wymagane** — bez tego www dostaje domyślny cert Traefika (CN=*, self-signed) i telefony pokazują „serwer nie potrafi udowodnić tożsamości” 4. (Opcjonalnie) **Advanced → Redirects** → preset **www → non-www**, żeby `www` przekierowywało na apex 5. Traefik routuje ruch HTTPS → port 80 kontenera (bez mapowania portu na hosta) -6. Deploy +6. Deploy — Dokploy klonuje repo na hoście i **buduje obraz z Dockerfile** (brak git clone w kontenerze) **Porty:** nie używaj `ports:` w compose — Traefik/Dokploy łączy się z wewnętrznym `:80`. W `docker ps` kolumna PORTS może być pusta — to normalne przy routingu przez Traefik. -**Gitea Provider w Dokploy** (OAuth) — autoryzuje Dokploy do listowania repo i deployu z UI, -ale **nie** zastępuje credentials dla `git clone` wewnątrz kontenera przy starcie. -Dla prywatnego repo: ustaw repo jako Public **lub** dodaj `GITEA_REPO_URL` z Deploy Token w `.env`. +**Gitea Provider w Dokploy** (OAuth) — autoryzuje Dokploy do listowania repo i deployu z UI. +Repo klonowane jest na **hoście** przez Dokploy; kontener nie potrzebuje tokena GITEA ani `git clone` w runtime. ## Jak to działa -1. **Start kontenera** — `git clone` repozytorium z Gitea -2. **`generate-from-env.sh`** — buduje `index.html` i `site.webmanifest` ze zmiennych `.env` +1. **Build** — Dokploy buduje obraz z `Dockerfile` (nginx + assets + szablony) +2. **Start kontenera** — entrypoint generuje `index.html` i `site.webmanifest` ze zmiennych `.env`, usuwa szablony z dysku 3. **Tło** — miniaturka YouTube (`img.youtube.com`) dopasowana do FHD/vertical 4. **Intro wideo** — YouTube IFrame API, autoplay 5. **Po filmie** — przekierowanie (jeśli `POST_VIDEO_REDIRECT_ENABLED=true`) lub tło z miniaturki @@ -50,43 +50,61 @@ Dla prywatnego repo: ustaw repo jako Public **lub** dodaj `GITEA_REPO_URL` z Dep | Zmienna | Opis | |---------|------| -| `GITEA_REPO_URL` | Opcjonalnie — URL clone z tokenem dla prywatnego repo | | `VIDEO_ID_FHD` / `VIDEO_ID_VERTICAL` | ID shortów YouTube | | `POST_VIDEO_REDIRECT_URL_*` | URL po zakończeniu filmu | | `POST_VIDEO_REDIRECT_ENABLED` | `true` = redirect, `false` = zostaje tło | -## Rozwiązywanie problemów +**Nie dodawaj do produkcyjnego `.env`:** `GITEA_REPO_URL`, `FRONTEND_PORT` — nie są używane w tym modelu deployu. -### Kontener `Restarting (128)`, brak portów w `docker ps` +## Hardening bezpieczeństwa (wbudowany) -**Przyczyna:** `git clone` nie może pobrać repozytorium (najczęściej **prywatne repo** bez credentials). +- Build z Dockerfile zamiast runtime git clone — brak tokenów w kontenerze +- nginx: whitelist ścieżek, blokada `.env`/`.sh`/`.template`, nagłówki CSP/Referrer-Policy +- Szablony i generator usuwane z kontenera po wygenerowaniu HTML +- `server_tokens off` — ukryta wersja nginx + +## Weryfikacja po deployu -**Diagnostyka na serwerze:** ```bash -docker logs slavic-pulse-com-frontend --tail 50 +# Strona działa +curl -sI https://slavic-pulse.com/ | head -5 + +# Wrażliwe pliki niedostępne (404) +curl -sI https://slavic-pulse.com/.env | grep HTTP +curl -sI https://slavic-pulse.com/generate-from-env.sh | grep HTTP +curl -sI https://slavic-pulse.com/random-path | grep HTTP + +# Nagłówki bezpieczeństwa +curl -sI https://slavic-pulse.com/ | grep -iE 'content-security-policy|referrer-policy|permissions-policy' + +# Brak wersji nginx w Server +curl -sI https://slavic-pulse.com/ | grep -i server + +# W kontenerze (na serwerze) +docker exec slavic-pulse-com-frontend ls /opt/slavic-pulse 2>&1 # powinno: No such file +docker inspect slavic-pulse-com-frontend --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -i gitea # brak wyniku ``` -Typowy błąd: -``` -fatal: could not read Username for 'https://gitea.expert-it.agency-ai.dev': No such device or address -``` - -**Rozwiązanie A (najprostsze):** ustaw repozytorium jako **Public** w Gitea → Settings → Repository → Make Repository Public → redeploy. - -**Rozwiązanie B (repo prywatne):** w Gitea utwórz Deploy Token / PAT i dodaj do `.env` w Dokploy: -```env -GITEA_REPO_URL=https://oauth2:TWÓJ_TOKEN@gitea.expert-it.agency-ai.dev/tomasz-syn-grzegorza/site-slavic-pulse-com.git -``` +## Rozwiązywanie problemów ### Brak portów w `docker ps` (kolumna PORTS pusta) Przy Traefik/Dokploy bez `ports:` w compose — **to oczekiwane**. Sprawdź w Dokploy UI: domena → Container Port = **80**, kontener `Up (healthy)`. +### Kontener nie startuje / unhealthy + +**Diagnostyka na serwerze:** +```bash +docker logs slavic-pulse-com-frontend --tail 50 +``` + +Typowe przyczyny: błąd builda Dockerfile, brak `.env`, błąd w `generate-from-env.sh`. + ### Zmiana konfiguracji po deployu 1. Edytuj `.env` w Dokploy -2. Redeploy / restart kontenera (przy starcie ponownie klonuje repo i generuje HTML) +2. Redeploy / restart kontenera (entrypoint regeneruje HTML z nowych zmiennych) ### Błąd certyfikatu SSL na telefonie („serwer nie potrafi udowodnić…”) diff --git a/docker-compose-example/docker-compose.yml b/docker-compose-example/docker-compose.yml index 0bc678b..d38195e 100644 --- a/docker-compose-example/docker-compose.yml +++ b/docker-compose-example/docker-compose.yml @@ -5,13 +5,15 @@ # W UI Dokploy ustaw domenę i Container Port = 80 — Traefik routuje po FQDN. # Wewnętrznie nginx nasłuchuje na :80 (expose poniżej). # -# Runtime git clone — repozytorium musi być publiczne LUB ustaw GITEA_REPO_URL w .env -# (patrz README — prywatne repo bez tokenu = exit 128, restart loop). +# Build z Dockerfile — Dokploy klonuje repo na hoście i buduje obraz. +# Brak runtime git clone w kontenerze (hardening bezpieczeństwa). # ============================================================ services: frontend: - image: nginx:1.27-alpine + build: + context: .. + dockerfile: Dockerfile container_name: slavic-pulse-com-frontend restart: unless-stopped @@ -21,30 +23,12 @@ services: env_file: - .env - command: > - sh -ce " - echo '[slavic-pulse] Installing git + gettext...' && - apk add --no-cache git gettext && - echo '[slavic-pulse] Cloning repo...' && - rm -rf /app && - git clone --depth 1 \"$${GITEA_REPO_URL:-https://gitea.expert-it.agency-ai.dev/tomasz-syn-grzegorza/site-slavic-pulse-com.git}\" /app && - echo '[slavic-pulse] Generating index.html from .env...' && - chmod +x /app/generate-from-env.sh && - TEMPLATE_DIR=/app /app/generate-from-env.sh /usr/share/nginx/html && - echo '[slavic-pulse] Copying assets and nginx config...' && - cp -r /app/assets /usr/share/nginx/html/assets && - rm -f /etc/nginx/conf.d/default.conf && - cp /app/default.conf /etc/nginx/conf.d/default.conf && - echo '[slavic-pulse] Starting nginx...' && - nginx -g 'daemon off;' - " - healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:80/"] interval: 30s timeout: 10s retries: 3 - start_period: 60s + start_period: 15s deploy: resources: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 964f028..b146e89 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,2 +1,4 @@ #!/bin/sh TEMPLATE_DIR=/opt/slavic-pulse /opt/slavic-pulse/generate-from-env.sh /usr/share/nginx/html +rm -rf /opt/slavic-pulse +unset GITEA_REPO_URL FRONTEND_PORT 2>/dev/null || true diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..0460e41 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,30 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + keepalive_timeout 15; + client_body_timeout 10; + client_header_timeout 10; + send_timeout 10; + + server_tokens off; + + include /etc/nginx/conf.d/*.conf; +}