IT-инфраструктура для бизнеса и творчества

Среда разработки PHP на базе Docker

Решение, которое позволит создать на локальном компьютере универсальную среду разработки на PHP за 30 — 40 минут.

Оглавление

Почему Docker?

  • Docker не является VM-системой, он не моделирует аппаратное обеспечение компьютера. Используя Docker вы получите минимальное потребление ресурсов. Docker-контейнеры взаимодействуют напрямую с ядром вашего компьютера (или ядром хоста), но при этом изолируют программу на уровне процесса.
  • Высокая скорость развертывания. Вы можете использовать готовые docker-образы, которые устанавливаются и запускаются за секунды.
  • Приложения, находящееся внутри docker-контейнеров, можно запустить на любом компьютере с установленным Docker, при этом окружение будет одинаковым.
  • Возможность простой сегрегации пользовательских данных и контейнеров-сервисов. Если вы сломаете или удалите docker-контейнер, то данные не потеряются, так как они не будут принадлежать контейнеру. Контейнер выполняет лишь функцию сервиса, и не хранит данные, которые нельзя потерять между запусками.
  • Вы можете очень быстро добавлять новые контейнеры, изменять их конфигурацию, запускать различные версии баз данных на одной машине.

Требования

  • Git.
  • Docker engine 19.x и выше.

Возможности и особенности среды разработки

  • Несколько версий PHP — 7.3 и 7.1 с набором наиболее востребованных расширений.
  • Возможность использовать для web-проектов разные версии PHP.
  • Готовый к работе монитор процессов Supervisor.
  • Предварительно сконфигурированный веб-сервер Nginx.
  • Базы данных: MySQL 5.7, MySQL 8, PostgreSQL (latest), MongoDB 4.2, Redis (latest).
  • Настройка основных параметров окружения через файл .env.
  • Возможность модификации сервисов через docker-compose.yml.
  • Последняя версия docker-compose.yml.
  • Все docker-контейнеры базируются на официальных образах.
  • Структурированный Dockerfile для создания образов PHP.
  • Каталоги большинства docker-контейнеров, в которых хранятся пользовательские данные и параметры конфигурации смонтированы на локальную машину.

В целом, среда разработки удовлетворяет требованию — при использовании Docker каждый контейнер должен содержать в себе только один сервис.

Репозиторий проекта

Структура проекта

Рассмотрим структуру проекта.

├── .env-example ├── .gitignore ├── .ssh ├── README.md ├── docker-compose.yml ├── mongo ├── mysql-5.7 ├── mysql-8 ├── nginx ├── php-ini ├── php-workers ├── php-workspace ├── postgres ├── projects └── redis

Примечание

В некоторых каталогах можно встретить пустой файл .gitkeep. Он нужен лишь для того, чтобы была возможность добавить каталог под наблюдение Git.

.gitkeep — является заполнением каталога, это фиктивный файл, на который не следует обращать внимание.

.env-example

Пример файла с основными настройками среды разработки.

# Временная зона WORKSPACE_TIMEZONE='Europe/Moscow' # XDEBUG DOCKER_PHP_ENABLE_XDEBUG='on' # Настройки Nginx # Порт, который следует использовать # для соединения с локального компьютера NGINX_PORT=80 # Настройки Redis # Порт, который следует использовать # для соединения с локального компьютера REDIS_PORT=6379 # Настройки Postgres POSTGRES_DB=test POSTGRES_USER=pg_user POSTGRES_PASSWORD=secret POSTGRES_PORT=54322 # Настройки общие для MySQL 8.x и MySQL 5.7.x MYSQL_ROOT_PASSWORD=secret MYSQL_DATABASE=test # Настройки MySQL 8.x # Порт, который следует использовать # для соединения с локального компьютера MYSQL_8_PORT=4308 # Настройки MySQL 5.7.x # Порт, который следует использовать # для соединения с локального компьютера MYSQL_5_7_PORT=4307 # Настройки MongoDB # Порт, который следует использовать # для соединения с локального компьютера MONGO_PORT=27017 # Настройки PHP 7.3 # Внешний порт, доступен с локального компьютера PHP_7_3_PORT=9003 # Настройки PHP 7.1 # Внешний порт, доступен с локального компьютера PHP_7_1_PORT=9001

.gitignore

Каталоги и файлы, в которых хранятся пользовательские данные, код ваших проектов и ssh-ключи внесены в. gitignore.

.ssh

