Repository: freqtrade/freqtrade

Issue:
Docker container as non root user
The current docker container uses root user by default. This caused permission problems from the host side. For example, I can't edit files from binded folders without sudo. 


Discussion:
that is correct, and i've stumbled over this just this week.

The problem is - changing it now to a non-root user will break everybody's running setup - because then the docker-running bot won't be able to read it's own files (trades.sqlite for example).
won't fix? documentation issue?

I don't know ... 
I'd really like to fix it as it bothers me myself (but for other reasons than the above) - but ideally in a way that won't break users installations and require a "chown" for all files created by the bot.


what's the problem describe and use `--user` option of `docker-compose run`?

I think the point is to add a `USER someuser` line in the Dockerfile.

It's a valid point - as running as root within docker can cause security problems and is generally seen as bad practice.

It's the update process (from running as Root to non-root) that worries me - as then the new container would loose access to the already existing files.

This isn't a bug with freqtrade, per se, this is how docker is supposed to be used, see https://docs.docker.com/engine/install/linux-postinstall/

Docker installation requires to add your user to the docker group and chmod your docker files to something appropriate, either your user or the docker group.

Running docker as non-root considered an experimental feature.
@nardholio i think you misunderstood this issue....

The problem is that the process within docker is running as root user (most other containers use a dedicated, non-root user).
This has security implications as in theory, a root user within docker could gain root access to the host (assuming the container is started with certain capabilities).

Also, files created by the container on a bind directory will be owned by root - making them difficult to access.

Now changing this in the dockerfile is as easy as adding a user within the container, and using the `USER <createduser>` command within the dockerfile.


The problem is, that this change would break every running container, as the new container (with the new version) would lose access to the files the previous container created...
I'm experiencing the same issue on WSL2 and docker, everything created by docker is having root permissions. Do you have a workaround for this?
it's currently intentional that everything is owned by root.
For host-level permissions, you should be able to get around this with "user id mapping" - but please research this yourself as we don't give support on this - as you can mess up your permissions / docker installation with it quite nicely...


But exactly this is showing the issue i'm referring to - Transitioning from a root-based container to a non-root based container will not work, as your existing files will have the wrong permissions, which will cause a lot of errors for users simply upgrading their containers.

Resolving patch:
diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py
--- a/freqtrade/commands/build_config_commands.py
+++ b/freqtrade/commands/build_config_commands.py
@@ -5,6 +5,7 @@
 
 from questionary import Separator, prompt
 
+from freqtrade.configuration.directory_operations import chown_user_directory
 from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
 from freqtrade.exceptions import OperationalException
 from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges
@@ -216,6 +217,7 @@ def start_new_config(args: Dict[str, Any]) -> None:
     """
 
     config_path = Path(args['config'][0])
+    chown_user_directory(config_path.parent)
     if config_path.exists():
         overwrite = ask_user_overwrite(config_path)
         if overwrite:
diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py
--- a/freqtrade/configuration/directory_operations.py
+++ b/freqtrade/configuration/directory_operations.py
@@ -24,6 +24,21 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> Pat
     return folder
 
 
+def chown_user_directory(directory: Path) -> None:
+    """
+    Use Sudo to change permissions of the home-directory if necessary
+    Only applies when running in docker!
+    """
+    import os
+    if os.environ.get('FT_APP_ENV') == 'docker':
+        try:
+            import subprocess
+            subprocess.check_output(
+                ['sudo', 'chown', '-R', 'ftuser:', str(directory.resolve())])
+        except Exception:
+            logger.warning(f"Could not chown {directory}")
+
+
 def create_userdata_dir(directory: str, create_dir: bool = False) -> Path:
     """
     Create userdata directory structure.
@@ -37,6 +52,7 @@ def create_userdata_dir(directory: str, create_dir: bool = False) -> Path:
     sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "logs",
                 "notebooks", "plot", "strategies", ]
     folder = Path(directory)
+    chown_user_directory(folder)
     if not folder.is_dir():
         if create_dir:
             folder.mkdir(parents=True)


Test patch:
diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py
--- a/tests/test_directory_operations.py
+++ b/tests/test_directory_operations.py
@@ -1,11 +1,12 @@
 # pragma pylint: disable=missing-docstring, protected-access, invalid-name
+import os
 from pathlib import Path
 from unittest.mock import MagicMock
 
 import pytest
 
-from freqtrade.configuration.directory_operations import (copy_sample_files, create_datadir,
-                                                          create_userdata_dir)
+from freqtrade.configuration.directory_operations import (chown_user_directory, copy_sample_files,
+                                                          create_datadir, create_userdata_dir)
 from freqtrade.exceptions import OperationalException
 from tests.conftest import log_has, log_has_re
 
@@ -31,6 +32,24 @@ def test_create_userdata_dir(mocker, default_conf, caplog) -> None:
     assert str(x) == str(Path("/tmp/bar"))
 
 
+def test_create_userdata_dir_and_chown(mocker, tmpdir, caplog) -> None:
+    sp_mock = mocker.patch('subprocess.check_output')
+    path = Path(tmpdir / 'bar')
+    assert not path.is_dir()
+
+    x = create_userdata_dir(str(path), create_dir=True)
+    assert sp_mock.call_count == 0
+    assert log_has(f'Created user-data directory: {path}', caplog)
+    assert isinstance(x, Path)
+    assert path.is_dir()
+    assert (path / 'data').is_dir()
+
+    os.environ['FT_APP_ENV'] = 'docker'
+    chown_user_directory(path / 'data')
+    assert sp_mock.call_count == 1
+    del os.environ['FT_APP_ENV']
+
+
 def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None:
     mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
     md = mocker.patch.object(Path, 'mkdir', MagicMock())