Developers Club geek daily blog

2 years, 10 months ago
This article appeared with the purpose to generalize quite long attempts to collect a convenient environment for work on projects. Undoubtedly, there is a set of services ready to provide similar functionality, but their use is not always convenient also for the different reasons, can be unacceptable. If there was such situation, hope, the configuration presented in article will be useful.

Development environment: Redmine + Git + ownCloud

The scenario of use of this sheaf, it is possible to describe briefly as follows:

  • Files of the project are stored in Git repositories;
  • The repository contains settings, source codes and other files of the project which existence is convenient and admissible in a collective repository;
  • In a root cloud directory excluded in .gitignore in which through WebDAV ownCloud the folder, for other files is mounted is located;
  • Contents of Git of a repository are traced in Redmine project management system.

The plan of system deployment includes setup of the following services:

  1. OpenLDAP — the uniform account for all services;
  2. Redmine — start in Docker the container, creation and a binding of Git of a repository, LDAP authentication;
  3. NGINX — access to Git to a repository through HTTPS and LDAP authentication;
  4. ownCloud — LDAP authentication and assembling of the folder through davfs2.

Tough binding to a distribution kit and operating system, here not. However I have on the Linux server a Gentoo distribution kit, on the client — Kubuntu therefore some examples will have specific features.

For convenience of reproduction the part of examples contains the preset passwords. Therefore I strongly recommend to use instead of a tag [VNESHNIY_IP] the local IP not available from the outside, or at once to change passwords for reliable.


LDAP is a data access protocol, organized in the form of directories. Here its implementation — OpenLDAP will be considered. This service represents in fact the database optimized for storage of tree structures, such as directories. In this case it is important that LDAP well is suitable for data storage of the user and is the standard which is supported anyway by the majority of services of the users working with accounts.

Minimum necessary configuration:

# /etc/openldap/slapd.conf

include		/etc/openldap/schema/core.schema
include		/etc/openldap/schema/cosine.schema
include		/etc/openldap/schema/inetorgperson.schema
include		/etc/openldap/schema/nis.schema

# Необходимо разрешить чтение записи cn=Subschema для всех.
access to dn.base="" by * read
access to dn.base="cn=Subschema" by * read

# Разрешение на чтение для cn=manager,dc=example,dc=com.
# Нужно, чтобы не использовать rootdn при настройке сторонних сервисов.
access to dn.regex=".+,dc=example,dc=com$"
	by self write
	by dn.exact="cn=manager,dc=example,dc=com" read
	by anonymous auth

# Всем остальным разрешить только авторизацию и изменение своих записей.
access to *
	by self write
	by anonymous auth
	by * none

pidfile		/var/run/openldap/
argsfile	/var/run/openldap/slapd.args

# Бинарное поле задающие уровень отладочной информации в логах.
# Обычно хватает 1 или 2, в тяжелых случаях - 256 (максимум 2048).
loglevel 0

modulepath    /usr/lib64/openldap/openldap

# Сертификаты для поддержки LDAPS.
TLSCertificateFile	/etc/ssl/openldap/server.pem
TLSCertificateKeyFile	/etc/ssl/openldap/server.key

### Основная база данных

database	hdb
# Директория должна существовать и иметь права 700 и владельца ldap:ldap.
directory	/var/lib/openldap-data
# Суффикс запросов, предназначенных этой базе.
suffix		"dc=example,dc=com"
# DN (Distinguished Name) на которую не распространяются ограничения в правах.
# Физическое наличие в БД этой записи не обязательно.
rootdn		"cn=admin,dc=example,dc=com"
# Пароль к rootdn, создается с помощью slappasswd -s [пароль]
# Здесь для примера использован пароль: passwd
rootpw		{SSHA}70m8+2axDu++Adp6EOLPVpISPxbMVPFv

# Оверлей для добавление к записи пользователя атрибута memberOf с DN группы, в которой он упомянут.
overlay memberof
memberof-group-oc groupOfUniqueNames
memberof-member-ad uniqueMember
memberof-refint true