Этот каталог предназначен для хранения ssh-ключей.

readme.md

Документация.

docker-compose.yml

Документ в формате YML, в котором определены правила создания и запуска многоконтейнерных приложений Docker. В этом файле описана структура среды разработки и некоторые параметры необходимые для корректной работы web-приложений.

mongo

Каталог базы данных MongoDB.

├── configdb │ └── mongo.conf ├── db └── dump

mongo.conf — Файл конфигурации MongoDB. В этот файл можно добавлять параметры, которые при перезапуске MongoDB будут применены.

db — эта папка предназначена для хранения пользовательских данных MongoDB.

dump — каталог для хранения дампов.

mysql-5.7

Каталог базы данных MySQL 5.7.

├── conf.d │ └── config-file.cnf ├── data ├── dump └── logs

config-file.cnf — файл конфигурации. В этот файл можно добавлять параметры, которые при перезапуске MySQL 5.7 будут применены.

data — эта папка предназначена для хранения пользовательских данных MySQL 5.7.

dump — каталог для хранения дампов.

logs — каталог для хранения логов.

mysql-8

Каталог базы данных MySQL 8.

├── conf.d │ └── config-file.cnf ├── data ├── dump └── logs

config-file.cnf — файл конфигурации. В этот файл можно добавлять параметры, которые при перезапуске MySQL 8 будут применены.

data — эта папка предназначена для хранения пользовательских данных MySQL 8.

dump — каталог для хранения дампов.

logs — каталог для хранения логов.

nginx

Эта папка предназначена для хранения файлов конфигурации Nginx и логов.

├── conf.d │ ├── default.conf │ └── vhost.conf └── logs

default.conf — файл конфигурации, который будет применён ко всем виртуальным хостам.

vhost.conf — здесь хранятся настройки виртуальных хостов web-проектов.

Рассмотрим vhost.conf подробнее:

server { listen 80; index index.php index.html; server_name project-1.localhost; error_log /var/log/nginx/project-1.error.log; access_log /var/log/nginx/project-1.access.log combined if=$loggable; root /var/www/project-1.ru; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass php-7.3:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_script_name; } } server { listen 80; index index.php index.html; server_name project-2.localhost; error_log /var/log/nginx/project-2.error.log; access_log /var/log/nginx/project-2.access.log combined if=$loggable; root /var/www/project-2.ru; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass php-7.1:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_script_name; } }

В файле конфигурации описаны настройки для двух web-проектов — project-1.localhost и project-2.localhost.

Здесь следует обратить внимание на то, как производится перенаправление запросов к нужному docker-контейнеру.

Например, для проекта project-1.localhost указано:

fastcgi_pass php-7.3:9000;

php-7.3 — название docker-контейнера, а 9000 — порт внутренней сети. Контейнеры между собой связаны через внутреннюю сеть, которая определена в файле docker-compose.yml.

php-ini

В этом каталоге находятся файлы конфигурации PHP.

├── 7.1 │ └── php.ini └── 7.3 └── php.ini

Для каждой версии PHP — свой файл конфигурации.

php-workers

Место для хранения файлов конфигурации Supervisor.

├── 7.1 │ └── supervisor.d │ └── 7.3 └── supervisor.d

Для каждой версии PHP — могут быть добавлены свои файлы с настройками.

php-workspace

Здесь хранится файл, в котором описаны действия, выполняемые при создании образов docker-контейнеров PHP.

└── Dockerfile

Dockerfile — это текстовый документ, содержащий все команды, которые следует выполнить для сборки образов PHP.

postgres

Каталог для системы управления базами данных PostgreSQL.

├── .gitkeep ├── data └── dump

data — эта папка предназначена для хранения пользовательских данных PostgreSQL.

dump — каталог для хранения дампов.

projects

Каталог предназначен для хранения web-проектов.

Вы можете создать в это каталоге папки, и поместить в них ваши web-проекты.

Например:

project-1.ru project-2.ru ...

Содержимое каталога projects доступно из контейнеров php-7.1 и php-7.3.

Если зайти в контейнер php-7.1 или php-7.3, то в каталоге /var/www будут доступны проекты, которые расположены в projects на локальной машине.

redis

Каталог key-value хранилища Redis.

├── conf └── data

conf — папка для хранения специфических параметров конфигурации.

data — если настройки конфигурации предполагают сохранения данных на диске, то Redis будет использовать именно этот каталог.

