04 грудня, Субота, 2021
A- A A+

Пошук

magento 2.4

Налаштування робочого середовища для Magento 2.4

В нас ОС Debian 10.

ПІсля входу на вашу ВМ першим ділом закриваєм доступ по SSH root користувачу, Створюєм нового з правами sudo

useradd -m -U -s /bin/bash -G sudo hello
passwd hello

Змінюєм налаштування SSH nano /etc/ssh/sshd_config

Port 22123
PermitRootLogin no
ClientAliveInterval 300 ClientAliveCountMax 1
AllowUsers holu hello
MaxAuthTries 2

Перевіряєм синтаксис сервісу: sshd -t  та перезавантажуєм systemctl restart sshd

Наспним кроком це встановлення Fail2Ban

apt install fail2ban
systemctl enable fail2ban

Налаштування Fail2Ban

cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

nano /etc/fail2ban/jail.local

enabled = true
port = SELECTED_SSH_PORT

systemctl restart fail2ban

Установка PHP 7.4 / 7.3 / 7.2 / 7.1 на Debian 10

php7 4

Оновлюєм репозиторії

sudo apt update

Встановіть наведені нижче пакети.

sudo apt install -y curl wget gnupg2 ca-certificates lsb-release apt-transport-https

Імпортуйте загальнодоступний за допомогою наведених нижче команд.

wget https://packages.sury.org/php/apt.gpg
sudo apt-key add apt.gpg

Добавляєм в наші списки репозиторіїв

echo "deb https://packages.sury.org/php/ $ (lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/php7.list

Оновлюєм

sudo apt update

Встановіть PHP v7.4 наступною командою

sudo apt install -y php7.4 php7.4-cli php7.4-common php7.4-fpm
apt install -y php7.4-curl php7.4-bcmath php7.4-dom php7.4-gd php7.4-imagick php7.4-mbstring php7.4-xml php7.4-zip php7.4-xsl php7.4-mysqli php7.4-soap

Якщо вам потрібно встановити іншу версію то просто зміюєте версію php7.3, php7.2, php7.1. Після встановлення PHP перевірте версію.

php -v
PHP 7.4.15 (cli) (built: Feb 23 2021 16:44:43) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.15, Copyright (c), by Zend Technologies

Встановлення Varnish 6.4

varnish cache6.4

aptitude update
aptitude install debian-archive-keyring
aptitude install curl gnupg apt-transport-https
curl -L https://packagecloud.io/varnishcache/varnish64/gpgkey | sudo apt-key add -

Додаєм в список репозиторія nano /etc/apt/sources.list

# varnish
deb https://packagecloud.io/varnishcache/varnish64/debian/ buster main
deb-src https://packagecloud.io/varnishcache/varnish64/debian/ buster main

Обновлюєм та встановлюєм

aptitude update
aptitude  install varnish=6.4.0-1~buster

varnishd -V

Змінюєм файл конфігурації

nano /lib/systemd/system/varnish.service
[Unit]
Description=Varnish HTTP accelerator
Documentation=https://www.varnish-cache.org/docs/6.4/ man:varnishd [Service]
Type=simple
LimitNOFILE=131072
LimitMEMLOCK=82000
ExecStart=/usr/sbin/varnishd -j unix,user=vcache -F -a :80 -T localhost:6082 -f /etc/varnish/default.vcl -S /etc/varnish/secret -s malloc,256m
ExecReload=/usr/share/varnish/varnishreload
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
PrivateDevices=true [Install]
WantedBy=multi-user.target

systemctl daemon-reload

service varnish restart

Тепер змінюєм конфіг файл для Magento 2 nano /etc/varnish/default.vcl

# VCL version 5.0 is not supported so it should be 4.0 even though actually used Varnish version is 6
vcl 4.0;

import std;
# The minimal Varnish version is 6.4
# For SSL offloading, pass the following header in your proxy server or load balancer: 'X-Forwarded-Proto: https'

backend default {
    .host = "localhost";
    .port = "8080";
    .first_byte_timeout = 600s;
    .probe = {
        .url = "/health_check.php";
        .timeout = 2s;
        .interval = 5s;
        .window = 10;
        .threshold = 5;
   }

}

acl purge {
    "localhost";
}

