Introduction

Oftentimes, applications insider docker containers are either run by root or some other user set up with some specific user and group ID.

However, sometimes it can be useful to have your application inside a container run by a user with the same user and group ID as the host's user. For example when you bind-mount scripts or secret files into your container that can be only executed / read by the host user. Or when you bind-mount volumes into your container and you do not want to have to use sudo on your host to e.g. remove files generated by the root user inside the container.

By default, the Jenkins docker container is set up with a user called jenkins with user and group ID 1000. In this post we will set up a docker-compose.yml file that creates a Jenkins server that is based on the official jenkins/jenkins:lts docker image and that changes the default uid and gid of the jenkins user to that of the host's user.

Implementation

TLDR: Code can be found here on Bitbucket.

The trick is to provide the uid and gid of the current user to a Dockerfile that modifies the default jenkins user.

Dockerfile

The Dockerfile is pretty straightforward:

FROM jenkins/jenkins:lts

USER root

ARG USER_GROUP_ID
ARG USER_ID

# RUN groupmod -g ${USER_GROUP_ID} jenkins
RUN usermod -u ${USER_ID} -g ${USER_GROUP_ID} jenkins

USER jenkins

WORKDIR $JENKINS_HOME

We are building upon the jenkins/jenkins:lts image. In order to run user modifications we first switch to USER root.

We then load the build arguments USER_GROUP_ID and USER_ID and run the usermod command to update the jenkins user. Those build arguments will correspond to the UID and GID of the local user starting the docker service and are provided by the docker-compose.yml file.

Note: Here we assume, the gid already exists inside the container. If it doesn't, see the Troubleshooting section at the end of this post.

docker-compose.yml

This one is also pretty straightforward:

version: "3.7"

services:
  jenkins:
    build:
      context: .
      args:
        USER_GROUP_ID: $USER_GROUP_ID
        USER_ID: $USER_ID
    ports:
      - 9080:8080
    volumes:
      - $HOME/data:/var/jenkins_home
      - ./bashrc:/var/jenkins_home/.bashrc

Here, we provide the build arguments used in the Dockerfile which will be fed by the shell environment. Additionally, we map port 8080 of the docker container to port 9080 on the host because 8080 is a popular choice for web applications so we try to avoid any conflict with other web apps running on the host machine.

To have persistent data storage beyond the life cycle of a container, we bind-mount the default JENKINS_HOME inside the container, that is /var/jenkins_home, to $HOME/data on the local PC, where $HOME corresponds to the home directory of the user running the docker-compose command. And because we want some nice colored bash output when we log into the docker container as jenkins user, we also bind-mount a custom .bashrc file with the following content:

#Changing color used to display stuff using 'ls' Version 2
export CLICOLOR=1
export LSCOLORS=gxFxCxDxBxegedabagaced

# Changing commandline prompt layout
PS1='\[\033[01;34m\]\u:\[\033[01;32m\]\w\\$\[$(tput sgr0)\] '

export LANG=en_US.UTF-8

# From ubunut 18.04 default bashrc: https://gist.github.com/marioBonales/1637696

# append to the history file, don't overwrite it
shopt -s histappend

# check the window size after each command and, if necessary,
# update the values of LINES and COLUMNS.
shopt -s checkwinsize