Программы в docker-контейнерах PHP

Полный перечень приложений, которые установлены в контейнерах php-7.x можно посмотреть в php-workspace/Dockerfile.

Здесь перечислим лишь некоторые, наиболее важные:

  • bash
  • htop
  • curl
  • Git
  • Сomposer
  • make
  • wget
  • NodeJS
  • Supervisor
  • npm

Начало работы

1. Выполните клонирование данного репозитория в любое место на вашем компьютере.

git clone https://github.com/drandin/docker-php-workspa

Перейдите в директорию, в которую вы клонировали репозиторий. Все дальнейшие команды следует выполнять именно в этой директории.

2. Скопируйте файл .env-example в .env

cp .env-example .env

Если это необходимо, то внесите изменения в файл .env. Измените настройки среды разработки в соответствии с вашими требованиями.

3. Выполните клонирование web-проектов в каталог projects.

Для примера, далее мы будем исходить из предположения, что у вас есть 2 проекта:

project-1.ru project-2.ru

project-1.ru — будет работать на версии PHP 7.3, а project-2.ru — на PHP 7.1.

4. Отредактируйте настройки виртуальных хостов Nginx.

Файл конфигурации виртуальных хостов находится в каталоге ./nginx/conf.d/.

5. Настройте хосты (доменные имена) web-проектов на локальной машине.

Необходимо добавить названия хостов web-проектов в файл hosts на вашем компьютере.

В файле hosts следует описать связь доменных имён ваших web-проектов в среде разработки на локальном компьютере и IP docker-контейнера Nginx.

На Mac и Linux этот файл расположен в /etc/hosts. На Windows он находится в C:\Windows\System32\drivers\etc\hosts.

Строки, которые вы добавляете в этот файл, будут выглядеть так:

127.0.0.1 project-1.localhost 127.0.0.1 project-2.localhost

В данном случае, мы исходим из того, что Nginx, запущенный в docker-контейнере, доступен по адресу 127.0.0.1 и web-сервер слушает порт 80.

Не рекомендуем использовать имя хоста с .dev на конце в среде разработки. Лучшей практикой является применение других названий — .localhost или .test.

6. [опционально, если это необходимо] Настройте маршрутизацию внутри контейнеров web-проектов.

Web-проекты должны иметь возможность отправлять http-запросы друг другу и использовать для этого название хостов.

Из одного запущенного docker-контейнера php-7.1 web-приложение № X должно иметь возможность отправить запрос к другому web-приложению № Y, которое работает внутри docker-контейнера php-7.3. При этом адресом запроса может быть название хоста, которое указано в файле /etc/hosts локального компьютера.

Чтобы это стало возможным нужно внутри контейнеров так же внести соответствующие записи в файл /etc/hosts.

Самый простой способ решить данную задачу — добавить секцию extra_hosts в описание сервисов php-7.1 и php-7.3 в docker-compose.yml.

Пример:

... php-7.1: ... extra_hosts: - 'project-1.localhost:IP_HOST_MACHINE' - 'project-2.localhost:IP_HOST_MACHINE' ...

IP_HOST_machine — IP адрес, по которому из docker-контейнера доступен ваш локальный компьютер.

Если вы разворачиваете среду разработки на Mac, то внутри docker-контейнера вам доступен хост docker.for.mac.localhost.

Узнать IP адрес вашего Mac можно при помощи команды, который нужно выполнить на локальной машине:

docker run -it alpine ping docker.for.mac.localhost

В результате вы получите, что-то подобное:

PING docker.for.mac.localhost (192.168.65.2): 56 data bytes 64 bytes from 192.168.65.2: seq=0 ttl=37 time=0.286 ms 64 bytes from 192.168.65.2: seq=1 ttl=37 time=0.504 ms 64 bytes from 192.168.65.2: seq=2 ttl=37 time=0.801 ms

После того, как вам станет известен IP-адрес, укажите его в секции extra_hosts в описание сервисов php-7.1 и php-7.3 в docker-compose.yml.

... php-7.1: ... extra_hosts: - 'project-1.localhost:192.168.65.2' - 'project-2.localhost:192.168.65.2' .

8. Настройте параметры соединения с системами хранения данных.

Хосты и порты сервисов

Для того, чтобы настроить соединения с базами данных из docker-контейнеров php-7.1 и php-7.3 следует использовать следующие названия хостов и порты:

Именно эти параметры следует использовать для конфигурации web-проектов.