# Оверлей для поддержания ссылочной целостности.
# При удалении пользователя, записи о нем в группах, также удаляются.
overlay refint
refint_attributes uniqueMember
# Пустая группа не допустима. Очевидное решение, добавить администратора.
refint_nothing "cn=admin,dc=example,dc=com"

# Индексы для базы данных.
index   objectClass                             eq
index   uid,uidNumber,gidNumber,memberUid       eq

For descriptive reasons the configuration is written in the slapd.conf file here, however since version 2.4 OpenLDAP this file is declared by outdated. The new format is called OLC (on-line configuration) and is presented in the form of the config database. It is convenient that change of settings does not demand service reset any more.

For migration on OLC it is necessary to add to the end of the slapd.conf file:

### База данных с настройками

# Изменять настройки разрешено только суперпользователю (root) через сокет.
# Ex: ldapadd/ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f [config_update].ldif
database config
access to *
	by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
	by * none

And to execute commands:

mkdir -p /etc/openldap/slapd.d
# Если slapd ещё не запускался, ошибку об отсутствии файла базы данных можно игнорировать.
slaptest -f /etc/openldap/slapd.conf -F /etc/openldap/slapd.d
chown ldap:ldap -R /etc/openldap/slapd.d /var/lib/openldap-data /etc/openldap/slapd.conf
chmod 750 /etc/openldap/slapd.d &&chmod 640 /etc/openldap/slapd.conf

Settings will be written in the specified folder, still in text form, but you should not edit manually them already.

Further it is necessary to change parameters of start of the demon that he read settings from the database instead of the file. In Gentoo for this purpose it is necessary to change file/etc/conf.d/slapd:

# /etc/conf.d/slapd

# Читать настройки из файла.
#OPTS_CONF="-f /etc/openldap/slapd.conf"
# Читать настройки из базы данных.
OPTS_CONF="-F /etc/openldap/slapd.d"
# Сервис доступен извне по протоколу ldaps; локально - ldap либо через сокет (%2f - так нужно).
OPTS="${OPTS_CONF} -h 'ldaps://[ВНЕШНИЙ_IP]:636 ldap:// ldapi://%2fvar%2frun%2fopenldap%2fslapd.sock'"

Now it is possible to start service and to add data:

Backup.ldif file contents
dn: dc=example, dc=com
objectClass: top
objectClass: dcObject
objectClass: organization
dc: example

dn: ou=people, dc=example, dc=com
objectClass: organizationalUnit
ou: People

dn: ou=groups, dc=example, dc=com
objectClass: organizationalUnit
ou: Groups

dn: uid=example, ou=people, dc=example, dc=com
cn: Example User
givenName: User
sn: Example
uid: example
uidNumber: 1001
gidNumber: 1000
homeDirectory: / var/www/
objectClass: top
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
loginShell: / sbin/nologin
userPassword: {SSHA} 1zGVasPHdQ7LXhBJxzAOseflZiqlecKT

dn: cn=manager, dc=example, dc=com
cn: Manager
objectClass: top
objectClass: simpleSecurityObject
objectClass: organizationalRole
userPassword: KihawPs8IchHTS/Lc7aqKGd1rfkpEKyi {SSHA}

dn: cn=developers,ou=groups,dc=example,dc=com
objectClass: groupOfUniqueNames
cn: developers
description: Developers
uniqueMember: uid=example, ou=people, dc=example, dc=com

ldapadd -x -D 'cn=admin,dc=example,dc=com' -w 'passwd' -f backup.ldif

To use the passwords it is necessary to replace value of the userPassword fields. New it is possible to receive command:

slappasswd -s [пароль]

At recovery from a backup copy it is important to be convinced that records for adding of groups are located after records for adding of users, and it is better at the very end of a script, otherwise overlay of memberof will not be able correctly to process them.

To check that OpenLDAP is correctly configured and works as it was conceived, it is possible command:

ldapsearch -D 'cn=manager,dc=example,dc=com' -w 'passwd' -b 'ou=people,dc=example,dc=com' '(uid=example)' memberOf