# make less more friendly for non-text input files, see lesspipe(1)
[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"

# enable color support of ls and also add handy aliases
if [ -x /usr/bin/dircolors ]; then
    test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
    alias ls='ls --color=auto'
    #alias dir='dir --color=auto'
    #alias vdir='vdir --color=auto'

    alias grep='grep --color=auto'
    alias fgrep='fgrep --color=auto'
    alias egrep='egrep --color=auto'
fi

# some more ls aliases
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'

# no duplicate shell history entries: https://unix.stackexchange.com/a/265649
export HISTCONTROL=ignoreboth:erasedups

So all we have to do now is to make sure that all shell variables used by the docker-compose.yml file are exported before calling a docker-compose command. We will achieve this with an extra shell script.

docker-compose helper-script: docker_run.sh

This is merely a convenience script that exports all necessary shell variables before calling docker-compose with the provided arguments:

#!/usr/bin/env bash

export USER_GROUP_ID=$(id -g)
export USER_ID=$(id -u)

docker-compose "$@"

And that's it. We can now start the service.

Start the dockerized Jenkins service

Simply execute the docker_run.sh script (you might have to chmod +x it first):

apoehlmann:~/workspace/blog/jenkins$ ./docker_run.sh up
Building jenkins
Step 1/7 : FROM jenkins/jenkins:lts
 ---> a3f949e5ebfd
Step 2/7 : USER root
 ---> Running in 93cd6884b1af
Removing intermediate container 93cd6884b1af
 ---> 7eba33317591
Step 3/7 : ARG USER_GROUP_ID
 ---> Running in 93dd5d13e170
Removing intermediate container 93dd5d13e170
 ---> bfc93dfc996a
Step 4/7 : ARG USER_ID
 ---> Running in 86ea31efd5c1
Removing intermediate container 86ea31efd5c1
 ---> b1bc14a41de5
Step 5/7 : RUN usermod -u ${USER_ID} -g ${USER_GROUP_ID} jenkins
 ---> Running in 8f512a37a4ce
Removing intermediate container 8f512a37a4ce
 ---> cf8fb4f7b566
Step 6/7 : USER jenkins
 ---> Running in 6f295c498fd2
Removing intermediate container 6f295c498fd2
 ---> 4dd39e8316f7
Step 7/7 : WORKDIR $JENKINS_HOME
 ---> Running in 90967823f6e0
Removing intermediate container 90967823f6e0
 ---> 80686889cc36

Successfully built 80686889cc36
Successfully tagged jenkins_jenkins:latest
WARNING: Image for service jenkins was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating jenkins_jenkins_1 ... done
Attaching to jenkins_jenkins_1
jenkins_1  | Running from: /usr/share/jenkins/jenkins.war
jenkins_1  | webroot: EnvVars.masterEnvVars.get("JENKINS_HOME")
jenkins_1  | 2020-01-18 13:07:55.648+0000 [id=1]	INFO	org.eclipse.jetty.util.log.Log#initialized: Logging initialized @423ms to org.eclipse.jetty.util.log.JavaUtilLog
jenkins_1  | 2020-01-18 13:07:55.790+0000 [id=1]	INFO	winstone.Logger#logInternal: Beginning extraction from war file
jenkins_1  | 2020-01-18 13:08:04.243+0000 [id=1]	WARNING	o.e.j.s.handler.ContextHandler#setContextPath: Empty contextPath
jenkins_1  | 2020-01-18 13:08:04.315+0000 [id=1]	INFO	org.eclipse.jetty.server.Server#doStart: jetty-9.4.z-SNAPSHOT; built: 2019-05-02T00:04:53.875Z; git: e1bc35120a6617ee3df052294e433f3a25ce7097; jvm 1.8.0_232-b09
jenkins_1  | 2020-01-18 13:08:06.502+0000 [id=1]	INFO	o.e.j.w.StandardDescriptorProcessor#visitServlet: NO JSP Support for /, did not find org.eclipse.jetty.jsp.JettyJspServlet
jenkins_1  | 2020-01-18 13:08:06.619+0000 [id=1]	INFO	o.e.j.s.s.DefaultSessionIdManager#doStart: DefaultSessionIdManager workerName=node0
jenkins_1  | 2020-01-18 13:08:06.619+0000 [id=1]	INFO	o.e.j.s.s.DefaultSessionIdManager#doStart: No SessionScavenger set, using defaults
jenkins_1  | 2020-01-18 13:08:06.628+0000 [id=1]	INFO	o.e.j.server.session.HouseKeeper#startScavenging: node0 Scavenging every 660000ms
jenkins_1  | 2020-01-18 13:08:07.232+0000 [id=1]	INFO	hudson.WebAppMain#contextInitialized: Jenkins home directory: /var/jenkins_home found at: EnvVars.masterEnvVars.get("JENKINS_HOME")
jenkins_1  | 2020-01-18 13:08:07.437+0000 [id=1]	INFO	o.e.j.s.handler.ContextHandler#doStart: Started w.@26be6ca7{Jenkins v2.204.1,/,file:///var/jenkins_home/war/,AVAILABLE}{/var/jenkins_home/war}
jenkins_1  | 2020-01-18 13:08:07.475+0000 [id=1]	INFO	o.e.j.server.AbstractConnector#doStart: Started ServerConnector@19932c16{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
jenkins_1  | 2020-01-18 13:08:07.477+0000 [id=1]	INFO	org.eclipse.jetty.server.Server#doStart: Started @12251ms
jenkins_1  | 2020-01-18 13:08:07.478+0000 [id=21]	INFO	winstone.Logger#logInternal: Winstone Servlet Engine v4.0 running: controlPort=disabled
jenkins_1  | 2020-01-18 13:08:08.730+0000 [id=28]	INFO	jenkins.InitReactorRunner$1#onAttained: Started initialization
jenkins_1  | 2020-01-18 13:08:08.793+0000 [id=35]	INFO	jenkins.InitReactorRunner$1#onAttained: Listed all plugins
jenkins_1  | 2020-01-18 13:08:10.263+0000 [id=35]	INFO	jenkins.InitReactorRunner$1#onAttained: Prepared all plugins
jenkins_1  | 2020-01-18 13:08:10.275+0000 [id=29]	INFO	jenkins.InitReactorRunner$1#onAttained: Started all plugins
jenkins_1  | 2020-01-18 13:08:10.282+0000 [id=29]	INFO	jenkins.InitReactorRunner$1#onAttained: Augmented all extensions
jenkins_1  | 2020-01-18 13:08:10.680+0000 [id=29]	INFO	jenkins.InitReactorRunner$1#onAttained: Loaded all jobs
jenkins_1  | 2020-01-18 13:08:10.699+0000 [id=50]	INFO	hudson.model.AsyncPeriodicWork#lambda$doRun$0: Started Download metadata
jenkins_1  | 2020-01-18 13:08:10.733+0000 [id=50]	INFO	hudson.util.Retrier#start: Attempt #1 to do the action check updates server
jenkins_1  | 2020-01-18 13:08:11.780+0000 [id=31]	INFO	o.s.c.s.AbstractApplicationContext#prepareRefresh: Refreshing org.springframework.web.context.support.StaticWebApplicationContext@74dbe0e8: display name [Root WebApplicationContext]; startup date [Sat Jan 18 13:08:11 UTC 2020]; root of context hierarchy
jenkins_1  | 2020-01-18 13:08:11.780+0000 [id=31]	INFO	o.s.c.s.AbstractApplicationContext#obtainFreshBeanFactory: Bean factory for application context [org.springframework.web.context.support.StaticWebApplicationContext@74dbe0e8]: org.springframework.beans.factory.support.DefaultListableBeanFactory@6763fc50
jenkins_1  | 2020-01-18 13:08:11.796+0000 [id=31]	INFO	o.s.b.f.s.DefaultListableBeanFactory#preInstantiateSingletons: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@6763fc50: defining beans [authenticationManager]; root of factory hierarchy
jenkins_1  | 2020-01-18 13:08:12.053+0000 [id=31]	INFO	o.s.c.s.AbstractApplicationContext#prepareRefresh: Refreshing org.springframework.web.context.support.StaticWebApplicationContext@50d7fe60: display name [Root WebApplicationContext]; startup date [Sat Jan 18 13:08:12 UTC 2020]; root of context hierarchy
jenkins_1  | 2020-01-18 13:08:12.053+0000 [id=31]	INFO	o.s.c.s.AbstractApplicationContext#obtainFreshBeanFactory: Bean factory for application context [org.springframework.web.context.support.StaticWebApplicationContext@50d7fe60]: org.springframework.beans.factory.support.DefaultListableBeanFactory@666ab6a
jenkins_1  | 2020-01-18 13:08:12.055+0000 [id=31]	INFO	o.s.b.f.s.DefaultListableBeanFactory#preInstantiateSingletons: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@666ab6a: defining beans [filter,legacy]; root of factory hierarchy
jenkins_1  | 2020-01-18 13:08:12.508+0000 [id=31]	INFO	jenkins.install.SetupWizard#init: 
jenkins_1  | 
jenkins_1  | *************************************************************
jenkins_1  | *************************************************************
jenkins_1  | *************************************************************
jenkins_1  | 
jenkins_1  | Jenkins initial setup is required. An admin user has been created and a password generated.
jenkins_1  | Please use the following password to proceed to installation:
jenkins_1  | 
jenkins_1  | ebf401f67f2d4608a1f7cd51e1118527
jenkins_1  | 
jenkins_1  | This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
jenkins_1  | 
jenkins_1  | *************************************************************
jenkins_1  | *************************************************************
jenkins_1  | *************************************************************
jenkins_1  | 
jenkins_1  | 2020-01-18 13:08:15.676+0000 [id=50]	INFO	hudson.model.UpdateSite#updateData: Obtained the latest update center data file for UpdateSource default
jenkins_1  | 2020-01-18 13:08:16.108+0000 [id=50]	INFO	h.m.DownloadService$Downloadable#load: Obtained the updated data file for hudson.tasks.Maven.MavenInstaller
jenkins_1  | 2020-01-18 13:08:16.109+0000 [id=50]	INFO	hudson.util.Retrier#start: Performed the action check updates server successfully at the attempt #1
jenkins_1  | 2020-01-18 13:08:16.114+0000 [id=50]	INFO	hudson.model.AsyncPeriodicWork#lambda$doRun$0: Finished Download metadata. 5,407 ms
jenkins_1  | 2020-01-18 13:08:21.243+0000 [id=31]	INFO	hudson.model.UpdateSite#updateData: Obtained the latest update center data file for UpdateSource default
jenkins_1  | 2020-01-18 13:08:21.397+0000 [id=31]	INFO	jenkins.InitReactorRunner$1#onAttained: Completed initialization
jenkins_1  | 2020-01-18 13:08:21.424+0000 [id=20]	INFO	hudson.WebAppMain$3#run: Jenkins is fully up and running

Sanity check

We can now logg into the docker container and check the uid and gid of the jenkins user

# logg into the container
apoehlmann:~$ docker exec -it jenkins_jenkins_1 bash 
# check id inside the container
jenkins:~$ id
uid=501(jenkins) gid=20(dialout) groups=20(dialout)

and compare it against the host's user data and see that GID and UID of the different user names match:

apoehlmann:~$ id
uid=501(apoehlmann) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),98(_lpadmin),701(com.apple.sharepoint.group.1),33(_appstore),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh),400(com.apple.access_remote_ae)