Для соединения с базами данных с локальной машины:

  • Хост для всех баз данных — 127.0.0.1.
  • Порты — значения указанные в .env.

7. Создайте контейнеры и запустите их.

Выполните команду:

docker-compose build && docker-compose up -d

Создание контейнеров займёт некоторое время. Обычно от 10 до 30 минут. Дождитесь окончания процесса. Ваш компьютер не должен потерять доступ в интернет.

Если вы всё сделали правильно, то контейнеры будут созданы и запущены.

Откройте Docker Dashboard и убедитесь в этом:

Docker Dashboard на Mac.

8. Создайте SSH-ключи.

Для работы web-проектов могут потребоваться SSH-ключи, например для того, чтобы из контейнера при помощи Composer можно было установить пакет из приватного репозитория.

Создать SSH-ключи можно при помощи следующей команды:

ssh-keygen -f ./.ssh/id_rsa -t rsa -b 2048 -C "your-name@example.com"

Вместо your-name@example.com укажите свой Email.

В папку .ssh/ будут сохранены 2 файла — публичный и приватный ключ.

9. Проверьте созданные docker-контейнеры.

Выполните команду:

docker ps

Если создание контейнеров прошло успешно, то вы увидите примерно такой результат:

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8d348959c475 docker-php-workspace_php-7.1 "docker-php-entrypoi…" 6 minuts ago Up 54 seconds 0.0.0.0:9001->9000/tcp php-7.1 a93399727ff6 docker-php-workspace_php-7.3 "docker-php-entrypoi…" 6 minuts ago Up 53 seconds 0.0.0.0:9003->9000/tcp php-7.3 5cd80ac95388 nginx:stable-alpine "/docker-entrypoint.…" 6 minuts ago Up 51 seconds 0.0.0.0:80->80/tcp nginx 70182bc9e44c mysql:5.7 "docker-entrypoint.s…" 6 minuts ago Up 54 seconds 33060/tcp, 0.0.0.0:4307->3306/tcp mysql-5.7 46f2766ec0b9 mysql:8.0.21 "docker-entrypoint.s…" 6 minuts ago Up 53 seconds 33060/tcp, 0.0.0.0:4308->3306/tcp mysql-8 a59e7f4b3c61 mongo:4.2 "docker-entrypoint.s…" 6 minuts ago Up 54 seconds 0.0.0.0:27017->27017/tcp mongo eae8d62ac66e postgres:alpine "docker-entrypoint.s…" 6 minuts ago Up 53 seconds 0.0.0.0:54322->5432/tcp postgres bba24e86778a redis:latest "docker-entrypoint.s…" 6 minuts ago Up 54 seconds 0.0.0.0:6379->6379/tcp redis

10. Установка зависимостей для web-приложений.

Если для работы web-приложений необходимо установить зависимости, например через менеджер пакетов Composer или NPM, то сейчас самое время сделать это.

В контейнерах php-7.1 и php-7.3 уже установлен и Composer и NPM.

Войдите в контейнер php-7.1:

docker exec -it php-7.1 bash

или

docker exec -it php-7.3 bash

Перейдите в рабочий каталог необходимого web-проекта и выполните требуемые действия.

Например, установите зависимости через Composer при помощи команды:

composer install

Вопросы и ответы

Несколько важных вопросов и ответов.

Как зайти в работающий docker-контейнер?

Выполните команду:

docker exec -it container_name bash

container_name — имя контейнера.

Как останавливать и удалить контейнеры и другие ресурсы среды разработки, которые были созданы?

docker-compose down

Как получить список всех контейнеров?

docker ps -a

Как получить подробную информацию о docker-контейнере?

docker inspect container_name

container_name — имя контейнера.

Как получить полный список расширений PHP, которые установлены в контейнере php-7.3?

Если контейнер php-7.3 запущен, то выполните команду:

docker exec -it php-7.3 php -m

Как удалить все контейнеры?

Удаление всех контейнеров:

docker rm -v $(docker ps -aq) # Все без исключения контейнеры будут удалены

Удаление всех активных контейнеров:

docker rm -v $(docker ps -q) # Все активные контейнеры будут удалены

Удаление всех неактивных контейнеров:

docker rm -v $(docker ps -aq -f status=exited) # Все неактивные контейнеры будут удалены

Развёртывание дампов MySQL, PostgreSQL и MongoDB

Если для работы web-проектов требуются перенести данные в хранилища, то следуйте описанным ниже инструкциям.