sub vcl_recv {
    if (req.restarts > 0) {
        set req.hash_always_miss = true;
    }
    if (req.method == "PURGE") {
        if (client.ip !~ purge) {
            return (synth(405, "Method not allowed"));
        }
        # To use the X-Pool header for purging varnish during automated deployments, make sure the X-Pool header
        # has been added to the response in your backend server config. This is used, for example, by the
        # capistrano-magento2 gem for purging old content from varnish during it's deploy routine.
        if (!req.http.X-Magento-Tags-Pattern && !req.http.X-Pool) {
            return (synth(400, "X-Magento-Tags-Pattern or X-Pool header required"));
        }
        if (req.http.X-Magento-Tags-Pattern) {
          ban("obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern);
        }
        if (req.http.X-Pool) {
          ban("obj.http.X-Pool ~ " + req.http.X-Pool);
        }
        return (synth(200, "Purged"));
    }

    if (req.method != "GET" &&
        req.method != "HEAD" &&
        req.method != "PUT" &&
        req.method != "POST" &&
        req.method != "TRACE" &&
        req.method != "OPTIONS" &&
        req.method != "DELETE") {
          /* Non-RFC2616 or CONNECT which is weird. */
          return (pipe);
    }

    # We only deal with GET and HEAD by default
    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    # Bypass shopping cart, checkout and search requests
    if (req.url ~ "/checkout" || req.url ~ "/catalogsearch") {
        return (pass);
    }

    # Bypass health check requests
    if (req.url ~ "/health_check.php") {
        return (pass);
    }

    if (req.url ~ "/PrimeLab/handler.php") {
	return (pass);
    }

    # Set initial grace period usage status
    set req.http.grace = "none";

    # normalize url in case of leading HTTP scheme and domain
    set req.url = regsub(req.url, "^http[s]?://", "");

    # collect all cookies
    std.collect(req.http.Cookie);

    # Compression filter. See https://www.varnish-cache.org/trac/wiki/FAQ/Compression
    if (req.http.Accept-Encoding) {
        if (req.url ~ "\.(jpg|jpeg|png|gif|gz|tgz|bz2|tbz|mp3|ogg|swf|flv)$") {
            # No point in compressing these
            unset req.http.Accept-Encoding;
        } elsif (req.http.Accept-Encoding ~ "gzip") {
            set req.http.Accept-Encoding = "gzip";
        } elsif (req.http.Accept-Encoding ~ "deflate" && req.http.user-agent !~ "MSIE") {
            set req.http.Accept-Encoding = "deflate";
        } else {
            # unknown algorithm
            unset req.http.Accept-Encoding;
        }
    }

    # Remove all marketing get parameters to minimize the cache objects
    if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") {
        set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", "");
        set req.url = regsub(req.url, "[?|&]+$", "");
    }

    # Static files caching
    if (req.url ~ "^/(pub/)?(media|static)/") {
        # Static files should not be cached by default
        return (pass);

        # But if you use a few locales and don't use CDN you can enable caching static files by commenting previous line (#return (pass);) and uncommenting next 3 lines
        #unset req.http.Https;
        #unset req.http.X-Forwarded-Proto;
        #unset req.http.Cookie;
    }

    # Authenticated GraphQL requests should not be cached by default
    if (req.url ~ "/graphql" && req.http.Authorization ~ "^Bearer") {
        return (pass);
    }

    return (hash);
}

sub vcl_hash {
    if (req.http.cookie ~ "X-Magento-Vary=") {
        hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1"));
    }

    # For multi site configurations to not cache each other's content
    if (req.http.host) {
        hash_data(req.http.host);
    } else {
        hash_data(server.ip);
    }

    # To make sure http users don't see ssl warning
    if (req.http.X-Forwarded-Proto) {
        hash_data(req.http.X-Forwarded-Proto);
    }
    

    if (req.url ~ "/graphql") {
        call process_graphql_headers;
    }
}

sub process_graphql_headers {
    if (req.http.Store) {
        hash_data(req.http.Store);
    }
    if (req.http.Content-Currency) {
        hash_data(req.http.Content-Currency);
    }
}

sub vcl_backend_response {

    set beresp.grace = 3d;

    if (beresp.http.content-type ~ "text") {
        set beresp.do_esi = true;
    }

    if (bereq.url ~ "\.js$" || beresp.http.content-type ~ "text") {
        set beresp.do_gzip = true;
    }

    if (beresp.http.X-Magento-Debug) {
        set beresp.http.X-Magento-Cache-Control = beresp.http.Cache-Control;
    }

    # cache only successfully responses and 404s
    if (beresp.status != 200 && beresp.status != 404) {
        set beresp.ttl = 0s;
        set beresp.uncacheable = true;
        return (deliver);
    } elsif (beresp.http.Cache-Control ~ "private") {
        set beresp.uncacheable = true;
        set beresp.ttl = 86400s;
        return (deliver);
    }

    # validate if we need to cache it and prevent from setting cookie
    if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) {
        unset beresp.http.set-cookie;
    }

   # If page is not cacheable then bypass varnish for 2 minutes as Hit-For-Pass
   if (beresp.ttl <= 0s ||
       beresp.http.Surrogate-control ~ "no-store" ||
       (!beresp.http.Surrogate-Control &&
       beresp.http.Cache-Control ~ "no-cache|no-store") ||
       beresp.http.Vary == "*") {
        # Mark as Hit-For-Pass for the next 2 minutes
        set beresp.ttl = 120s;
        set beresp.uncacheable = true;
    }

    return (deliver);
}