As a last check, we compare the user and group of the written files inside the docker container to those of the bind-mounted directory:

# Logg into container and check user and group of written files
apoehlmann:~$ docker exec -it jenkins_jenkins_1 bash
jenkins:~$ ll
total 40
drwxr-xr-x 24 jenkins dialout  768 Jan 18 13:21 ./
drwxr-xr-x  1 root    root    4096 Dec 19 08:44 ../
-rw-r--r--  1 jenkins dialout 1242 Jan 18 13:14 .bashrc
drwxr-xr-x  3 jenkins dialout   96 Jan 18 13:20 .cache/
drwxr-xr-x  3 jenkins dialout   96 Jan 18 13:20 .java/
-rw-r--r--  1 jenkins dialout    0 Jan 18 13:21 .lastStarted
-rw-r--r--  1 jenkins dialout 1643 Jan 18 13:21 config.xml
-rw-r--r--  1 jenkins dialout   50 Jan 18 13:20 copy_reference_file.log
-rw-r--r--  1 jenkins dialout  156 Jan 18 13:20 hudson.model.UpdateCenter.xml
-rw-------  1 jenkins dialout 1712 Jan 18 13:20 identity.key.enc
-rw-r--r--  1 jenkins dialout    7 Jan 18 13:20 jenkins.install.UpgradeWizard.state
-rw-r--r--  1 jenkins dialout  171 Jan 18 13:20 jenkins.telemetry.Correlator.xml
drwxr-xr-x  2 jenkins dialout   64 Jan 18 13:20 jobs/
drwxr-xr-x  3 jenkins dialout   96 Jan 18 13:20 logs/
-rw-r--r--  1 jenkins dialout  907 Jan 18 13:20 nodeMonitors.xml
drwxr-xr-x  2 jenkins dialout   64 Jan 18 13:20 nodes/
drwxr-xr-x  2 jenkins dialout   64 Jan 18 13:20 plugins/
-rw-r--r--  1 jenkins dialout   64 Jan 18 13:20 secret.key
-rw-r--r--  1 jenkins dialout    0 Jan 18 13:20 secret.key.not-so-secret
drwx------  9 jenkins dialout  288 Jan 18 13:20 secrets/
drwxr-xr-x  4 jenkins dialout  128 Jan 18 13:21 updates/
drwxr-xr-x  3 jenkins dialout   96 Jan 18 13:20 userContent/
drwxr-xr-x  4 jenkins dialout  128 Jan 18 13:20 users/
drwxr-xr-x 26 jenkins dialout  832 Jan 18 13:20 war/

