diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/enhanced-nginx.iml b/.idea/enhanced-nginx.iml
new file mode 100644
index 0000000..c956989
--- /dev/null
+++ b/.idea/enhanced-nginx.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..0c2c824
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/php.xml b/.idea/php.xml
new file mode 100644
index 0000000..f324872
--- /dev/null
+++ b/.idea/php.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..83ffdc6
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,30 @@
+FROM nginx:1.26.0
+
+LABEL org.opencontainers.image.source="https://github.com/fedorov-xyz/nginx"
+
+RUN apt-get update && apt-get install --no-install-recommends -y \
+ nano \
+ curl \
+ cron \
+ certbot \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /etc/nginx/
+
+COPY nginx/nginxconfig.io/ ./nginxconfig.io
+COPY nginx/nginx.conf .
+
+COPY scripts/entrypoint.sh /
+RUN ["chmod", "+x", "/entrypoint.sh"]
+
+COPY scripts/update_cloudflare_ips.sh /usr/local/bin/update_cloudflare_ips.sh
+RUN chmod +x /usr/local/bin/update_cloudflare_ips.sh
+
+COPY cron/crontab /etc/cron.d/crontab
+RUN chmod 0644 /etc/cron.d/crontab
+RUN crontab /etc/cron.d/crontab
+RUN touch /var/log/cron.log
+
+# Запуск cron и nginx
+CMD ["/entrypoint.sh"]
diff --git a/README.md b/README.md
index 2df5036..0dbc943 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,63 @@
-# nginx
-Nginx Docker container with standalone certbot and automatic update of Cloudflare IP address ranges
+# Enhanced Nginx
+
+> Documentation in progress
+
+The script to generate the config for Cloudflare is taken from here: https://github.com/ergin/nginx-cloudflare-real-ip
+
+Nginx Docker container with standalone certbot and automatic update of Cloudflare IP address ranges.
+
+## How it works
+
+Instead of agonizing with certbot + nginx + well-known/acme-challenge, let's just let certbot do its job in standalone mode.
+
+When the container is started for the first time, certbot will bring up its server, issue and fail certificates. And then at the very end nginx will start up.
+
+When restarting the container, certbot will check the existing certificate for expiration, and if the expiration is ok, it will just not do anything, nginx will start right away.
+
+## Usage
+
+1. Specify 2 volumes for nginx and letsencrypt
+2. Mount your site config to container. You can mount multiple sites
+
+You should use the following lines in your nginx config for the site. `REPLACEMENT_CERT_NAME` will be replaced by the name of the certificate you pass to the container.
+
+```nginx configuration
+ ssl_certificate /etc/letsencrypt/live/REPLACEMENT_CERT_NAME/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/REPLACEMENT_CERT_NAME/privkey.pem;
+ ssl_trusted_certificate /etc/letsencrypt/live/REPLACEMENT_CERT_NAME/chain.pem;
+```
+
+If you want to serve multiple domains within a container, list their domains in the `SITE_DOMAINS` environment variable. A common certificate will be issued for them.
+
+Example configuration for Docker Compose:
+
+```yaml filename="docker-compose.yml"
+volumes:
+ nginx_data:
+ letsencrypt_data:
+
+services:
+ nginx:
+ image: fedorov-xyz/enhanced-nginx:latest
+ ports:
+ - 80:80
+ - 443:443
+ volumes:
+ - nginx_data:/etc/nginx/data
+ - letsencrypt_data:/etc/letsencrypt
+ - ./example.com.conf:/sites/example.com.conf
+ environment:
+ - SITE_DOMAINS=example.com,staging.example.com
+ - CERT_NAME=example.com
+ - CERTBOT_EMAIL=your@email.com
+ - CERTBOT_TEST_CERT=true
+```
+
+## Environment variables list
+
+| Variable | Requirded | Description |
+|----------------------|-----------|--------------------------------------------------------------|
+| `SITE_DOMAINS` | yes | Сomma-separated list of domains for Let's Encrypt certificate |
+| `CERT_NAME` | yes | Certificate name. |
+| `CERTBOT_EMAIL` | yes | Email for Let's Encrypt. |
+| `CERTBOT_TEST_CERT` | | Pass `true` for test certificates. |
diff --git a/cron/crontab b/cron/crontab
new file mode 100644
index 0000000..080254b
--- /dev/null
+++ b/cron/crontab
@@ -0,0 +1,3 @@
+# Auto sync ip addresses of Cloudflare and reload nginx
+# Every day at 8:30
+30 8 * * * /usr/local/bin/update_cloudflare_ips.sh restart_nginx >/dev/null 2>&1
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
new file mode 100644
index 0000000..b41e0b1
--- /dev/null
+++ b/nginx/nginx.conf
@@ -0,0 +1,88 @@
+# Generated by nginxconfig.io
+# See nginxconfig.txt for the configuration share link
+
+user nginx;
+pid /var/run/nginx.pid;
+worker_processes auto;
+worker_rlimit_nofile 65535;
+
+# Load modules
+include /etc/nginx/modules-enabled/*.conf;
+
+events {
+ multi_accept on;
+ worker_connections 65535;
+}
+
+http {
+ charset utf-8;
+ sendfile on;
+ tcp_nopush on;
+ tcp_nodelay on;
+ server_tokens off;
+ log_not_found off;
+ types_hash_max_size 2048;
+ types_hash_bucket_size 64;
+ client_max_body_size 16M;
+
+ # MIME
+ include mime.types;
+ default_type application/octet-stream;
+
+ # Log Format
+ include nginxconfig.io/log_format.conf;
+
+ # Logging
+ access_log off;
+ error_log /var/log/nginx/error.log warn;
+
+ # SSL
+ ssl_session_timeout 1d;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_tickets off;
+
+ # Diffie-Hellman parameter for DHE ciphersuites
+ ssl_dhparam /etc/nginx/data/dhparam.pem;
+
+ # Mozilla Intermediate configuration
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
+
+ # OCSP Stapling
+ ssl_stapling on;
+ ssl_stapling_verify on;
+ resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s;
+ resolver_timeout 2s;
+
+ # Connection header for WebSocket reverse proxy
+ map $http_upgrade $connection_upgrade {
+ default upgrade;
+ "" close;
+ }
+
+ map $remote_addr $proxy_forwarded_elem {
+
+ # IPv4 addresses can be sent as-is
+ ~^[0-9.]+$ "for=$remote_addr";
+
+ # IPv6 addresses need to be bracketed and quoted
+ ~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\"";
+
+ # Unix domain socket names cannot be represented in RFC 7239 syntax
+ default "for=unknown";
+ }
+
+ map $http_forwarded $proxy_add_forwarded {
+
+ # If the incoming Forwarded header is syntactically valid, append to it
+ "~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem";
+
+ # Otherwise, replace it
+ default "$proxy_forwarded_elem";
+ }
+
+ # Load configs and sites
+ # include /etc/nginx/conf.d/*.conf;
+ include /etc/nginx/data/cloudflare.conf;
+ include /etc/nginx/sites.d/*.conf;
+}
diff --git a/nginx/nginxconfig.io/general.conf b/nginx/nginxconfig.io/general.conf
new file mode 100644
index 0000000..84a3b58
--- /dev/null
+++ b/nginx/nginxconfig.io/general.conf
@@ -0,0 +1,9 @@
+# favicon.ico
+location = /favicon.ico {
+ log_not_found off;
+}
+
+# robots.txt
+location = /robots.txt {
+ log_not_found off;
+}
diff --git a/nginx/nginxconfig.io/gzip.conf b/nginx/nginxconfig.io/gzip.conf
new file mode 100644
index 0000000..d061996
--- /dev/null
+++ b/nginx/nginxconfig.io/gzip.conf
@@ -0,0 +1,6 @@
+# gzip
+gzip on;
+gzip_vary on;
+gzip_proxied any;
+gzip_comp_level 6;
+gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
diff --git a/nginx/nginxconfig.io/log_format.conf b/nginx/nginxconfig.io/log_format.conf
new file mode 100644
index 0000000..e548797
--- /dev/null
+++ b/nginx/nginxconfig.io/log_format.conf
@@ -0,0 +1 @@
+log_format cloudflare '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $http_cf_ray $http_cf_connecting_ip $http_cf_ipcountry';
diff --git a/nginx/nginxconfig.io/proxy.conf b/nginx/nginxconfig.io/proxy.conf
new file mode 100644
index 0000000..8b0d71e
--- /dev/null
+++ b/nginx/nginxconfig.io/proxy.conf
@@ -0,0 +1,20 @@
+proxy_http_version 1.1;
+proxy_cache_bypass $http_upgrade;
+
+# Proxy SSL
+proxy_ssl_server_name on;
+
+# Proxy headers
+proxy_set_header Upgrade $http_upgrade;
+proxy_set_header Connection $connection_upgrade;
+proxy_set_header X-Real-IP $remote_addr;
+proxy_set_header Forwarded $proxy_add_forwarded;
+proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+proxy_set_header X-Forwarded-Proto $scheme;
+proxy_set_header X-Forwarded-Host $host;
+proxy_set_header X-Forwarded-Port $server_port;
+
+# Proxy timeouts
+proxy_connect_timeout 60s;
+proxy_send_timeout 60s;
+proxy_read_timeout 60s;
diff --git a/nginx/nginxconfig.io/security.conf b/nginx/nginxconfig.io/security.conf
new file mode 100644
index 0000000..fbfe726
--- /dev/null
+++ b/nginx/nginxconfig.io/security.conf
@@ -0,0 +1,12 @@
+# security headers
+add_header X-XSS-Protection "1; mode=block" always;
+add_header X-Content-Type-Options "nosniff" always;
+add_header Referrer-Policy "no-referrer-when-downgrade" always;
+add_header Content-Security-Policy "default-src 'self' http: https: ws: wss: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always;
+add_header Permissions-Policy "interest-cohort=()" always;
+add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+
+# . files
+location ~ /\.(?!well-known) {
+ deny all;
+}
diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh
new file mode 100644
index 0000000..c2918bd
--- /dev/null
+++ b/scripts/entrypoint.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+# Static
+DOCKER_DATA=/etc/nginx/data
+CLOUDFLARE_FILE_PATH=/etc/nginx/data/cloudflare.conf
+DHPARAMS_FILE_PATH=$DOCKER_DATA/dhparams.pem
+
+# External
+: "${SITE_DOMAINS:?Must provide SITE_DOMAINS in environment}"
+: "${CERT_NAME:?Must provide CERT_NAME in environment}"
+: "${CERTBOT_EMAIL:?Must provide CERTBOT_EMAIL in environment}"
+
+mkdir -p $DOCKER_DATA;
+
+echo "Init nginx container"
+
+# Generate dhparams.pem
+if [ ! -f $DHPARAMS_FILE_PATH ]; then
+ echo "Generate dhparams.pem"
+ openssl dhparam -out $DHPARAMS_FILE_PATH 2048
+ chmod 600 $DHPARAMS_FILE_PATH
+fi
+
+# Generate Cloudflare IP ranges
+if [ ! -f $CLOUDFLARE_FILE_PATH ]; then
+ echo "Generate Cloudflare IP ranges config"
+ /usr/local/bin/update_cloudflare_ips.sh
+fi
+
+# Applying replacements for site configs
+
+cp -r /sites/* /etc/nginx/sites.d/
+
+declare -a replacements=(
+ "CERT_NAME"
+)
+
+for env in "${replacements[@]}"
+do
+ if [ -n "${!env}" ]; then
+ replacement=$(printf "%s" "${!env}" | sed 's/[,\/&]/\\&/g') # Escape special characters in the replacement value
+ for file in /etc/nginx/sites.d/*; do
+ sed -i "s,REPLACEMENT_$env,$replacement,g" "$file" || exit
+ done
+ echo " $env is updated in sites.d"
+ fi
+done
+
+# Check cert exist
+if [ ! -f /etc/letsencrypt/live/"$CERT_NAME"/fullchain.pem ]; then
+ echo "Certificate $CERT_NAME do not exists, will generate it now"
+fi
+
+echo "Running certbot"
+
+certbot_args=(
+ certonly
+ --standalone
+ --renew-by-default
+ --non-interactive
+ --agree-tos
+ --cert-name "$CERT_NAME"
+ --email "$CERTBOT_EMAIL"
+ -d "$SITE_DOMAINS"
+)
+
+if [ "$CERTBOT_TEST_CERT" = "true" ]; then
+ echo "Using staging (test) certificate"
+ certbot_args+=(--test-cert)
+fi
+
+certbot "${certbot_args[@]}" || exit
+
+echo "Start nginx"
+nginx -g "daemon off;"
diff --git a/scripts/update_cloudflare_ips.sh b/scripts/update_cloudflare_ips.sh
new file mode 100644
index 0000000..aa9cf34
--- /dev/null
+++ b/scripts/update_cloudflare_ips.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+CLOUDFLARE_FILE_PATH=/etc/nginx/data/cloudflare.conf
+
+echo "# Cloudflare IP ranges" > $CLOUDFLARE_FILE_PATH;
+echo "" >> $CLOUDFLARE_FILE_PATH;
+
+echo "# - IPv4" >> $CLOUDFLARE_FILE_PATH;
+for i in $(curl -s -L https://www.cloudflare.com/ips-v4); do
+ echo "set_real_ip_from $i;" >> $CLOUDFLARE_FILE_PATH;
+done
+
+echo "" >> $CLOUDFLARE_FILE_PATH;
+echo "# - IPv6" >> $CLOUDFLARE_FILE_PATH;
+for i in $(curl -s -L https://www.cloudflare.com/ips-v6); do
+ echo "set_real_ip_from $i;" >> $CLOUDFLARE_FILE_PATH;
+done
+
+echo "" >> $CLOUDFLARE_FILE_PATH;
+echo "real_ip_header CF-Connecting-IP;" >> $CLOUDFLARE_FILE_PATH;
+
+# Test configuration
+nginx -t;
+
+if [ "$1" = "restart_nginx" ]; then
+ systemctl reload nginx;
+fi