The answer has to be the following:

dn: uid=example,ou=people,dc=example,dc=com
memberOf: cn=developers,ou=groups,dc=example,dc=com

# search result
search: 2
result: 0 Success

Except the test user, cn=manager, dc=example, dc=com which will be used further for access to the database was also added here. Its rights have to be minimum necessary, and the password to differ from the password of cn=admin, dc=example, dc=com as it should be specified by the blank text in files of settings. Here the password of passwd is used that undoubtedly is a bad example.

Work with records not the most convenient and obvious business, but Apache Directory Studio can significantly help with OpenLDAP with it.


Redmine web application very well is suitable for project management. However its installation demands a large number of dependences of the Ruby language. My activity is not connected with Ruby or Rails therefore installation on the server of additional 76 packets which were demanded by ebuild was not desirable. The acceptable option was very quickly — Docker the container sameersbn/docker-redmine.

It is convenient to configure and start containers by means of docker-compose. This utility allows to integrate Docker command line options in one configuration file and to manage the started containers.

Very wide and work with it it is better for possibility of the docker-redmine setup to begin with official documentation at once. The example given below, only one of options which was convenient in my case:

Redmine settings of the container
# /var/www/

  # Автоматически запускать при старте системы и перезапускать в случае падения
  restart: always
    - TZ=Europe/Moscow
    # Использовать в контейнере uid:gid локального пользователя, чтобы 
    # владелец файлов, на хосте в смонтированных томах, был предсказуем.
    - DB_TYPE=postgres
    # Docker-у требуется существенное время, чтобы поднять bridge IP (, и лучше
    # с ним не связывать сервисы запускаемые при старте системы. Выставлять сервис наружу,
    # без крайней необходимости, тоже плохая идея. Можно добавить на сетевой интерфес IP,
    # который не маршрутизируется наружу и обращаться к нему из контейнеров.
    - DB_HOST=
    - DB_USER=redmine
    - DB_NAME=redmine_production
    - REDMINE_HTTPS=true
    - REDMINE_PORT=10083
    - SMTP_ENABLED=true
    # Локальная MTA доверяет подсети Docker-а, что в данном случае приемлемо.
    - SMTP_PORT=25
    - IMAP_ENABLED=false
    - ""
    - /var/www/
    # Чтобы далеко не ходить за логами.
    - /var/www/

Start of the container is made by command:

docker-compose -f /var/www/ up -d

In order that Redmine automatically created accounts based on accounts from OpenLDAP and used its authentication mechanism it is necessary to add the corresponding method to Administration → LDAP authentication → New authentication mode:

Name *: LDAP
Host *: [ВНЕШНИЙ_IP]
Port *: 636; LDAPS: x
Account: cn=manager,dc=example,dc=com
Password: passwd
Base DN *: ou=people,dc=example,dc=com
LDAP filter: (&(objectClass=person)(memberOf=cn=developers,ou=groups,dc=example,dc=com))
Timeout (in seconds):
On-the-fly user creation: x

Login attribute *: uid
Firstname attribute: givenName
Lastname attribute: sn
Email attribute: mail

The project can be added to Redmine only already existing Git repositories therefore the folder with repositories has to be available from the container. As in this case it is necessary to trace only locally located repositories, it is possible to be limited to assembling in the folder container with repositories, but it is not convenient. If it is required to connect an external repository, then its clone should be placed in a directory with own repositories, besides it is not beautiful to mount a system directory in the container.

As a result at me turned out the following set of scripts for work with local repositories:

Script for creation of new Git of a repository
set -e

# /var/git/

SCRIPT_NAME=`basename $0`

function usage()
	echo "USAGE:"
	echo "    $SCRIPT_NAME [repo_name]"
	echo "OPTIONS:"
	echo "    repo_name - name of the new Git repository."

function fatal_error()
	echo "$1" > /dev/stderr
	exit 1