sub vcl_deliver {
    if (resp.http.X-Magento-Debug) {
        if (resp.http.x-varnish ~ " ") {
            set resp.http.X-Magento-Cache-Debug = "HIT";
            set resp.http.Grace = req.http.grace;
        } else {
            set resp.http.X-Magento-Cache-Debug = "MISS";
        }
    } else {
        unset resp.http.Age;
    }

    # Not letting browser to cache non-static files.
    if (resp.http.Cache-Control !~ "private" && req.url !~ "^/(pub/)?(media|static)/") {
        set resp.http.Pragma = "no-cache";
        set resp.http.Expires = "-1";
        set resp.http.Cache-Control = "no-store, no-cache, must-revalidate, max-age=0";
    }

    unset resp.http.X-Magento-Debug;
    unset resp.http.X-Magento-Tags;
    unset resp.http.X-Powered-By;
    unset resp.http.Server;
    unset resp.http.X-Varnish;
    unset resp.http.Via;
    unset resp.http.Link;
}

sub vcl_hit {
    if (obj.ttl >= 0s) {
        # Hit within TTL period
        return (deliver);
    }
    if (std.healthy(req.backend_hint)) {
        if (obj.ttl + 300s > 0s) {
            # Hit after TTL expiration, but within grace period
            set req.http.grace = "normal (healthy server)";
            return (deliver);
        } else {
            # Hit after TTL and grace expiration
            return (restart);
        }
    } else {
        # server is not healthy, retrieve from cache
        set req.http.grace = "unlimited (unhealthy server)";
        return (deliver);
    }
}

NGINX Встановлення

apt -y install nginx
systemctl status nginx
systemctl enable nginx
apt install software-properties-common
add-apt-repository universe
add-apt-repository ppa:certbot/certbot
apt update
apt -y install python-certbot-nginx
certbot --nginx -d domain.com -d www.domain.com
certbot renew --dry-run
certbot renew --dry-run

Також змінюєм налаштування NGINX для прослуховування 8080 порта

nano /etc/nginx/sites-available/my.conf

upstream fastcgi_backend {
        server unix:/var/run/php/php7.4-fpm.sock;
}

