41

In my team we use Docker containers to locally run our website applications while we do development on them.

Assuming I'm working on a Flask app at app.py with dependencies in requirements.txt, a working flow would look roughly like this:

# I am "robin" and I am in the docker group
$ whoami
robin
$ groups
robin docker

# Install dependencies into a docker volume
$ docker run -ti -v `pwd`:`pwd` -w `pwd` -v pydeps:/usr/local python:3-slim pip install -r requirements.txt
Collecting Flask==0.12.2 (from -r requirements.txt (line 1))
# ... etc.

# Run the app using the same docker volume
$ docker run -ti -v `pwd`:`pwd` -w `pwd` -v pydeps:/usr/local -e FLASK_APP=app.py -e FLASK_DEBUG=true -p 5000:5000 python:3-slim flask run -h 0.0.0.0
 * Serving Flask app "app"
 * Forcing debug mode on
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 251-131-649

Now we have a local server running our application, and we can make changes to the local files and the server will refresh as needed.

In the above example, the application end up running as the root user. This isn't a problem unless the application writes files back into the working directory. If it does then we could end up with files (e.g. something like cache.sqlite or debug.log) in our working directory owned by root. This has caused a number of problems for users in our team.

For our other applications we've solved this by running the application with the host user's UID and GID - e.g. for a Django app:

$ docker run -ti -u `id -u`:`id -g` -v `pwd`:`pwd` -w `pwd` -v pydeps:/usr/local -p 8000:8000 python:3-slim ./manage.py runserver

In this case, the application will be running as a non-existent user with ID 1000 inside the container, but any files written to the host directory end up correctly owned by the robin user. This works fine in Django.

However, Flask refuses to run as a non-existent user (in debug mode):

$ docker run -ti -u `id -u`:`id -g` -v `pwd`:`pwd` -w `pwd` -v pydeps:/usr/local -e FLASK_APP=app.py -e FLASK_DEBUG=true -p 5000:5000 python:3-slim flask run -h 0.0.0.0
 * Serving Flask app "app"
 * Forcing debug mode on
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
Traceback (most recent call last):
...
  File "/usr/local/lib/python3.6/getpass.py", line 169, in getuser
    return pwd.getpwuid(os.getuid())[0]
KeyError: 'getpwuid(): uid not found: 1000'

Does anyone know if there's any way that I could either:

  • Make Flask not worry about the unassigned user-id, or
  • Somehow dynamically assign the user ID to a username at runtime, or
  • Otherwise allow the docker application to create files on the host as the host user?

The only solution I can think of right now (super hacky) is to change the permissions of /etc/passwd in the docker image to be globally writeable, and then add a new line to that file at runtime to assign the new UID/GID pair to a username.

Robin Winslow
  • 10,908
  • 8
  • 62
  • 91

3 Answers3

49

You can share the host's passwd file:

docker run -ti -v /etc/passwd:/etc/passwd -u `id -u`:`id -g` -v `pwd`:`pwd` -w `pwd` -v pydeps:/usr/local -p 8000:8000 python:3-slim ./manage.py runserver

Or, add the user to the image with useradd, using /etc as volume, in the same way you use /usr/local:

docker run -v etcvol:/etc python..... useradd -u `id -u` $USER

(Both id -u and $USER are resolved in the host shell, before docker receive the command)

Robert
  • 33,429
  • 8
  • 90
  • 94
  • 3
    I'll award you the bounty in 13 hours when it lets me – Robin Winslow Aug 30 '17 at 19:11
  • Actually I think I'll use your second suggestion 'cos it's more portable - e.g. will work with macOS systems. – Robin Winslow Aug 30 '17 at 20:06
  • I'm glad to know that it is useful! – Robert Aug 30 '17 at 20:58
  • The suggested trick assumes that the numeric user IDs (both those used by the image and the one used to run the image) agree with those in the host `/etc/passwd`. If they don't (as I would expect in general), the workarounds would not be generally safe either. We should understand what's attractive in running as the same user and how to internalize the urge by abstracting away from user names. – eel ghEEz Jan 14 '19 at 22:13
  • Ignoring the user names serves as a good first step in abstracting away from them: `docker run -v "${HOME}:/root" -w "/root" SERVER/IMAGE:VERSION env HOME="/root" COMMAND`. However, this needs cleaning up after the run because the docker daemon will write as `root` into `${HOME}`. – eel ghEEz Jan 14 '19 at 22:45
  • I needed `passwd` and also `shadow`, as in: `-v /etc/passwd:/etc/passwd -v /etc/shadow:/etc/shadow` – pestophagous May 12 '21 at 20:04
  • It might be worth mentioning how this solution impacts security of the host `/etc/passwd` file... the implicit implication is that you 100% trust the container... – Robert Oct 17 '21 at 19:15
  • For the second answer.. is useradd a docker parameter? eg if my normal comand is 'docker container run mycontainer:latest' where would I insert the "useradd" part to run it as the host user? – majorgear May 03 '22 at 16:07
5

Just hit this problem and found a different workaround.

From getpass.py:

def getuser():
    """Get the username from the environment or password database.

    First try various environment variables, then the password
    database.  This works on Windows as long as USERNAME is set.

    """


    for name in ('LOGNAME', 'USER', 'LNAME', 'USERNAME'):
        user = os.environ.get(name)
        if user:
            return user


    # If this fails, the exception will "explain" why
    import pwd
    return pwd.getpwuid(os.getuid())[0]

The call to getpwuid() is only made if none of the following environment variables are set: LOGNAME, USER, LNAME, USERNAME

Setting any of them should allow the container to start.

$ docker run -ti -e USER=someuser ...

In my case, the call to getuser() seems to come from the Werkzeug library trying to generate a debugger pin code.

codemonkey
  • 3,510
  • 3
  • 23
  • 35
  • 3
    Thank you. This is almost certainly the most 'correct' response. Especially when you look at what Werkzeug actually uses the username for, which is extremely trivial and not at all reliant on the value being 'correct'. Readers should avoid bind mounting `/etc/passwd` or creating a user inside the Docker container. These are much hacker solutions. – Kye Nov 15 '18 at 05:33
0

If it's okay for you to use another Python package to start your container, maybe you want to use my Python package https://github.com/boon-code/docker-inside which overwrites the entrypoint and creates your user in the container on the fly...

docker-inside -v `pwd`:`pwd` -w `pwd` -v pydeps:/usr/local -e FLASK_APP=app.py -e FLASK_DEBUG=true -p 5000:5000 python:3-slim -- flask run -h 0.0.0.0

Overriding entrypoint on the command line and passing a script that creates your user might also be okay for you, if you want to stick with Docker CLI.

boo-hoo
  • 123
  • 1
  • 9