Как развернуть дамп PostgreSQL?

Выполните следующую команду на локальной машине:

docker exec -i postgres psql --username user_name database_name < /path/to/dump/pgsql-backup.sql

или зайдите в контейнер postgres и выполните:

psql --username user_name database_name < /path/to/dump/pgsql-backup.sql

user_name — имя пользователя. Значение postgres_USER (см. файл .env).

database_name — название базы данных. Значение postgres_DB (см. файл .env).

Как развернуть дамп MySQL?

Существует два варианта.

Вариант 1

Если требуется создать дополнительных пользователей, то следует это сделать перед началом процедуры загрузки дампа.

В файле mysql/conf.d/config-file.cnf отключите лог медленных запросов slow_query_log=0 или установите большое значение long_query_time, например 1000.

Если дамп сжат утилитой gzip, сначала следует распаковать архив:

gunzip databases-dump.sql.gz

Затем можно развернуть дамп, выполнив на локальном компьютере команду:

docker exec -i mysql mysql --user=root --password=secret --force < databases-dump.sql

Указывать пароль в командной строке — плохая практика, не делайте так в производственной среде.

MySQL выдаст справедливое предупреждение:

mysql: [Warning] Using a password on the command line interface can be insecure

Ключ --force говорит MySQL, что ошибки следует проигнорировать и продолжить развёртывание дампа. Этот ключ иногда может пригодится, но лучше его без необходимости не применять.

Вариант 2

Воспользоваться утилитой Percona XtraBackup.

Percona XtraBackup — это утилита для горячего резервного копирования баз данных MySQL.

О том, как работать с XtraBackup можно узнать по ссылке: https://vc.ru/dev/158815-sozdanie-rezervnoy-kopii-mysql-pri-pomoshchi-utility-xtrabackup.

Как развернуть дамп MongoDB?

  • Скопируйте фалы дампа в каталог mongo/dump.
  • Войдите в контейнер mongo:
docker exec -it mongo sh

Выполните следующую команду, чтобы развернуть дамп базы database_name:

mongorestore -d database_name /dump/databases/database_nam

Git-репозиторий проекта