server {
	listen 8080 default_server;
	listen [::]:8080 default_server;

error_log  /var/log/nginx/example_error_log  warn;
error_log  /var/log/nginx/example1.error_log  crit;

set $MAGE_ROOT /var/www/html;

root $MAGE_ROOT/pub;

index index.php;
autoindex off;
charset UTF-8;
error_page 404 403 = /errors/404.php;
#add_header "X-UA-Compatible" "IE=Edge";


# Deny access to sensitive files
location /.user.ini {
    deny all;
}

location ~* ^/PrimeLab($|/) {
    root $MAGE_ROOT;
    location ~ ^/PrimeLab/handler.php {
        fastcgi_pass   fastcgi_backend;

        fastcgi_param  PHP_FLAG  "session.auto_start=off \n suhosin.session.cryptua=off";
        fastcgi_param  PHP_VALUE "memory_limit=756M \n max_execution_time=600";
        fastcgi_read_timeout 600s;
        fastcgi_connect_timeout 600s;

        fastcgi_index  handler.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}

# PHP entry point for setup application
location ~* ^/setup($|/) {
    root $MAGE_ROOT;
    location ~ ^/setup/index.php {
    
        fastcgi_pass   fastcgi_backend;

        fastcgi_param  PHP_FLAG  "session.auto_start=off \n suhosin.session.cryptua=off";
        fastcgi_param  PHP_VALUE "memory_limit=756M \n max_execution_time=600";
        fastcgi_read_timeout 600s;
        fastcgi_connect_timeout 600s;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }

    location ~ ^/setup/(?!pub/). {
        deny all;
    }

    location ~ ^/setup/pub/ {
        add_header X-Frame-Options "SAMEORIGIN";
    }
}

# PHP entry point for update application
location ~* ^/update($|/) {
    root $MAGE_ROOT;

    location ~ ^/update/index.php {
        fastcgi_split_path_info ^(/update/index.php)(/.+)$;
        fastcgi_pass   fastcgi_backend;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        fastcgi_param  PATH_INFO        $fastcgi_path_info;
        include        fastcgi_params;
    }

    # Deny everything but index.php
    location ~ ^/update/(?!pub/). {
        deny all;
    }

    location ~ ^/update/pub/ {
        add_header X-Frame-Options "SAMEORIGIN";
    }
}

location / {
    try_files $uri $uri/ /index.php$is_args$args;



}

location /pub/ {
   # location ~ ^/pub/media/(downloadable|customer|import|theme_customization/.*\.xml) {
   #     deny all;
   # }
    alias $MAGE_ROOT/pub/;
    add_header X-Frame-Options "SAMEORIGIN";
}

location /static/ {
    # Uncomment the following line in production mode
    # expires max;

    # Remove signature of the static files that is used to overcome the browser cache
    location ~ ^/static/version {
        rewrite ^/static/(version\d*/)?(.*)$ /static/$2 last;
    }

    location ~* \.(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2|json)$ {
        add_header Cache-Control "public";
        add_header X-Frame-Options "SAMEORIGIN";
        expires +1y;

        if (!-f $request_filename) {
            rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last;
        }
    }
    location ~* \.(zip|gz|gzip|bz2|csv|xml)$ {
        add_header Cache-Control "no-store";
        add_header X-Frame-Options "SAMEORIGIN";
        expires    off;

        if (!-f $request_filename) {
           rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last;
        }
    }
    if (!-f $request_filename) {
        rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last;
    }
    add_header X-Frame-Options "SAMEORIGIN";
}

location /media/ {
    try_files $uri $uri/ /get.php$is_args$args;

    location ~ ^/media/theme_customization/.*\.xml {
        deny all;
    }

    location ~* \.(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2)$ {
        add_header Cache-Control "public";
        add_header X-Frame-Options "SAMEORIGIN";
        expires +1y;
        try_files $uri $uri/ /get.php$is_args$args;
    }
    location ~* \.(zip|gz|gzip|bz2|csv|xml)$ {
        add_header Cache-Control "no-store";
        add_header X-Frame-Options "SAMEORIGIN";
        expires    off;
        try_files $uri $uri/ /get.php$is_args$args;
    }
    add_header X-Frame-Options "SAMEORIGIN";
}

location /media/customer/ {
    deny all;
}

location /media/downloadable/ {
    deny all;
}

location /media/import/ {
    deny all;
}
location /errors/ {
    location ~* \.xml$ {
        deny all;
    }
}

# PHP entry point for main application
location ~ ^/(index|get|static|errors/report|errors/404|errors/503|health_check)\.php$ {
    try_files $uri =404;
    fastcgi_pass   fastcgi_backend;
  #  fastcgi_buffers 1024 4k;
  fastcgi_buffers 16 16k;
  fastcgi_buffer_size 32k;
  proxy_buffer_size   128k;
  proxy_buffers   4 256k;
  proxy_busy_buffers_size   256k;


client_max_body_size    100m;
client_body_buffer_size 128k;
proxy_connect_timeout   90;
proxy_send_timeout      90;
proxy_read_timeout      90;
proxy_buffering         on;

    fastcgi_param  PHP_FLAG  "session.auto_start=off \n suhosin.session.cryptua=off";
    fastcgi_param  PHP_VALUE "memory_limit=756M \n max_execution_time=18000";
    fastcgi_read_timeout 600s;
    fastcgi_connect_timeout 600s;

    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    include        fastcgi_params;
}

gzip on;
gzip_disable "msie6";

gzip_comp_level 6;
gzip_min_length 1100;
gzip_buffers 16 8k;
gzip_proxied any;
gzip_types
    text/plain
    text/css
    text/js
    text/xml
    text/javascript
    application/javascript
    application/x-javascript
    application/json
    application/xml
    application/xml+rss
    image/svg+xml;
gzip_vary on;

# Banned locations (only reached if the earlier PHP entry point regexes don't match)
location ~* (\.php$|\.phtml$|\.htaccess$|\.git) {
    deny all;
}


}

server {
   server_name examle.com.ua;
   listen 443 ssl http2;

error_log  /var/log/nginx/example_error_log  warn;
error_log  /var/log/nginx/example1.error_log  crit;

   ssl on;
#   ssl_certificate /etc/letsencrypt/live/mysite/fullchain.pem;
#   ssl_certificate_key /etc/letsencrypt/live/mysite/privkey.pem;
    ssl_certificate /etc/letsencrypt/live/mysite/fullchain.pem; 
    ssl_certificate_key /etc/letsencrypt/live/mysite/privkey.pem;


    location / {
        proxy_pass http://127.0.0.1;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Ssl-Offloaded "1";
        #proxy_hide_header X-Varnish;
        #proxy_hide_header Via;

client_max_body_size    100m;
client_body_buffer_size 128k;
proxy_connect_timeout   90;
proxy_send_timeout      90;
proxy_read_timeout      90;
proxy_buffering         on;
proxy_buffer_size       1024k;
proxy_buffers           8 1024k;
proxy_busy_buffers_size 1024k;

        proxy_set_header X-Forwarded-Proto $scheme;

    }

}



Встановлення Elasticsearch 7

Добавляєм PGP ключі

wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -

Elasticsearch 7.x (Latest):

sudo apt -y install apt-transport-https
echo "deb https://artifacts.elastic.co/packages/oss-7.x/apt stable main" | sudo tee  /etc/apt/sources.list.d/elastic-7.x.list

Elasticsearch 6.x:

sudo apt -y install apt-transport-https
echo "deb https://artifacts.elastic.co/packages/oss-6.x/apt stable main" | sudo tee  /etc/apt/sources.list.d/elastic-6.x.list

Elasticsearch 5.x:

sudo apt -y install apt-transport-https
echo "deb https://artifacts.elastic.co/packages/oss-5.x/apt stable main" | sudo tee  /etc/apt/sources.list.d/elastic-5.x.list

sudo apt update

sudo apt -y install elasticsearch-oss

Змінити налаштування nano /etc/elasticsearch/elasticsearch.yml

cluster.name: my-application

запускаєм та оновлюєм сервіси

sudo systemctl enable elasticsearch.service && sudo systemctl restart elasticsearch.service

Як встановити найновіший MySQL на Debian 10

db1

На сервері клієнті встановлюємо клієнт apt install default-mysql-client

sudo apt update
sudo apt install gnupg

Після підтвердження встановлення apt встановить gnupgта його залежності.

cd /tmp
wget https://dev.mysql.com/get/mysql-apt-config_0.8.13-1_all.deb
sudo dpkg -i mysql-apt-config*
sudo apt update

Після додавання сховища та оновлення нашого кешу пакунків тепер ми можемо використовувати aptдля встановлення останнього серверного пакету MySQL:

sudo apt install mysql-server
sudo systemctl status mysql

Захист MySQL mysql_secure_installation

Як дозволити віддалений доступ до MySQL

Однією з найпоширеніших проблем, з якою стикаються користувачі при спробі налаштування віддаленої бази даних MySQL, є те, що їх екземпляр MySQL налаштований лише на прослуховування локальних з'єднань. Це налаштування MySQL за замовчуванням, але воно не буде працювати для віддаленого налаштування бази даних, оскільки MySQL повинен мати можливість прослуховувати зовнішню IP-адресу, де можна отримати доступ до сервера. Щоб увімкнути це, відкрийте mysqld.cnfфайл:

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
datadir         = /var/lib/mysql
log-error       = /var/log/mysql/error.log
#localhost
bind-address=127.0.0.1
#IP remote host
bind-address=10.0.0.3

Можна налаштувати доступ з усіх ІР bind-address=0.0.0.0

sudo systemctl restart mysql
mysql -u root -p

В залежності від версії мускуля

# Створюжм нову БД
CREATE DATABASE newdb character set utf8 collate utf8_bin;
# Створюєм нового користувача з вказанням пароля
CREATE USER 'bduser'@'10.0.0.2' IDENTIFIED BY 'hellomypass';
# або так
CREATE USER 'bduser'@'10.0.0.2' IDENTIFIED WITH mysql_native_password BY 'hellomypass';
# всі права на БД користувачу
GRANT ALL PRIVILEGES ON newdb.* TO 'bduser'@'10.0.0.2' WITH GRANT OPTION;

GRANT CREATE, ALTER, DROP, INSERT, UPDATE, DELETE, SELECT, REFERENCES, RELOAD on *.* TO 'bduser'@'10.0.0.2' WITH GRANT OPTION;
# обновити привілеї
FLUSH PRIVILEGES;
# якщо з віддаленого хоста не захоче приймати пасс тоді 
ALTER USER 'forsageu'@'10.0.0.2' IDENTIFIED WITH mysql_native_password BY 'hellomypass!';

Після чого з віддаленого сервера з IP 10.0.0.2 заходимо на віддалену БД *.0.3

mysql -u bduser -h 10.0.0.2 -p

Перевірити використовуючі порти netstat -lntp

Допоміжні статті magemastery.net devdocs.magento.com digitalocean computingforgeeks