Nginx jako reverse proxy dla Apache

Nginx? Reverse proxy? O co tu chodzi?

Nginx jest lekkim serwerem WWW oraz serwerem proxy dla HTTP. Zaprojektowany został z myślą o wysokiej dostępności i silnie obciążonych serwisach (położono nacisk na skalowalność i niską zajętość zasobów).

Reverse proxy może spełniać różne funkcje – w naszym przypadku będziemy chcieli użyć go do serwowania statycznego contentu (cache) oraz weryfikacji certyfikatu SSL. Ponadto w późniejszym czasie można wykorzystać reverse-proxy jako load balancer.

Docker i docker-compose.

Zarówno nginx oraz Apache z PHP-em będą znajdowały się w kontenerach Docker. Nie opisuję tutaj w jaki sposób zainstalować Dockera na serwerze. Jedynie wspomnę, że będziemy potrzebowali Docker i docker-compose.

Konfiguracja kontenera Dockera z WordPress.

Dla WordPress korzystam z jego oficjalnego obrazu na Docker Hub: https://hub.docker.com/_/wordpress. W tym przykładzie korzystam z tagu php7.4-apache

wordpress:php7.4-apache

Poniżej przedstawiam konfigurację pliku docker-compose.yml oraz schemat katalogów.

version: '3'
services:
  php:
    image: wordpress:php7.4-apache
    restart: always
    ports:
      - 8080:80
    volumes:
      - ./www:/var/www/html:cached
      - ./config/apache.conf:/etc/apache2/sites-enabled/000-default.conf

Plik config/apache.conf (vhost):

<VirtualHost *:80>
        DocumentRoot /var/www/html

        <Directory /var/www/html>
                AllowOverride All
                Order Allow,Deny
                Allow from All

                RewriteEngine On
                RewriteBase /
                RewriteRule ^index\.php$ - [L]
                RewriteCond %{REQUEST_FILENAME} !-f
                RewriteCond %{REQUEST_FILENAME} !-d
                RewriteRule . /index.php [L]
        </Directory>
</VirtualHost>

Struktura plików i katalogów:

| example.com
|- config/
|-- apache.conf # apache vhost
|- docker-compose.yml
|- www/ # katalog z WordPress

Aplikacja WordPress znajduje się w katalogu www.

Teraz uruchamiam kontener będąc w katalogu example.com.
docker-compose up -d.

Konfiguracja WordPress.

Aby WordPress poprawnie działał musi widzieć, że na proxy działa SSL. Bez tej dodatkowej konfiguracji PHP będzie brał pod uwagę tylko informacje, które dostarcza mu Apache działający na porcie 80 (HTTP).

Do pliku wp-config.php dodaję poniższy fragment kodu.

/** SSL */
define('FORCE_SSL_ADMIN', true);

if (strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false){
    $_SERVER['HTTPS']='on';
}

Konfiguracja kontenera Dockera z nginx-em.

W przypadku kontenera nginx’a także korzystam z oficjalnego repozytorium na Docker Hub: https://hub.docker.com/_/nginx. Najczęściej korzystam z tagu nginx:alpine.

Poniżej konfiguracja docker-compose.yml dla nginx.

version: '3'
services:
  nginx:
    image: nginx:alpine
    restart: always
    environment:
      - "TZ=Europe/Warsaw"
    network_mode: "host"
    volumes:
      - ./var/www:/var/www
      - /var/log/nginx:/var/log/nginx

Po zapisaniu konfiguracji uruchamiam kontener poleceniem docker-compose up -d.

Po uruchomieniu warto przenieść (skopiować z kontenera) istniejącą konfigurację. Pozwoli to na edycję konfiguracji bez obaw utraty przy restarcie Docker’a.

Kopiowanie zawartości katalogu /etc/nginx/:
docker cp nginx_nginx_1:/etc/nginx etc/,
gdzie nginx_nginx_1 to nazwa naszego kontenera z nginx-em.

Oprócz konfiguracji nginx’a na serwer należy skopiować katalog z konfiguracją pod Let’s Encrypta:
docker cp nginx_nginx_1:/etc/letsencrypt etc/.

Po powyższych zabiegach kontener z nginx-em należy wyłączyć (docker-compose down) i dodać do sekcji volumes w docker-compose.yml poniższe linijki:

      - ./etc/letsencrypt:/etc/letsencrypt
      - ./etc/nginx:/etc/nginx

Konfiguracja nginx (vhost i SSL).