{ "author_name": "Игорь Драндин", "author_type": "self", "tags": ["selectel_\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f","postgresql","php","mysql","docker","dev"], "comments": 4, "likes": 8, "favorites": 33, "is_advertisement": false, "subsite_label": "dev", "id": 158897, "is_wide": true, "is_ugc": true, "date": "Wed, 16 Sep 2020 17:07:22 +0300", "is_special": false }
(function () { let cdnUrl = `https://specialsf378ef5-a.akamaihd.net/SelectelBranding/images/` let previousArticleNumber = null let currentArticleNumber = 0 let platform = 'Desktop' let articles = [ // { // name: 'camera', // url: `${cdnUrl}CameraCat`, // text: 'умную камеру для\u00A0наблюдения за\u00A0котиками', // link: '1', // }, { name: 'chill', url: `${cdnUrl}ChillCat`, text: 'трекер, который подскажет, когда пора отдохнуть', link: 'https://vc.ru/promo/288561-eye-tracker', }, // { // name: 'cloud', // url: `${cdnUrl}CloudCat`, // text: 'котика: даёшь ему «пять», а\u00A0он делает бэкап в облако', // link: '3', // } ] let buttonCycle = document.querySelector('.button--cycle') let textField = document.querySelector('.selectel-footer-subtitle') let imageAgent = document.querySelector('.image--agent') let banner = document.querySelector('.selectel-footer') buttonCycle.addEventListener('click', cycleClick) let media = window.matchMedia("(max-width: 570px)") media.addEventListener('change', matchMedia) function matchMedia() { if (media.matches) { platform = 'Mobile' } else { platform = 'Desktop' } update() } matchMedia() function cycleClick(event) { if (event) { event.preventDefault() event.stopPropagation() } window.open('https://vc.ru/tag/selectelDIY', '_blank') //cycle(event) } function cycle(event) { // incrementArticleNumber() textField.innerHTML = generatedText() imageAgent.src = articles[currentArticleNumber].url + platform + '.svg?5' imageAgent.setAttribute("class", "") imageAgent.classList.add('image--agent', articles[currentArticleNumber].name) banner.href = articles[currentArticleNumber].link } function update() { banner.href = articles[currentArticleNumber].link imageAgent.src = articles[currentArticleNumber].url + platform + '.svg?5' textField.innerHTML = generatedText() } function incrementArticleNumber() { previousArticleNumber = currentArticleNumber if (currentArticleNumber >= articles.length - 1) { currentArticleNumber = 0 } else { currentArticleNumber++ } } function generatedText() { let defaultText if (platform === 'Desktop') { defaultText = `Мы тут собрали %text%. Хотите почитать?` } else { defaultText = `Мы тут собрали %text%.` } return defaultText.replace('%text%', articles[currentArticleNumber].text) } function getRandom(min, max) { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min + 1)) + min } (function create() { currentArticleNumber = getRandom(0, articles.length - 1) cycle() let page = document.querySelector('.page--entry') if (page) { function insertAfter() { let parents = page.querySelectorAll('[data-id="7"]') let referenceNode = parents[0] referenceNode.parentNode.insertBefore(banner, referenceNode.nextSibling); loaded() } setTimeout(() => insertAfter(), 0) } }()) function loaded() { banner.classList.add('loaded') } loadImages([ `${cdnUrl}CameraCatDesktop.svg`, `${cdnUrl}ChillCatDesktop.svg`, `${cdnUrl}CloudCatDesktop.svg`, `${cdnUrl}CameraCatMobile.svg`, `${cdnUrl}ChillCatMobile.svg`, `${cdnUrl}CloudCatMobile.svg`, ]) function loadImages(urls) { return Promise.all(urls.map(function (url) { return new Promise(function (resolve) { var img = document.createElement('img'); img.onload = resolve; img.onerror = resolve; img.src = url; }); })); } }())
0
4 комментария
Популярные
По порядку
3

Когда ошибся сайтом %)

Ответить
0

Чет контейнер с postgres выдает в логах 
initdb: error: superuser name "pg_user" is disallowed; role names cannot begin with "pg_"
гуглил ничего не нашел полезного :(

Ответить
0

Ну там же в ошибке и написано, что нельзя использовать пользователя с префиксом pg_ нужно в .env поменять имя POSTGRES_USER=postgres
там может быть и другие ошибки с контейнером postgres:
1 - \docker-php-workspace\postgres\data нужно удалить .gitkeep
2 - environment добавить PGDATA: /tmp

Ответить
0

что-то postgres не хочет запускаться((

Ответить
Читать все 4 комментария
Дайджест новостей Сбера: AR-экскурсия, распознавание животных и премия для учёных

Прогулки по Политехническому музею с роботом Сепулькой, чудеса компьютерного зрения, научная премия — начало сентября в Сбере выдалось насыщенным. Делимся с вами самыми интересными новостями компании во втором выпуске нашего дайджеста.

«Вам звонок из Циан»: зачем компания скупает номера телефонов и как call-tracking помогает клиентам сервиса

Подменные номера телефонов защищают от нежелательных звонков и упрощают аналитику продаж

Почему не стоит заказывать товары 18+ на Озоне, если вы не готовы их потом нести в пункт самовывоза

Хочу поделиться неудачной историей заказа товара 18+ на Озоне. И как Озон не хочет помогать решить проблему. Текст могут читать только совершеннолетние пользователи сайта.

Конференция GoGlobal! соберет ведущих маркетологов

29 сентября 2021 года впервые состоится GoGlobal! — однодневная онлайн-конференция для маркетологов, заинтересованных в ускорении глобального присутствия своих кампаний.

Как работать удалённо по московскому времени, если живёшь в Сибири

Команда ИТ-компании Southbridge — о преодолении трудностей часовых поясов: графике работы, планировании и отдыхе.

Выборы, выборы, кандидаты

Нет, не в рифму известной песни. "Всего лишь" педофилы... Делюсь своим негодованием после посещения избирательного участка.

«Сбербанк» через час после личного визита позвал 84-летнюю бабушку еще раз в прийти офис

Казалось бы удобно - оформил получение пенсии на карту и нет проблем. Но это не про историю со Сбербанком. Здесь через час после получения карты лично в отделении прилетело смс о блокировке онлайн сервисов банка.

«Яндекс» оставляет только положительные отзывы о своих сервисах

После ситуации с не доставленным заказом из Яндекс Лавки, я оставил отзыв на сервисе Карт. Его приняли и разместили, но доступен он только для аккаунта, с которого я его оставлял)))

Я задолбался пропускать интересные статьи на vc.ru и создал Capitan

Это самый удобный рабочий стол для браузера

null