In our bind-mounted directory under $HOME/data/jenkins of the local PC, we see that the user and group correspond to those of the host user:

# On the local PC where we started the docker service
apoehlmann:~$ ll data/jenkins/
total 72
drwxr-xr-x  23 apoehlmann  staff   736 Jan 18 14:20 ./
drwxr-xr-x   3 apoehlmann  staff    96 Jan 18 14:20 ../
-rwxr-xr-x   1 apoehlmann  staff     0 Jan 18 14:20 .bashrc*
drwxr-xr-x   3 apoehlmann  staff    96 Jan 18 14:20 .cache/
drwxr-xr-x   3 apoehlmann  staff    96 Jan 18 14:20 .java/
-rw-r--r--   1 apoehlmann  staff  1658 Jan 18 14:20 config.xml
-rw-r--r--   1 apoehlmann  staff    50 Jan 18 14:20 copy_reference_file.log
-rw-r--r--   1 apoehlmann  staff    29 Jan 18 14:20 failed-boot-attempts.txt
-rw-r--r--   1 apoehlmann  staff   156 Jan 18 14:20 hudson.model.UpdateCenter.xml
-rw-------   1 apoehlmann  staff  1712 Jan 18 14:20 identity.key.enc
-rw-r--r--   1 apoehlmann  staff     7 Jan 18 14:20 jenkins.install.UpgradeWizard.state
-rw-r--r--   1 apoehlmann  staff   171 Jan 18 14:20 jenkins.telemetry.Correlator.xml
drwxr-xr-x   2 apoehlmann  staff    64 Jan 18 14:20 jobs/
drwxr-xr-x   3 apoehlmann  staff    96 Jan 18 14:20 logs/
-rw-r--r--   1 apoehlmann  staff   907 Jan 18 14:20 nodeMonitors.xml
drwxr-xr-x   2 apoehlmann  staff    64 Jan 18 14:20 nodes/
drwxr-xr-x   2 apoehlmann  staff    64 Jan 18 14:20 plugins/
-rw-r--r--   1 apoehlmann  staff    64 Jan 18 14:20 secret.key
-rw-r--r--   1 apoehlmann  staff     0 Jan 18 14:20 secret.key.not-so-secret
drwx------   9 apoehlmann  staff   288 Jan 18 14:20 secrets/
drwxr-xr-x   3 apoehlmann  staff    96 Jan 18 14:20 userContent/
drwxr-xr-x   4 apoehlmann  staff   128 Jan 18 14:20 users/
drwxr-xr-x  26 apoehlmann  staff   832 Jan 18 14:20 war/

Everything behaves as expected and we achieved our goal: the Jenkins application inside the container is run by a user with uid and gid identical to the ones of the host user.

Troubleshooting

In my case, the gid of the host user is 20 which already exists in the Docker image (group name dialout). However, your the GID of your host user could be different and might not exist. In this case, you would have to create the group first by adding the following line to the Dockerfile right before the RUN usermod command:

RUN groupmod -g ${USER_GROUP_ID} jenkins
RUN usermod -u ${USER_ID} -g ${USER_GROUP_ID} jenkins
Getagged mit:
Docker Tutorial Jenkins-Docker-Tutorial
blog comments powered by Disqus