Kiedy jest już skonfigurowany kontener z nginx-em pozostaje utworzenie vhosta. Najprostszy vhost dla HTTP (port 80), który pozwoli za chwilę na instalację darmowego certyfikatu SSL należy zapisać w lokalizacji etc/nginx/sites-enabled/example.com.conf.

server {
        listen 80;
        listen [::]:80;
        server_name example.com;
        include snippets/letsencrypt.conf;
        location / {
                return 301 https://example.com\$request_uri;
        }
}

Plik snippets/letsencrypt.conf:

location ^~ /.well-known/acme-challenge/ {
    allow all;
    default_type "text/plain";
    root /var/www/letsencrypt/;
}

Teraz włączam kontener z nginx’em i uruchamiam kolejny do instalacji certyfikatu SSL za pomocą poniższej komendy.

docker run -it --rm --name certbot \
        -v `pwd`/etc/letsencrypt:/etc/letsencrypt \
        -v `pwd`/var/lib/letsencrypt:/var/lib/letsencrypt \
        -v `pwd`/var/www:/var/www \
        --network host \
        certbot/certbot certonly --webroot -d "example.com" --email [email protected] -w /var/www/letsencrypt -n --agree-tos

Jeśli certyfikat zostanie poprawnie zainstalowany powinien wyświetlić się komunikat podobny do poniższego przykładu.

IMPORTANT NOTES:
Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/example.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/example.com/privkey.pem
Your cert will expire on 2020-06-10. To obtain a new or tweaked version of this certificate in the future, simply run letsencrypt-auto again. To non-interactively renew all of your certificates, run "letsencrypt-auto renew".
If you like Certbot, please consider supporting our work by:
Donating to ISRG / Let's Encrypt:
https://letsencrypt.org/donate
Donating to EFF:
https://eff.org/donate-le 

Kolejnym krokiem jest dodanie wpisu do poprzednio utworzonego vhosta dla SSL (port 443).

server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;

        access_log  /var/log/nginx/example.com_access.log;
        error_log   /var/log/nginx/example.com_error.log;

        server_name example.com;
        root /var/www/html;

        # SSL
        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
        ssl_trusted_certificate /etc/letsencrypt/live/example.com/fullchain.pem;

        # reverse proxy
        location / {
            proxy_pass http://0.0.0.0:8080; # 8080 - port Apache
            include snippets/proxy.conf;
        }
}

Plik snippets/proxy.conf:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Server-Addr $server_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;

Po zapisaniu zmian należy zrestartować kontener i sprawdzić czy wszystko działa.

Przejście na stronę http://example.com powinno przekierować na https://example.com z poprawnym certyfikatem. Jeśli wszystko działa jak należy przystępuję do skonfigurowania cache’u.

Cache w nginx.

Do istniejącego vhosta nginx’a (etc/nginx/sites-enabled/example.com.conf) należy dodać poniższe konfiguracje.

Na początku pliku:

proxy_cache_path /var/cache/nginx/proxy_cache_dir/example.com levels=1:2 keys_zone=example.com:100m max_size=700m inactive=6h use_temp_path=off;

proxy_buffers 256 16k;
proxy_buffer_size 32k;

W sekcji server dla SSL-a (443) po certyfikatach polecam dodać poniższe wpisy, które pozwolą na pominięcie cache’owania dla ciasteczek sesyjnych zalogowanych użytkowników oraz komentujących i wybranych ścieżek WordPress.

# WordPress cookies (https://www.cookielawinfo.com/wordpress-cookies-list-why-they-are-used/)
if ($http_cookie ~* "wordpress_(?!test_cookie)|comment_author_" ) {
    set $no_cache 1;
}

# URLs
if ($request_uri ~* "/wp-admin/|wp-.*.php|/feed/|/xmlrpc.php|sitemap(_index)?.xml") {
    set $no_cache 1;
}

Następnym krokiem jest dodanie informacji o cache’u w sekcji location pod komentarzem #reverse proxy.

proxy_cache example.com;
proxy_cache_valid 200 304 6h;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_bypass $no_cache;
proxy_no_cache $no_cache;
proxy_cache_background_update on;
proxy_cache_lock on;
proxy_cache_key $scheme$host$uri;
proxy_ignore_headers Expires;
proxy_ignore_headers Cache-Control;

Finalny plik example.com.conf można znaleźć na Githubie.

Mam cache, ale jak go teraz wyczyścić?

Aby wyczyścić cache dla domeny (example.com) należy wykonać komendę:

docker exec nginx_nginx_1 sh -c "rm -rf /var/cache/nginx/proxy_cache_dir/example.com/*"