# Проверить входные параметры.
if [ $# -ne 1 ]; then
	echo "Wrong number of arguments specified."


if [ -d "${REPO_NAME}" ]; then
	fatal_error "Error: the repository already exists!"

# Создать чистый репозиторий.
mkdir ${REPO_NAME}
git --bare init
git update-server-info -f

# Установить хук синхронизирующий Redmine копию репозитория с данным.
cd ..
cp ./post-update "${REPO_NAME}/hooks/"
chmod 755 "${REPO_NAME}/hooks/post-update"
chown -R nginx:nginx ${REPO_NAME}

echo "Git repository successfully created."
exit 0

Script for migration of the existing repository Git
set -e

# /var/git/

SCRIPT_NAME=`basename $0`

function usage()
	echo "USAGE:"
	echo "    $SCRIPT_NAME [repo_name]"
	echo "OPTIONS:"
	echo "    repo_name - name of the existing Git repository."

function fatal_error()
	echo "$1" > /dev/stderr
	exit 1

# Проверить входные параметры.
if [ $# -ne 1 ]; then
	echo "Wrong number of arguments specified."


if [ ! -d "${LOCAL_GIT_DIR}/${REPO_NAME}" ]; then
	fatal_error "Error: the repository does not exists!"

# Проверить, что ничего не будет сломано.
if [ -f "${LOCAL_GIT_DIR}/${REPO_NAME}/hooks/post-update" ]; then
	fatal_error "Error: post-update hook already exists! The repository already migrated or should be migrated manually."

if [ -d "${REDMINE_GIT_DIR}/${REPO_NAME}" ]; then
	fatal_error "Error: redmine already contains the repository with the same name!"

# Установить хук синхронизирующий Redmine копию репозитория с данным.
cp "${LOCAL_GIT_DIR}/post-update" "${LOCAL_GIT_DIR}/${REPO_NAME}/hooks/"
chown nginx:nginx "${LOCAL_GIT_DIR}/${REPO_NAME}/hooks/post-update"
chmod 755 "${LOCAL_GIT_DIR}/${REPO_NAME}/hooks/post-update"

# Создать Redmine копию репозитория.
git clone --mirror "${LOCAL_GIT_DIR}/${REPO_NAME}" ${REPO_NAME}

echo "Git repository successfully migrated."
exit 0

Hook for synchronization of Redmine of the copy of a repository with local

# /var/git/post-update

# Запускается на сервере после того, как git push успешно отработает в локальном репозитории.

REPO_NAME=$(basename "${REPO_PATH}")

function log_message()
	echo `date '+%d-%m-%y %H:%M:%S'` "$1" >>"${LOG_FILE}"

if [ -d "${REDMINE_GIT_DIR}" ]; then
	if [ -d "${REPO_NAME}" ]; then
		# Если копия репозитория уже создана, загрузить изменения.
		cd "${REPO_NAME}"
		log_message "UPDATED: ${PWD}"
		exec git fetch -q --all -p &>>"${LOG_FILE}"
		# Иначе создать клон репозитория.
		log_message "NEW: ${REPO_PATH} : ${REPO_NAME} : ${PWD}"
		exec git clone -q --mirror ${REPO_PATH} ${REPO_NAME} &>>"${LOG_FILE}"

In case of need to connect an external repository, for example GitHub, it will also need to be cloned in folder/var/www/, then to set a task in cron for periodic synchronization of Redmine of the copy with a remote repository.


Further it is necessary to open access to repositories on HTTPS. Unfortunately, from a box of NGINX does not support LDAP authorization, for this purpose he needs to be collected with the third-party kvspb/nginx-auth-ldap module.

In Gentoo for this purpose it is enough to copy ebuild in local portage (PORTDIR_OVERLAY) and to add to it this module. In official ebuild the set of modules is connected, it is possible to add one more. For the NGINX version actual at the moment, at me the following patch turned out:

Patch the adding http_auth_ldap module for nginx-1.8.0.ebuild
--- nginx-1.8.0.ebuild	2015-08-05 14:31:19.000000000 +0300
+++	2015-08-07 08:19:35.899578187 +0300
@@ -126,6 +126,12 @@
+# http_auth_ldap (, ??? license)
 inherit eutils ssl-cert toolchain-funcs perl-module flag-o-matic user systemd versionator multilib
 DESCRIPTION="Robust, small and high performance http and reverse proxy server"
@@ -148,7 +154,8 @@
 	nginx_modules_http_security? ( ${HTTP_SECURITY_MODULE_URI} -> ${HTTP_SECURITY_MODULE_P}.tar.gz )
 	nginx_modules_http_push_stream? ( ${HTTP_PUSH_STREAM_MODULE_URI} -> ${HTTP_PUSH_STREAM_MODULE_P}.tar.gz )
 	nginx_modules_http_sticky? ( ${HTTP_STICKY_MODULE_URI} -> ${HTTP_STICKY_MODULE_P}.tar.bz2 )
-	nginx_modules_http_mogilefs? ( ${HTTP_MOGILEFS_MODULE_URI} -> ${HTTP_MOGILEFS_MODULE_P}.tar.gz )"
+	nginx_modules_http_mogilefs? ( ${HTTP_MOGILEFS_MODULE_URI} -> ${HTTP_MOGILEFS_MODULE_P}.tar.gz )
+	nginx_modules_http_auth_ldap? ( ${HTTP_AUTH_LDAP_MODULE_URI} -> ${HTTP_AUTH_LDAP_MODULE_P}.tar.gz )"
 	nginx_modules_http_security? ( Apache-2.0 )
@@ -180,7 +187,8 @@
-	http_mogilefs"
+	http_mogilefs
+	http_auth_ldap"
 IUSE="aio debug +http +http-cache ipv6 libatomic luajit +pcre pcre-jit rtmp
 selinux ssl userland_GNU vim-syntax"
@@ -220,7 +228,8 @@
 	nginx_modules_http_auth_pam? ( virtual/pam )
 	nginx_modules_http_metrics? ( dev-libs/yajl )
 	nginx_modules_http_dav_ext? ( dev-libs/expat )
-	nginx_modules_http_security? ( >=dev-libs/libxml2-2.7.8 dev-libs/apr-util www-servers/apache )"
+	nginx_modules_http_security? ( >=dev-libs/libxml2-2.7.8 dev-libs/apr-util www-servers/apache )
+	nginx_modules_http_auth_ldap? ( net-nds/openldap )"
 	selinux? ( sec-policy/selinux-nginx )
@@ -440,6 +449,11 @@
 		myconf+=" --add-module=${HTTP_MOGILEFS_MODULE_WD}"
+	if use nginx_modules_http_auth_ldap; then
+		http_enabled=1
+		myconf+=" --add-module=${HTTP_AUTH_LDAP_MODULE_WD}"
+	fi
 	if use http || use http-cache; then

NGINX configuration option for a host of

# /etc/nginx/conf.d/

# Сокет сервиса fcgiwrap, должен быть доступен на запись пользователю nginx:nginx.
upstream fastcgi-server {
	server unix:/run/fcgiwrap.sock-1;

ldap_server ldap_git_users {
	# ldap[s]://hostname:port/base_dn?attributes?scope?filter
	url "ldap://,dc=example,dc=com?uid?sub?(objectClass=person)";
	# DN с ограниченными правами.
	binddn "cn=manager,dc=example,dc=com";
	binddn_passwd passwd;
	# Поиск пользователей непосредственно в группе без использования memberOf оверлея.
	group_attribute uniqueMember;
	# Атрибут должен содержать полный DN участника группы.
	group_attribute_is_dn on;
	# satisfy any;
	require group "cn=developers,ou=groups,dc=example,dc=com";

# Связь репозитория с пользователем в LDAP.
# $repo - репозиторий полученный из URL в location; $repo_login - авторизованный пользователь.
map $repo $repo_login {
	default "";
	# Можно разрешить доступ одному пользователю к нескольким репозиториям.
	"" "example";

# Редирект на HTTPS
server {
	listen [ВНЕШНИЙ_IP]:80;

	return 301$request_uri;

server {
	listen [ВНЕШНИЙ_IP]:443 ssl;

	add_header Strict-Transport-Security max-age=2592000;

	charset utf-8;

	ssl_certificate /etc/ssl/nginx/;
	ssl_certificate_key /etc/ssl/nginx/;
	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

	access_log /var/log/nginx/ main;
	error_log /var/log/nginx/ info;

	# Директория по умолчанию для аутентифицированных пользователей.
	# Если пользователь не авторизован для доступа в репозиторий, в этой папке он и останется.
	root /var/git/empty;

	# Статичные файлы Git репозитория.
	location ~ "^/(?<repo>[^/]+)/objects/([0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$" {
		auth_ldap "git::repository";
		auth_ldap_servers ldap_git_users;
		# значение переменной $repo_login получается сопоставлением в map переменной $repo
		if ($remote_user = $repo_login) {
			root /var/git;

	# Запросы к репозиторию, которые необходимо направить на git-http-backend.
	location ~ "^/(?<repo>[^/]+)/(HEAD|info/refs|objects/info/[^/]+|git-(upload|receive)-pack)$" {
		auth_ldap "git::repository";
		auth_ldap_servers ldap_git_users;

		fastcgi_param SCRIPT_FILENAME /usr/libexec/git-core/git-http-backend;
		fastcgi_param PATH_INFO $uri;
		fastcgi_param GIT_HTTP_EXPORT_ALL "";
		fastcgi_param GIT_PROJECT_ROOT /var/git;
		fastcgi_param REMOTE_USER $remote_user;
		include fastcgi_params;

		if ($remote_user = $repo_login) {
			fastcgi_pass fastcgi-server;

	# Если репозиторий не найден вернуть 404.
	location / {
		return 404;


The cloudy storage ownCloud already contains all necessary for work with LDAP. It is enough to come into Apps → Not enabled to find "LDAP user and group backend" and to click Enable. As a result in settings the point LDAP will appear. It is necessary to pass to it and, in the Server tab, to specify already usual settings:

SERVER:; Port: 389
DN: cn=manager,dc=example,dc=com
PASS: passwd
BASEDN: ou=ou=people,dc=example,dc=com

In order that earned the choice of group from the following tab, it is necessary to come into Advanced → Directory Settings and to change the following settings:

Group Display Name Field: cn
Base Group Tree: ou=groups,dc=example,dc=com
Group-Member association: uniqueMember

However even after that I had only one posixGroup that was explicit not enough. As settings allow to specify all filters manually, it is simpler and more reliable to use this function. For this purpose on the Server tab it is desirable to include the Manually enter LDAP filters setup, then to register all filters manually:

Users: (&(objectClass=person)(memberOf=cn=developers,ou=groups,dc=example,dc=com))
Login Attributes: (&(objectClass=person)(memberOf=cn=developers,ou=groups,dc=example,dc=com)(uid=%uid))
Groups: здесь ничего указывать не нужно, эта настройка добавляет в ownCloud дополнительные группы пользователей.

Having finished with it, it is possible to pass into Users and to be convinced that all necessary users are present at the list.

For assembling of ownCloud of the folder by means of davfs2, it is necessary to add on client side the next lines to files:

/ etc/davfs2/davfs2.conf

use_locks 0

/ etc/davfs2/secrets

/home/<user>/workspace/example/cloud example "passwd"

/ etc/fstab /home/<user>/workspace/example/cloud davfs user,noauto 0 0

And, at last, an example of an alyas of wo (work on) for activation of a working environment on the example of project Django:

# ~/.bashrc

alias wo=" \
    mkdir -p ~/workspace/example/cloud &&\
    (mountpoint -q ~/workspace/example/cloud || mount ~/workspace/example/cloud) &&\
    cp -u ~/workspace/example/cloud/deploy/ ~/workspace/example/src/project/ &&\
    source ~/VEnvs/example/bin/activate &&\
    cd ~/workspace/example/src \

All files of configurations and scripts used in article can be found on GitHub

This article is a translation of the original post at
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here:

We believe that the knowledge, which is available at the most popular Russian IT blog, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus