From ec87479b7e2bf6f1b5bcc657a377c6e6a847574f Mon Sep 17 00:00:00 2001
From: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
Date: Mon, 15 Oct 2018 16:52:53 +0900
Subject: [PATCH] allow running `dockerd` in an unprivileged user namespace
 (rootless mode)

Please refer to `docs/rootless.md`.

TLDR:
 * Make sure `/etc/subuid` and `/etc/subgid` contain the entry for you
 * `dockerd-rootless.sh --experimental`
 * `docker -H unix://$XDG_RUNTIME_DIR/docker.sock run ...`

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
---
 Dockerfile                                    |  8 +-
 cli/config/configdir.go                       |  2 +
 cmd/dockerd/config.go                         | 15 ++-
 cmd/dockerd/config_common_unix.go             | 41 ++++++++-
 cmd/dockerd/config_unix.go                    | 11 ++-
 cmd/dockerd/config_unix_test.go               |  3 +-
 cmd/dockerd/config_windows.go                 | 23 +++--
 cmd/dockerd/daemon.go                         | 54 +++++++++--
 cmd/dockerd/daemon_test.go                    | 32 ++++---
 cmd/dockerd/daemon_unix.go                    | 28 +++++-
 cmd/dockerd/daemon_unix_test.go               | 10 +-
 cmd/dockerd/daemon_windows.go                 |  8 +-
 cmd/dockerd/docker.go                         | 25 +++--
 cmd/dockerd/options.go                        |  2 +
 contrib/dockerd-rootless.sh                   | 77 ++++++++++++++++
 daemon/config/config_unix.go                  |  6 ++
 daemon/config/config_windows.go               |  5 +
 daemon/daemon.go                              |  1 +
 daemon/daemon_unix.go                         |  3 -
 daemon/info.go                                |  3 +
 daemon/info_unix.go                           |  4 +
 daemon/info_windows.go                        |  4 +
 daemon/listeners/listeners_linux.go           |  5 +
 daemon/oci_linux.go                           |  9 +-
 docs/rootless.md                              | 92 +++++++++++++++++++
 hack/dockerfile/install/rootlesskit.installer | 34 +++++++
 hack/make/.binary-setup                       |  2 +
 hack/make/binary-daemon                       |  2 +-
 hack/make/install-binary                      |  2 +
 opts/hosts.go                                 | 15 ++-
 opts/hosts_test.go                            |  4 +-
 pkg/homedir/homedir_linux.go                  | 88 ++++++++++++++++++
 pkg/homedir/homedir_others.go                 | 20 ++++
 pkg/sysinfo/sysinfo_linux.go                  |  4 +-
 rootless/rootless.go                          | 26 ++++++
 rootless/specconv/specconv_linux.go           | 38 ++++++++
 36 files changed, 638 insertions(+), 68 deletions(-)
 create mode 100755 contrib/dockerd-rootless.sh
 create mode 100644 docs/rootless.md
 create mode 100755 hack/dockerfile/install/rootlesskit.installer
 create mode 100644 rootless/rootless.go
 create mode 100644 rootless/specconv/specconv_linux.go

diff --git a/Dockerfile b/Dockerfile
index b4b9a40c39..6516403acf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -161,7 +161,12 @@ ENV INSTALL_BINARY_NAME=tini
 COPY hack/dockerfile/install/$INSTALL_BINARY_NAME.installer ./
 RUN PREFIX=/build ./install.sh $INSTALL_BINARY_NAME
 
-
+FROM base AS rootlesskit
+ENV INSTALL_BINARY_NAME=rootlesskit
+COPY hack/dockerfile/install/install.sh ./install.sh
+COPY hack/dockerfile/install/$INSTALL_BINARY_NAME.installer ./
+RUN PREFIX=/build/ ./install.sh $INSTALL_BINARY_NAME
+COPY ./contrib/dockerd-rootless.sh /build
 
 # TODO: Some of this is only really needed for testing, it would be nice to split this up
 FROM runtime-dev AS dev
@@ -233,6 +238,7 @@ RUN cd /docker-py \
 	&& pip install paramiko==2.4.2 \
 	&& pip install yamllint==1.5.0 \
 	&& pip install -r test-requirements.txt
+COPY --from=rootlesskit /build/ /usr/local/bin/
 
 ENV PATH=/usr/local/cli:$PATH
 ENV DOCKER_BUILDTAGS apparmor seccomp selinux
diff --git a/cli/config/configdir.go b/cli/config/configdir.go
index 4bef4e104d..f0b68ee387 100644
--- a/cli/config/configdir.go
+++ b/cli/config/configdir.go
@@ -13,6 +13,8 @@ var (
 )
 
 // Dir returns the path to the configuration directory as specified by the DOCKER_CONFIG environment variable.
+// If DOCKER_CONFIG is unset, Dir returns ~/.docker .
+// Dir ignores XDG_CONFIG_HOME (same as the docker client).
 // TODO: this was copied from cli/config/configfile and should be removed once cmd/dockerd moves
 func Dir() string {
 	return configDir
diff --git a/cmd/dockerd/config.go b/cmd/dockerd/config.go
index 2c8ed8edb4..bf64108ef4 100644
--- a/cmd/dockerd/config.go
+++ b/cmd/dockerd/config.go
@@ -17,8 +17,20 @@ const (
 )
 
 // installCommonConfigFlags adds flags to the pflag.FlagSet to configure the daemon
-func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
+func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
 	var maxConcurrentDownloads, maxConcurrentUploads int
+	defaultPidFile, err := getDefaultPidFile()
+	if err != nil {
+		return err
+	}
+	defaultDataRoot, err := getDefaultDataRoot()
+	if err != nil {
+		return err
+	}
+	defaultExecRoot, err := getDefaultExecRoot()
+	if err != nil {
+		return err
+	}
 
 	installRegistryServiceFlags(&conf.ServiceOptions, flags)
 
@@ -80,6 +92,7 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
 
 	conf.MaxConcurrentDownloads = &maxConcurrentDownloads
 	conf.MaxConcurrentUploads = &maxConcurrentUploads
+	return nil
 }
 
 func installRegistryServiceFlags(options *registry.ServiceOptions, flags *pflag.FlagSet) {
diff --git a/cmd/dockerd/config_common_unix.go b/cmd/dockerd/config_common_unix.go
index febf30ae9f..c6526285f3 100644
--- a/cmd/dockerd/config_common_unix.go
+++ b/cmd/dockerd/config_common_unix.go
@@ -3,17 +3,48 @@
 package main
 
 import (
+	"path/filepath"
+
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/daemon/config"
 	"github.com/docker/docker/opts"
+	"github.com/docker/docker/pkg/homedir"
+	"github.com/docker/docker/rootless"
 	"github.com/spf13/pflag"
 )
 
-var (
-	defaultPidFile  = "/var/run/docker.pid"
-	defaultDataRoot = "/var/lib/docker"
-	defaultExecRoot = "/var/run/docker"
-)
+func getDefaultPidFile() (string, error) {
+	if !rootless.RunningWithNonRootUsername() {
+		return "/var/run/docker.pid", nil
+	}
+	runtimeDir, err := homedir.GetRuntimeDir()
+	if err != nil {
+		return "", err
+	}
+	return filepath.Join(runtimeDir, "docker.pid"), nil
+}
+
+func getDefaultDataRoot() (string, error) {
+	if !rootless.RunningWithNonRootUsername() {
+		return "/var/lib/docker", nil
+	}
+	dataHome, err := homedir.GetDataHome()
+	if err != nil {
+		return "", err
+	}
+	return filepath.Join(dataHome, "docker"), nil
+}
+
+func getDefaultExecRoot() (string, error) {
+	if !rootless.RunningWithNonRootUsername() {
+		return "/var/run/docker", nil
+	}
+	runtimeDir, err := homedir.GetRuntimeDir()
+	if err != nil {
+		return "", err
+	}
+	return filepath.Join(runtimeDir, "docker"), nil
+}
 
 // installUnixConfigFlags adds command-line options to the top-level flag parser for
 // the current process that are common across Unix platforms.
diff --git a/cmd/dockerd/config_unix.go b/cmd/dockerd/config_unix.go
index 2dbd84b1db..cc42ff36c8 100644
--- a/cmd/dockerd/config_unix.go
+++ b/cmd/dockerd/config_unix.go
@@ -5,14 +5,17 @@ package main
 import (
 	"github.com/docker/docker/daemon/config"
 	"github.com/docker/docker/opts"
+	"github.com/docker/docker/rootless"
 	"github.com/docker/go-units"
 	"github.com/spf13/pflag"
 )
 
 // installConfigFlags adds flags to the pflag.FlagSet to configure the daemon
-func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
+func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
 	// First handle install flags which are consistent cross-platform
-	installCommonConfigFlags(conf, flags)
+	if err := installCommonConfigFlags(conf, flags); err != nil {
+		return err
+	}
 
 	// Then install flags common to unix platforms
 	installUnixConfigFlags(conf, flags)
@@ -46,5 +49,7 @@ func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
 	flags.BoolVar(&conf.NoNewPrivileges, "no-new-privileges", false, "Set no-new-privileges by default for new containers")
 	flags.StringVar(&conf.IpcMode, "default-ipc-mode", config.DefaultIpcMode, `Default mode for containers ipc ("shareable" | "private")`)
 	flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "Default address pools for node specific local networks")
-
+	// Mostly users don't need to set this flag explicitly.
+	flags.BoolVar(&conf.Rootless, "rootless", rootless.RunningWithNonRootUsername(), "Enable rootless mode (experimental)")
+	return nil
 }
diff --git a/cmd/dockerd/config_unix_test.go b/cmd/dockerd/config_unix_test.go
index d7dbf4b4cc..4b5e894beb 100644
--- a/cmd/dockerd/config_unix_test.go
+++ b/cmd/dockerd/config_unix_test.go
@@ -15,7 +15,8 @@ func TestDaemonParseShmSize(t *testing.T) {
 	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
 
 	conf := &config.Config{}
-	installConfigFlags(conf, flags)
+	err := installConfigFlags(conf, flags)
+	assert.NilError(t, err)
 	// By default `--default-shm-size=64M`
 	assert.Check(t, is.Equal(int64(64*1024*1024), conf.ShmSize.Value()))
 	assert.Check(t, flags.Set("default-shm-size", "128M"))
diff --git a/cmd/dockerd/config_windows.go b/cmd/dockerd/config_windows.go
index 36af76645f..88b76196dc 100644
--- a/cmd/dockerd/config_windows.go
+++ b/cmd/dockerd/config_windows.go
@@ -8,19 +8,28 @@ import (
 	"github.com/spf13/pflag"
 )
 
-var (
-	defaultPidFile  string
-	defaultDataRoot = filepath.Join(os.Getenv("programdata"), "docker")
-	defaultExecRoot = filepath.Join(os.Getenv("programdata"), "docker", "exec-root")
-)
+func getDefaultPidFile() (string, error) {
+	return "", nil
+}
+
+func getDefaultDataRoot() (string, error) {
+	return filepath.Join(os.Getenv("programdata"), "docker"), nil
+}
+
+func getDefaultExecRoot() (string, error) {
+	return filepath.Join(os.Getenv("programdata"), "docker", "exec-root"), nil
+}
 
 // installConfigFlags adds flags to the pflag.FlagSet to configure the daemon
-func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
+func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
 	// First handle install flags which are consistent cross-platform
-	installCommonConfigFlags(conf, flags)
+	if err := installCommonConfigFlags(conf, flags); err != nil {
+		return err
+	}
 
 	// Then platform-specific install flags.
 	flags.StringVar(&conf.BridgeConfig.FixedCIDR, "fixed-cidr", "", "IPv4 subnet for fixed IPs")
 	flags.StringVarP(&conf.BridgeConfig.Iface, "bridge", "b", "", "Attach containers to a virtual switch")
 	flags.StringVarP(&conf.SocketGroup, "group", "G", "", "Users or groups that can access the named pipe")
+	return nil
 }
diff --git a/cmd/dockerd/daemon.go b/cmd/dockerd/daemon.go
index 0daf197270..f61246d5c6 100644
--- a/cmd/dockerd/daemon.go
+++ b/cmd/dockerd/daemon.go
@@ -40,12 +40,14 @@ import (
 	"github.com/docker/docker/libcontainerd/supervisor"
 	dopts "github.com/docker/docker/opts"
 	"github.com/docker/docker/pkg/authorization"
+	"github.com/docker/docker/pkg/homedir"
 	"github.com/docker/docker/pkg/jsonmessage"
 	"github.com/docker/docker/pkg/pidfile"
 	"github.com/docker/docker/pkg/plugingetter"
 	"github.com/docker/docker/pkg/signal"
 	"github.com/docker/docker/pkg/system"
 	"github.com/docker/docker/plugin"
+	"github.com/docker/docker/rootless"
 	"github.com/docker/docker/runconfig"
 	"github.com/docker/go-connections/tlsconfig"
 	swarmapi "github.com/docker/swarmkit/api"
@@ -97,6 +99,17 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
 
 	if cli.Config.Experimental {
 		logrus.Warn("Running experimental build")
+		if cli.Config.IsRootless() {
+			logrus.Warn("Running in rootless mode. Cgroups, AppArmor, and CRIU are disabled.")
+		}
+	} else {
+		if cli.Config.IsRootless() {
+			return fmt.Errorf("rootless mode is supported only when running in experimental mode")
+		}
+	}
+	// return human-friendly error before creating files
+	if runtime.GOOS == "linux" && os.Geteuid() != 0 {
+		return fmt.Errorf("dockerd needs to be started with root. To see how to run dockerd in rootless mode with unprivileged user, see the documentation")
 	}
 
 	system.InitLCOW(cli.Config.Experimental)
@@ -115,11 +128,14 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
 		return err
 	}
 
+	potentiallyUnderRuntimeDir := []string{cli.Config.ExecRoot}
+
 	if cli.Pidfile != "" {
 		pf, err := pidfile.New(cli.Pidfile)
 		if err != nil {
 			return errors.Wrap(err, "failed to start daemon")
 		}
+		potentiallyUnderRuntimeDir = append(potentiallyUnderRuntimeDir, cli.Pidfile)
 		defer func() {
 			if err := pf.Remove(); err != nil {
 				logrus.Error(err)
@@ -127,6 +143,12 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
 		}()
 	}
 
+	// Set sticky bit if XDG_RUNTIME_DIR is set && the file is actually under XDG_RUNTIME_DIR
+	if _, err := homedir.StickRuntimeDirContents(potentiallyUnderRuntimeDir); err != nil {
+		// StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset
+		logrus.WithError(err).Warn("cannot set sticky bit on files under XDG_RUNTIME_DIR")
+	}
+
 	serverConfig, err := newAPIServerConfig(cli)
 	if err != nil {
 		return errors.Wrap(err, "failed to create API server")
@@ -140,7 +162,11 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
 
 	ctx, cancel := context.WithCancel(context.Background())
 	if cli.Config.ContainerdAddr == "" && runtime.GOOS != "windows" {
-		if !systemContainerdRunning() {
+		systemContainerdAddr, ok, err := systemContainerdRunning(cli.Config.IsRootless())
+		if err != nil {
+			return errors.Wrap(err, "could not determine whether the system containerd is running")
+		}
+		if !ok {
 			opts, err := cli.getContainerdDaemonOpts()
 			if err != nil {
 				cancel()
@@ -157,7 +183,7 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
 			// Try to wait for containerd to shutdown
 			defer r.WaitTimeout(10 * time.Second)
 		} else {
-			cli.Config.ContainerdAddr = containerddefaults.DefaultAddress
+			cli.Config.ContainerdAddr = systemContainerdAddr
 		}
 	}
 	defer cancel()
@@ -403,9 +429,11 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) {
 	}
 
 	if conf.TrustKeyPath == "" {
-		conf.TrustKeyPath = filepath.Join(
-			getDaemonConfDir(conf.Root),
-			defaultTrustKeyFile)
+		daemonConfDir, err := getDaemonConfDir(conf.Root)
+		if err != nil {
+			return nil, err
+		}
+		conf.TrustKeyPath = filepath.Join(daemonConfDir, defaultTrustKeyFile)
 	}
 
 	if flags.Changed("graph") && flags.Changed("data-root") {
@@ -585,7 +613,7 @@ func loadListeners(cli *DaemonCli, serverConfig *apiserver.Config) ([]string, er
 	var hosts []string
 	for i := 0; i < len(cli.Config.Hosts); i++ {
 		var err error
-		if cli.Config.Hosts[i], err = dopts.ParseHost(cli.Config.TLS, cli.Config.Hosts[i]); err != nil {
+		if cli.Config.Hosts[i], err = dopts.ParseHost(cli.Config.TLS, rootless.RunningWithNonRootUsername(), cli.Config.Hosts[i]); err != nil {
 			return nil, errors.Wrapf(err, "error parsing -H %s", cli.Config.Hosts[i])
 		}
 
@@ -662,9 +690,17 @@ func validateAuthzPlugins(requestedPlugins []string, pg plugingetter.PluginGette
 	return nil
 }
 
-func systemContainerdRunning() bool {
-	_, err := os.Lstat(containerddefaults.DefaultAddress)
-	return err == nil
+func systemContainerdRunning(isRootless bool) (string, bool, error) {
+	addr := containerddefaults.DefaultAddress
+	if isRootless {
+		runtimeDir, err := homedir.GetRuntimeDir()
+		if err != nil {
+			return "", false, err
+		}
+		addr = filepath.Join(runtimeDir, "containerd", "containerd.sock")
+	}
+	_, err := os.Lstat(addr)
+	return addr, err == nil, nil
 }
 
 // configureDaemonLogs sets the logrus logging level and formatting
diff --git a/cmd/dockerd/daemon_test.go b/cmd/dockerd/daemon_test.go
index 38b2d0fb53..fd167654af 100644
--- a/cmd/dockerd/daemon_test.go
+++ b/cmd/dockerd/daemon_test.go
@@ -11,18 +11,22 @@ import (
 	"gotest.tools/fs"
 )
 
-func defaultOptions(configFile string) *daemonOptions {
+func defaultOptions(t *testing.T, configFile string) *daemonOptions {
 	opts := newDaemonOptions(&config.Config{})
 	opts.flags = &pflag.FlagSet{}
 	opts.InstallFlags(opts.flags)
-	installConfigFlags(opts.daemonConfig, opts.flags)
+	if err := installConfigFlags(opts.daemonConfig, opts.flags); err != nil {
+		t.Fatal(err)
+	}
+	defaultDaemonConfigFile, err := getDefaultDaemonConfigFile()
+	assert.NilError(t, err)
 	opts.flags.StringVar(&opts.configFile, "config-file", defaultDaemonConfigFile, "")
 	opts.configFile = configFile
 	return opts
 }
 
 func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) {
-	opts := defaultOptions("")
+	opts := defaultOptions(t, "")
 	opts.Debug = true
 
 	loadedConfig, err := loadDaemonCliConfig(opts)
@@ -34,7 +38,7 @@ func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) {
 }
 
 func TestLoadDaemonCliConfigWithTLS(t *testing.T) {
-	opts := defaultOptions("")
+	opts := defaultOptions(t, "")
 	opts.TLSOptions.CAFile = "/tmp/ca.pem"
 	opts.TLS = true
 
@@ -49,7 +53,7 @@ func TestLoadDaemonCliConfigWithConflicts(t *testing.T) {
 	defer tempFile.Remove()
 	configFile := tempFile.Path()
 
-	opts := defaultOptions(configFile)
+	opts := defaultOptions(t, configFile)
 	flags := opts.flags
 
 	assert.Check(t, flags.Set("config-file", configFile))
@@ -65,7 +69,7 @@ func TestLoadDaemonCliWithConflictingNodeGenericResources(t *testing.T) {
 	defer tempFile.Remove()
 	configFile := tempFile.Path()
 
-	opts := defaultOptions(configFile)
+	opts := defaultOptions(t, configFile)
 	flags := opts.flags
 
 	assert.Check(t, flags.Set("config-file", configFile))
@@ -77,7 +81,7 @@ func TestLoadDaemonCliWithConflictingNodeGenericResources(t *testing.T) {
 }
 
 func TestLoadDaemonCliWithConflictingLabels(t *testing.T) {
-	opts := defaultOptions("")
+	opts := defaultOptions(t, "")
 	flags := opts.flags
 
 	assert.Check(t, flags.Set("label", "foo=bar"))
@@ -88,7 +92,7 @@ func TestLoadDaemonCliWithConflictingLabels(t *testing.T) {
 }
 
 func TestLoadDaemonCliWithDuplicateLabels(t *testing.T) {
-	opts := defaultOptions("")
+	opts := defaultOptions(t, "")
 	flags := opts.flags
 
 	assert.Check(t, flags.Set("label", "foo=the-same"))
@@ -102,7 +106,7 @@ func TestLoadDaemonCliConfigWithTLSVerify(t *testing.T) {
 	tempFile := fs.NewFile(t, "config", fs.WithContent(`{"tlsverify": true}`))
 	defer tempFile.Remove()
 
-	opts := defaultOptions(tempFile.Path())
+	opts := defaultOptions(t, tempFile.Path())
 	opts.TLSOptions.CAFile = "/tmp/ca.pem"
 
 	loadedConfig, err := loadDaemonCliConfig(opts)
@@ -115,7 +119,7 @@ func TestLoadDaemonCliConfigWithExplicitTLSVerifyFalse(t *testing.T) {
 	tempFile := fs.NewFile(t, "config", fs.WithContent(`{"tlsverify": false}`))
 	defer tempFile.Remove()
 
-	opts := defaultOptions(tempFile.Path())
+	opts := defaultOptions(t, tempFile.Path())
 	opts.TLSOptions.CAFile = "/tmp/ca.pem"
 
 	loadedConfig, err := loadDaemonCliConfig(opts)
@@ -128,7 +132,7 @@ func TestLoadDaemonCliConfigWithoutTLSVerify(t *testing.T) {
 	tempFile := fs.NewFile(t, "config", fs.WithContent(`{}`))
 	defer tempFile.Remove()
 
-	opts := defaultOptions(tempFile.Path())
+	opts := defaultOptions(t, tempFile.Path())
 	opts.TLSOptions.CAFile = "/tmp/ca.pem"
 
 	loadedConfig, err := loadDaemonCliConfig(opts)
@@ -141,7 +145,7 @@ func TestLoadDaemonCliConfigWithLogLevel(t *testing.T) {
 	tempFile := fs.NewFile(t, "config", fs.WithContent(`{"log-level": "warn"}`))
 	defer tempFile.Remove()
 
-	opts := defaultOptions(tempFile.Path())
+	opts := defaultOptions(t, tempFile.Path())
 	loadedConfig, err := loadDaemonCliConfig(opts)
 	assert.NilError(t, err)
 	assert.Assert(t, loadedConfig != nil)
@@ -153,7 +157,7 @@ func TestLoadDaemonConfigWithEmbeddedOptions(t *testing.T) {
 	tempFile := fs.NewFile(t, "config", fs.WithContent(content))
 	defer tempFile.Remove()
 
-	opts := defaultOptions(tempFile.Path())
+	opts := defaultOptions(t, tempFile.Path())
 	loadedConfig, err := loadDaemonCliConfig(opts)
 	assert.NilError(t, err)
 	assert.Assert(t, loadedConfig != nil)
@@ -170,7 +174,7 @@ func TestLoadDaemonConfigWithRegistryOptions(t *testing.T) {
 	tempFile := fs.NewFile(t, "config", fs.WithContent(content))
 	defer tempFile.Remove()
 
-	opts := defaultOptions(tempFile.Path())
+	opts := defaultOptions(t, tempFile.Path())
 	loadedConfig, err := loadDaemonCliConfig(opts)
 	assert.NilError(t, err)
 	assert.Assert(t, loadedConfig != nil)
diff --git a/cmd/dockerd/daemon_unix.go b/cmd/dockerd/daemon_unix.go
index 7b03e28594..f6ada580aa 100644
--- a/cmd/dockerd/daemon_unix.go
+++ b/cmd/dockerd/daemon_unix.go
@@ -15,11 +15,33 @@ import (
 	"github.com/docker/docker/daemon"
 	"github.com/docker/docker/daemon/config"
 	"github.com/docker/docker/libcontainerd/supervisor"
+	"github.com/docker/docker/pkg/homedir"
+	"github.com/docker/docker/rootless"
 	"github.com/docker/libnetwork/portallocator"
 	"golang.org/x/sys/unix"
 )
 
-const defaultDaemonConfigFile = "/etc/docker/daemon.json"
+func getDefaultDaemonConfigDir() (string, error) {
+	if !rootless.RunningWithNonRootUsername() {
+		return "/etc/docker", nil
+	}
+	// NOTE: CLI uses ~/.docker while the daemon uses ~/.config/docker, because
+	// ~/.docker was not designed to store daemon configurations.
+	// In future, the daemon directory may be renamed to ~/.config/moby-engine (?).
+	configHome, err := homedir.GetConfigHome()
+	if err != nil {
+		return "", nil
+	}
+	return filepath.Join(configHome, "docker"), nil
+}
+
+func getDefaultDaemonConfigFile() (string, error) {
+	dir, err := getDefaultDaemonConfigDir()
+	if err != nil {
+		return "", err
+	}
+	return filepath.Join(dir, "daemon.json"), nil
+}
 
 // setDefaultUmask sets the umask to 0022 to avoid problems
 // caused by custom umask
@@ -33,8 +55,8 @@ func setDefaultUmask() error {
 	return nil
 }
 
-func getDaemonConfDir(_ string) string {
-	return "/etc/docker"
+func getDaemonConfDir(_ string) (string, error) {
+	return getDefaultDaemonConfigDir()
 }
 
 func (cli *DaemonCli) getPlatformContainerdDaemonOpts() ([]supervisor.DaemonOpt, error) {
diff --git a/cmd/dockerd/daemon_unix_test.go b/cmd/dockerd/daemon_unix_test.go
index 692d0328c4..be8474f040 100644
--- a/cmd/dockerd/daemon_unix_test.go
+++ b/cmd/dockerd/daemon_unix_test.go
@@ -16,7 +16,7 @@ func TestLoadDaemonCliConfigWithDaemonFlags(t *testing.T) {
 	tempFile := fs.NewFile(t, "config", fs.WithContent(content))
 	defer tempFile.Remove()
 
-	opts := defaultOptions(tempFile.Path())
+	opts := defaultOptions(t, tempFile.Path())
 	opts.Debug = true
 	opts.LogLevel = "info"
 	assert.Check(t, opts.flags.Set("selinux-enabled", "true"))
@@ -37,7 +37,7 @@ func TestLoadDaemonConfigWithNetwork(t *testing.T) {
 	tempFile := fs.NewFile(t, "config", fs.WithContent(content))
 	defer tempFile.Remove()
 
-	opts := defaultOptions(tempFile.Path())
+	opts := defaultOptions(t, tempFile.Path())
 	loadedConfig, err := loadDaemonCliConfig(opts)
 	assert.NilError(t, err)
 	assert.Assert(t, loadedConfig != nil)
@@ -54,7 +54,7 @@ func TestLoadDaemonConfigWithMapOptions(t *testing.T) {
 	tempFile := fs.NewFile(t, "config", fs.WithContent(content))
 	defer tempFile.Remove()
 
-	opts := defaultOptions(tempFile.Path())
+	opts := defaultOptions(t, tempFile.Path())
 	loadedConfig, err := loadDaemonCliConfig(opts)
 	assert.NilError(t, err)
 	assert.Assert(t, loadedConfig != nil)
@@ -71,7 +71,7 @@ func TestLoadDaemonConfigWithTrueDefaultValues(t *testing.T) {
 	tempFile := fs.NewFile(t, "config", fs.WithContent(content))
 	defer tempFile.Remove()
 
-	opts := defaultOptions(tempFile.Path())
+	opts := defaultOptions(t, tempFile.Path())
 	loadedConfig, err := loadDaemonCliConfig(opts)
 	assert.NilError(t, err)
 	assert.Assert(t, loadedConfig != nil)
@@ -90,7 +90,7 @@ func TestLoadDaemonConfigWithTrueDefaultValuesLeaveDefaults(t *testing.T) {
 	tempFile := fs.NewFile(t, "config", fs.WithContent(`{}`))
 	defer tempFile.Remove()
 
-	opts := defaultOptions(tempFile.Path())
+	opts := defaultOptions(t, tempFile.Path())
 	loadedConfig, err := loadDaemonCliConfig(opts)
 	assert.NilError(t, err)
 	assert.Assert(t, loadedConfig != nil)
diff --git a/cmd/dockerd/daemon_windows.go b/cmd/dockerd/daemon_windows.go
index 3b9ed9551f..f4d213da96 100644
--- a/cmd/dockerd/daemon_windows.go
+++ b/cmd/dockerd/daemon_windows.go
@@ -12,15 +12,17 @@ import (
 	"golang.org/x/sys/windows"
 )
 
-var defaultDaemonConfigFile = ""
+func getDefaultDaemonConfigFile() (string, error) {
+	return "", nil
+}
 
 // setDefaultUmask doesn't do anything on windows
 func setDefaultUmask() error {
 	return nil
 }
 
-func getDaemonConfDir(root string) string {
-	return filepath.Join(root, `\config`)
+func getDaemonConfDir(root string) (string, error) {
+	return filepath.Join(root, `\config`), nil
 }
 
 // preNotifySystem sends a message to the host when the API is active, but before the daemon is
diff --git a/cmd/dockerd/docker.go b/cmd/dockerd/docker.go
index 6097e2c6bc..a6d3387828 100644
--- a/cmd/dockerd/docker.go
+++ b/cmd/dockerd/docker.go
@@ -16,7 +16,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func newDaemonCommand() *cobra.Command {
+func newDaemonCommand() (*cobra.Command, error) {
 	opts := newDaemonOptions(config.New())
 
 	cmd := &cobra.Command{
@@ -36,12 +36,18 @@ func newDaemonCommand() *cobra.Command {
 
 	flags := cmd.Flags()
 	flags.BoolP("version", "v", false, "Print version information and quit")
+	defaultDaemonConfigFile, err := getDefaultDaemonConfigFile()
+	if err != nil {
+		return nil, err
+	}
 	flags.StringVar(&opts.configFile, "config-file", defaultDaemonConfigFile, "Daemon configuration file")
 	opts.InstallFlags(flags)
-	installConfigFlags(opts.daemonConfig, flags)
+	if err := installConfigFlags(opts.daemonConfig, flags); err != nil {
+		return nil, err
+	}
 	installServiceFlags(flags)
 
-	return cmd
+	return cmd, nil
 }
 
 func init() {
@@ -72,10 +78,17 @@ func main() {
 		logrus.SetOutput(stderr)
 	}
 
-	cmd := newDaemonCommand()
-	cmd.SetOutput(stdout)
-	if err := cmd.Execute(); err != nil {
+	onError := func(err error) {
 		fmt.Fprintf(stderr, "%s\n", err)
 		os.Exit(1)
 	}
+
+	cmd, err := newDaemonCommand()
+	if err != nil {
+		onError(err)
+	}
+	cmd.SetOutput(stdout)
+	if err := cmd.Execute(); err != nil {
+		onError(err)
+	}
 }
diff --git a/cmd/dockerd/options.go b/cmd/dockerd/options.go
index cb5601a768..f6ea21a6dc 100644
--- a/cmd/dockerd/options.go
+++ b/cmd/dockerd/options.go
@@ -49,6 +49,8 @@ func newDaemonOptions(config *config.Config) *daemonOptions {
 // InstallFlags adds flags for the common options on the FlagSet
 func (o *daemonOptions) InstallFlags(flags *pflag.FlagSet) {
 	if dockerCertPath == "" {
+		// cliconfig.Dir returns $DOCKER_CONFIG or ~/.docker.
+		// cliconfig.Dir does not look up $XDG_CONFIG_HOME
 		dockerCertPath = cliconfig.Dir()
 	}
 
diff --git a/contrib/dockerd-rootless.sh b/contrib/dockerd-rootless.sh
new file mode 100755
index 0000000000..ca8f5d439f
--- /dev/null
+++ b/contrib/dockerd-rootless.sh
@@ -0,0 +1,77 @@
+#!/bin/sh
+# dockerd-rootless.sh executes dockerd in rootless mode.
+#
+# Usage: dockerd-rootless.sh --experimental [DOCKERD_OPTIONS]
+# Currently, specifying --experimental is mandatory.
+#
+# External dependencies:
+# * newuidmap and newgidmap needs to be installed.
+# * /etc/subuid and /etc/subgid needs to be configured for the current user.
+# * Either slirp4netns (v0.3+) or VPNKit needs to be installed.
+#
+# See the documentation for the further information.
+
+set -e -x
+if ! [ -w $XDG_RUNTIME_DIR ]; then
+	echo "XDG_RUNTIME_DIR needs to be set and writable"
+	exit 1
+fi
+if ! [ -w $HOME ]; then
+	echo "HOME needs to be set and writable"
+	exit 1
+fi
+
+rootlesskit=""
+for f in docker-rootlesskit rootlesskit; do
+	if which $f >/dev/null 2>&1; then
+		rootlesskit=$f
+		break
+	fi
+done
+if [ -z $rootlesskit ]; then
+	echo "rootlesskit needs to be installed"
+	exit 1
+fi
+
+net=""
+mtu=""
+if which slirp4netns >/dev/null 2>&1; then
+	if slirp4netns --help | grep -- --disable-host-loopback; then
+		net=slirp4netns
+		mtu=65520
+	else
+		echo "slirp4netns does not support --disable-host-loopback. Falling back to VPNKit."
+	fi
+fi
+if [ -z $net ]; then
+	if which vpnkit >/dev/null 2>&1; then
+		net=vpnkit
+		mtu=1500
+	else
+		echo "Either slirp4netns (v0.3+) or vpnkit needs to be installed"
+		exit 1
+	fi
+fi
+
+if [ -z $_DOCKERD_ROOTLESS_CHILD ]; then
+	_DOCKERD_ROOTLESS_CHILD=1
+	export _DOCKERD_ROOTLESS_CHILD
+	# Re-exec the script via RootlessKit, so as to create unprivileged {user,mount,network} namespaces.
+	#
+	# --copy-up allows removing/creating files in the directories by creating tmpfs and symlinks
+	# * /etc: copy-up is required so as to prevent `/etc/resolv.conf` in the
+	#         namespace from being unexpectedly unmounted when `/etc/resolv.conf` is recreated on the host
+	#         (by either systemd-networkd or NetworkManager)
+	# * /run: copy-up is required so that we can create /run/docker (hardcoded for plugins) in our namespace
+	$rootlesskit \
+		--net=$net --mtu=$mtu --disable-host-loopback \
+		--copy-up=/etc --copy-up=/run \
+		$DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS \
+		$0 $@
+else
+	[ $_DOCKERD_ROOTLESS_CHILD = 1 ]
+	# remove the symlinks for the existing files in the parent namespace if any,
+	# so that we can create our own files in our mount namespace.
+	rm -f /run/docker /run/xtables.lock
+	dockerd $@
+fi
diff --git a/daemon/config/config_unix.go b/daemon/config/config_unix.go
index 5ed6abd89e..0ba7e754fb 100644
--- a/daemon/config/config_unix.go
+++ b/daemon/config/config_unix.go
@@ -39,6 +39,7 @@ type Config struct {
 	IpcMode              string                   `json:"default-ipc-mode,omitempty"`
 	// ResolvConf is the path to the configuration of the host resolver
 	ResolvConf string `json:"resolv-conf,omitempty"`
+	Rootless   bool   `json:"rootless,omitempty"`
 }
 
 // BridgeConfig stores all the bridge driver specific
@@ -87,3 +88,8 @@ func verifyDefaultIpcMode(mode string) error {
 func (conf *Config) ValidatePlatformConfig() error {
 	return verifyDefaultIpcMode(conf.IpcMode)
 }
+
+// IsRootless returns conf.Rootless
+func (conf *Config) IsRootless() bool {
+	return conf.Rootless
+}
diff --git a/daemon/config/config_windows.go b/daemon/config/config_windows.go
index 0aa7d54bf2..47624fab46 100644
--- a/daemon/config/config_windows.go
+++ b/daemon/config/config_windows.go
@@ -55,3 +55,8 @@ func (conf *Config) IsSwarmCompatible() error {
 func (conf *Config) ValidatePlatformConfig() error {
 	return nil
 }
+
+// IsRootless returns conf.Rootless on Unix but false on Windows
+func (conf *Config) IsRootless() bool {
+	return false
+}
diff --git a/daemon/daemon.go b/daemon/daemon.go
index 651a57a139..428da80846 100644
--- a/daemon/daemon.go
+++ b/daemon/daemon.go
@@ -800,6 +800,7 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
 		logrus.Warnf("Failed to configure golang's threads limit: %v", err)
 	}
 
+	// ensureDefaultAppArmorProfile does nothing if apparmor is disabled
 	if err := ensureDefaultAppArmorProfile(); err != nil {
 		logrus.Errorf(err.Error())
 	}
diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go
index 817025e93b..f4b18b3ca5 100644
--- a/daemon/daemon_unix.go
+++ b/daemon/daemon_unix.go
@@ -745,9 +745,6 @@ func verifyDaemonSettings(conf *config.Config) error {
 
 // checkSystem validates platform-specific requirements
 func checkSystem() error {
-	if os.Geteuid() != 0 {
-		return fmt.Errorf("The Docker daemon needs to be run as root")
-	}
 	return checkKernel()
 }
 
diff --git a/daemon/info.go b/daemon/info.go
index 7b9a8588a2..b2e4724aee 100644
--- a/daemon/info.go
+++ b/daemon/info.go
@@ -175,6 +175,9 @@ func (daemon *Daemon) fillSecurityOptions(v *types.Info, sysInfo *sysinfo.SysInf
 	if rootIDs := daemon.idMapping.RootPair(); rootIDs.UID != 0 || rootIDs.GID != 0 {
 		securityOptions = append(securityOptions, "name=userns")
 	}
+	if daemon.configStoreRootless() {
+		securityOptions = append(securityOptions, "name=rootless")
+	}
 	v.SecurityOptions = securityOptions
 }
 
diff --git a/daemon/info_unix.go b/daemon/info_unix.go
index 0bc255e41f..b367171bfa 100644
--- a/daemon/info_unix.go
+++ b/daemon/info_unix.go
@@ -245,3 +245,7 @@ func parseRuncVersion(v string) (version string, commit string, err error) {
 	}
 	return version, commit, err
 }
+
+func (daemon *Daemon) configStoreRootless() bool {
+	return daemon.configStore.Rootless
+}
diff --git a/daemon/info_windows.go b/daemon/info_windows.go
index fe9b1e6732..b8611d7f82 100644
--- a/daemon/info_windows.go
+++ b/daemon/info_windows.go
@@ -13,3 +13,7 @@ func (daemon *Daemon) fillPlatformVersion(v *types.Version) {}
 
 func fillDriverWarnings(v *types.Info) {
 }
+
+func (daemon *Daemon) configStoreRootless() bool {
+	return false
+}
diff --git a/daemon/listeners/listeners_linux.go b/daemon/listeners/listeners_linux.go
index c8956db258..9b5b3b7e77 100644
--- a/daemon/listeners/listeners_linux.go
+++ b/daemon/listeners/listeners_linux.go
@@ -8,6 +8,7 @@ import (
 	"strconv"
 
 	"github.com/coreos/go-systemd/activation"
+	"github.com/docker/docker/pkg/homedir"
 	"github.com/docker/go-connections/sockets"
 	"github.com/sirupsen/logrus"
 )
@@ -45,6 +46,10 @@ func Init(proto, addr, socketGroup string, tlsConfig *tls.Config) ([]net.Listene
 		if err != nil {
 			return nil, fmt.Errorf("can't create unix socket %s: %v", addr, err)
 		}
+		if _, err := homedir.StickRuntimeDirContents([]string{addr}); err != nil {
+			// StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset
+			logrus.WithError(err).Warnf("cannot set sticky bit on socket %s under XDG_RUNTIME_DIR", addr)
+		}
 		ls = append(ls, l)
 	default:
 		return nil, fmt.Errorf("invalid protocol format: %q", proto)
diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go
index ca6074cfac..574f29c7d9 100644
--- a/daemon/oci_linux.go
+++ b/daemon/oci_linux.go
@@ -17,10 +17,12 @@ import (
 	"github.com/docker/docker/oci/caps"
 	"github.com/docker/docker/pkg/idtools"
 	"github.com/docker/docker/pkg/mount"
+	"github.com/docker/docker/rootless/specconv"
 	volumemounts "github.com/docker/docker/volume/mounts"
 	"github.com/opencontainers/runc/libcontainer/apparmor"
 	"github.com/opencontainers/runc/libcontainer/cgroups"
 	"github.com/opencontainers/runc/libcontainer/devices"
+	rsystem "github.com/opencontainers/runc/libcontainer/system"
 	"github.com/opencontainers/runc/libcontainer/user"
 	"github.com/opencontainers/runtime-spec/specs-go"
 	"github.com/pkg/errors"
@@ -89,7 +91,7 @@ func setDevices(s *specs.Spec, c *container.Container) error {
 	// Build lists of devices allowed and created within the container.
 	var devs []specs.LinuxDevice
 	devPermissions := s.Linux.Resources.Devices
-	if c.HostConfig.Privileged {
+	if c.HostConfig.Privileged && !rsystem.RunningInUserNS() {
 		hostDevices, err := devices.HostDevices()
 		if err != nil {
 			return err
@@ -867,6 +869,11 @@ func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, e
 		s.Linux.ReadonlyPaths = c.HostConfig.ReadonlyPaths
 	}
 
+	if daemon.configStore.Rootless {
+		if err := specconv.ToRootless(&s); err != nil {
+			return nil, err
+		}
+	}
 	return &s, nil
 }
 
diff --git a/docs/rootless.md b/docs/rootless.md
new file mode 100644
index 0000000000..bb0486b6e2
--- /dev/null
+++ b/docs/rootless.md
@@ -0,0 +1,92 @@
+# Rootless mode (Experimental)
+
+The rootless mode allows running `dockerd` as an unprivileged user, using `user_namespaces(7)`, `mount_namespaces(7)`, `network_namespaces(7)`.
+
+No SETUID/SETCAP binary is required except `newuidmap` and `newgidmap`.
+
+## Requirements
+* `newuidmap` and `newgidmap` need to be installed on the host. These commands are provided by the `uidmap` package on most distros.
+
+* `/etc/subuid` and `/etc/subgid` should contain >= 65536 sub-IDs. e.g. `penguin:231072:65536`.
+
+```console
+$ id -u
+1001
+$ whoami
+penguin
+$ grep ^$(whoami): /etc/subuid
+penguin:231072:65536
+$ grep ^$(whoami): /etc/subgid
+penguin:231072:65536
+```
+
+* Either [slirp4netns](https://github.com/rootless-containers/slirp4netns) (v0.3+) or [VPNKit](https://github.com/moby/vpnkit) needs to be installed. slirp4netns is preferred for the best performance.
+
+### Distribution-specific hint
+
+#### Debian (excluding Ubuntu)
+* `sudo sh -c "echo 1 > /proc/sys/kernel/unprivileged_userns_clone"` is required
+
+#### Arch Linux
+* `sudo sh -c "echo 1 > /proc/sys/kernel/unprivileged_userns_clone"` is required
+
+#### openSUSE
+* `sudo modprobe ip_tables iptable_mangle iptable_nat iptable_filter` is required. (This is likely to be required on other distros as well)
+
+#### RHEL/CentOS 7
+* `sudo sh -c "echo 28633 > /proc/sys/user/max_user_namespaces"` is required
+* [COPR package `vbatts/shadow-utils-newxidmap`](https://copr.fedorainfracloud.org/coprs/vbatts/shadow-utils-newxidmap/) needs to be installed
+
+## Restrictions
+
+* Only `vfs` graphdriver is supported. However, on [Ubuntu](http://kernel.ubuntu.com/git/ubuntu/ubuntu-artful.git/commit/fs/overlayfs?h=Ubuntu-4.13.0-25.29&id=0a414bdc3d01f3b61ed86cfe3ce8b63a9240eba7) and a few distros, `overlay2` and `overlay` are also supported.
+* Following features are not supported:
+  * Cgroups (including `docker top`, which depends on the cgroups device controller)
+  * Apparmor
+  * Checkpoint
+  * Overlay network
+
+## Usage
+
+### Daemon
+
+You need to run `dockerd-rootless.sh` instead of `dockerd`.
+
+```console
+$ dockerd-rootless.sh --experimental"
+```
+As Rootless mode is experimental per se, currently you always need to run `dockerd-rootless.sh` with `--experimental`.
+
+Remarks:
+* The socket path is set to `$XDG_RUNTIME_DIR/docker.sock` by default. `$XDG_RUNTIME_DIR` is typically set to `/run/user/$UID`.
+* The data dir is set to `~/.local/share/docker` by default.
+* The exec dir is set to `$XDG_RUNTIME_DIR/docker` by default.
+* The daemon config dir is set to `~/.config/docker` (not `~/.docker`, which is used by the client) by default.
+* The `dockerd-rootless.sh` script executes `dockerd` in its own user, mount, and network namespaces. You can enter the namespaces by running `nsenter -U --preserve-credentials -n -m -t $(cat $XDG_RUNTIME_DIR/docker.pid)`.
+
+### Client
+
+You can just use the upstream Docker client but you need to set the socket path explicitly.
+
+```console
+$ docker -H unix://$XDG_RUNTIME_DIR/docker.sock run -d nginx
+```
+
+### Exposing ports
+
+In addition to exposing container ports to the `dockerd` network namespace, you also need to expose the ports in the `dockerd` network namespace to the host network namespace.
+
+```console
+$ docker -H unix://$XDG_RUNTIME_DIR/docker.sock run -d -p 80:80 nginx
+$ socat -t -- TCP-LISTEN:8080,reuseaddr,fork EXEC:"nsenter -U -n -t $(cat $XDG_RUNTIME_DIR/docker.pid) socat -t -- STDIN TCP4\:127.0.0.1\:80"
+```
+
+In future, `dockerd` will be able to expose the ports automatically.
+
+### Routing ping packets
+
+To route ping packets, you need to set up `net.ipv4.ping_group_range` properly as the root.
+
+```console
+$ sudo sh -c "echo 0   2147483647  > /proc/sys/net/ipv4/ping_group_range"
+```
diff --git a/hack/dockerfile/install/rootlesskit.installer b/hack/dockerfile/install/rootlesskit.installer
new file mode 100755
index 0000000000..fd6ecef543
--- /dev/null
+++ b/hack/dockerfile/install/rootlesskit.installer
@@ -0,0 +1,34 @@
+#!/bin/sh
+
+# v0.3.0-alpha.0
+ROOTLESSKIT_COMMIT=3c4582e950e3a67795c2832179c125b258b78124
+
+install_rootlesskit() {
+	case "$1" in
+	"dynamic")
+		install_rootlesskit_dynamic
+		return
+		;;
+	"")
+		export CGO_ENABLED=0
+		_install_rootlesskit
+		;;
+	*)
+		echo 'Usage: $0 [dynamic]'
+		;;
+	esac
+}
+
+install_rootlesskit_dynamic() {
+	export ROOTLESSKIT_LDFLAGS="-linkmode=external" install_rootlesskit
+	export BUILD_MODE="-buildmode=pie"
+	_install_rootlesskit
+}
+
+_install_rootlesskit() {
+	echo "Install rootlesskit version $ROOTLESSKIT_COMMIT"
+	git clone https://github.com/rootless-containers/rootlesskit.git "$GOPATH/src/github.com/rootless-containers/rootlesskit"
+	cd "$GOPATH/src/github.com/rootless-containers/rootlesskit"
+	git checkout -q "$ROOTLESSKIT_COMMIT"
+	go build $BUILD_MODE -ldflags="$ROOTLESSKIT_LDFLAGS" -o "${PREFIX}/rootlesskit" github.com/rootless-containers/rootlesskit/cmd/rootlesskit
+}
diff --git a/hack/make/.binary-setup b/hack/make/.binary-setup
index 69bb39b364..43e4b2cdc1 100644
--- a/hack/make/.binary-setup
+++ b/hack/make/.binary-setup
@@ -7,3 +7,5 @@ DOCKER_CONTAINERD_CTR_BINARY_NAME='ctr'
 DOCKER_CONTAINERD_SHIM_BINARY_NAME='containerd-shim'
 DOCKER_PROXY_BINARY_NAME='docker-proxy'
 DOCKER_INIT_BINARY_NAME='docker-init'
+DOCKER_ROOTLESSKIT_BINARY_NAME='rootlesskit'
+DOCKER_DAEMON_ROOTLESS_SH_BINARY_NAME='dockerd-rootless.sh'
diff --git a/hack/make/binary-daemon b/hack/make/binary-daemon
index c1a6e6f9ed..dbf51b1426 100644
--- a/hack/make/binary-daemon
+++ b/hack/make/binary-daemon
@@ -14,7 +14,7 @@ copy_binaries() {
 		return
 	fi
 	echo "Copying nested executables into $dir"
-	for file in containerd containerd-shim ctr runc docker-init docker-proxy; do
+	for file in containerd containerd-shim ctr runc docker-init docker-proxy rootlesskit dockerd-rootless.sh; do
 		cp -f `which "$file"` "$dir/"
 		if [ "$hash" == "hash" ]; then
 			hash_files "$dir/$file"
diff --git a/hack/make/install-binary b/hack/make/install-binary
index f6a4361fdb..4ad2fb1d75 100644
--- a/hack/make/install-binary
+++ b/hack/make/install-binary
@@ -26,4 +26,6 @@ install_binary() {
 	install_binary "${DEST}/${DOCKER_CONTAINERD_SHIM_BINARY_NAME}"
 	install_binary "${DEST}/${DOCKER_PROXY_BINARY_NAME}"
 	install_binary "${DEST}/${DOCKER_INIT_BINARY_NAME}"
+	install_binary "${DEST}/${DOCKER_ROOTLESSKIT_BINARY_NAME}"
+	install_binary "${DEST}/${DOCKER_DAEMON_ROOTLESS_SH_BINARY_NAME}"
 )
diff --git a/opts/hosts.go b/opts/hosts.go
index 2adf4211d5..3d8785f11c 100644
--- a/opts/hosts.go
+++ b/opts/hosts.go
@@ -4,8 +4,11 @@ import (
 	"fmt"
 	"net"
 	"net/url"
+	"path/filepath"
 	"strconv"
 	"strings"
+
+	"github.com/docker/docker/pkg/homedir"
 )
 
 var (
@@ -41,12 +44,20 @@ func ValidateHost(val string) (string, error) {
 	return val, nil
 }
 
-// ParseHost and set defaults for a Daemon host string
-func ParseHost(defaultToTLS bool, val string) (string, error) {
+// ParseHost and set defaults for a Daemon host string.
+// defaultToTLS is preferred over defaultToUnixRootless.
+func ParseHost(defaultToTLS, defaultToUnixRootless bool, val string) (string, error) {
 	host := strings.TrimSpace(val)
 	if host == "" {
 		if defaultToTLS {
 			host = DefaultTLSHost
+		} else if defaultToUnixRootless {
+			runtimeDir, err := homedir.GetRuntimeDir()
+			if err != nil {
+				return "", err
+			}
+			socket := filepath.Join(runtimeDir, "docker.sock")
+			host = "unix://" + socket
 		} else {
 			host = DefaultHost
 		}
diff --git a/opts/hosts_test.go b/opts/hosts_test.go
index e46326a5be..8c54ec0f4b 100644
--- a/opts/hosts_test.go
+++ b/opts/hosts_test.go
@@ -38,13 +38,13 @@ func TestParseHost(t *testing.T) {
 	}
 
 	for _, value := range invalid {
-		if _, err := ParseHost(false, value); err == nil {
+		if _, err := ParseHost(false, false, value); err == nil {
 			t.Errorf("Expected an error for %v, got [nil]", value)
 		}
 	}
 
 	for value, expected := range valid {
-		if actual, err := ParseHost(false, value); err != nil || actual != expected {
+		if actual, err := ParseHost(false, false, value); err != nil || actual != expected {
 			t.Errorf("Expected for %v [%v], got [%v, %v]", value, expected, actual, err)
 		}
 	}
diff --git a/pkg/homedir/homedir_linux.go b/pkg/homedir/homedir_linux.go
index ee15ed52b1..47ecd0c092 100644
--- a/pkg/homedir/homedir_linux.go
+++ b/pkg/homedir/homedir_linux.go
@@ -1,7 +1,10 @@
 package homedir // import "github.com/docker/docker/pkg/homedir"
 
 import (
+	"errors"
 	"os"
+	"path/filepath"
+	"strings"
 
 	"github.com/docker/docker/pkg/idtools"
 )
@@ -19,3 +22,88 @@ func GetStatic() (string, error) {
 	}
 	return usr.Home, nil
 }
+
+// GetRuntimeDir returns XDG_RUNTIME_DIR.
+// XDG_RUNTIME_DIR is typically configured via pam_systemd.
+// GetRuntimeDir returns non-nil error if XDG_RUNTIME_DIR is not set.
+//
+// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
+func GetRuntimeDir() (string, error) {
+	if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" {
+		return xdgRuntimeDir, nil
+	}
+	return "", errors.New("could not get XDG_RUNTIME_DIR")
+}
+
+// StickRuntimeDirContents sets the sticky bit on files that are under
+// XDG_RUNTIME_DIR, so that the files won't be periodically removed by the system.
+//
+// StickyRuntimeDir returns slice of sticked files.
+// StickyRuntimeDir returns nil error if XDG_RUNTIME_DIR is not set.
+//
+// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
+func StickRuntimeDirContents(files []string) ([]string, error) {
+	runtimeDir, err := GetRuntimeDir()
+	if err != nil {
+		// ignore error if runtimeDir is empty
+		return nil, nil
+	}
+	runtimeDir, err = filepath.Abs(runtimeDir)
+	if err != nil {
+		return nil, err
+	}
+	var sticked []string
+	for _, f := range files {
+		f, err = filepath.Abs(f)
+		if err != nil {
+			return sticked, err
+		}
+		if strings.HasPrefix(f, runtimeDir+"/") {
+			if err = stick(f); err != nil {
+				return sticked, err
+			}
+			sticked = append(sticked, f)
+		}
+	}
+	return sticked, nil
+}
+
+func stick(f string) error {
+	st, err := os.Stat(f)
+	if err != nil {
+		return err
+	}
+	m := st.Mode()
+	m |= os.ModeSticky
+	return os.Chmod(f, m)
+}
+
+// GetDataHome returns XDG_DATA_HOME.
+// GetDataHome returns $HOME/.local/share and nil error if XDG_DATA_HOME is not set.
+//
+// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
+func GetDataHome() (string, error) {
+	if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" {
+		return xdgDataHome, nil
+	}
+	home := os.Getenv("HOME")
+	if home == "" {
+		return "", errors.New("could not get either XDG_DATA_HOME or HOME")
+	}
+	return filepath.Join(home, ".local", "share"), nil
+}
+
+// GetConfigHome returns XDG_CONFIG_HOME.
+// GetConfigHome returns $HOME/.config and nil error if XDG_CONFIG_HOME is not set.
+//
+// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
+func GetConfigHome() (string, error) {
+	if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
+		return xdgConfigHome, nil
+	}
+	home := os.Getenv("HOME")
+	if home == "" {
+		return "", errors.New("could not get either XDG_CONFIG_HOME or HOME")
+	}
+	return filepath.Join(home, ".config"), nil
+}
diff --git a/pkg/homedir/homedir_others.go b/pkg/homedir/homedir_others.go
index 75ada2fe54..f0a363dedf 100644
--- a/pkg/homedir/homedir_others.go
+++ b/pkg/homedir/homedir_others.go
@@ -11,3 +11,23 @@ import (
 func GetStatic() (string, error) {
 	return "", errors.New("homedir.GetStatic() is not supported on this system")
 }
+
+// GetRuntimeDir is unsupported on non-linux system.
+func GetRuntimeDir() (string, error) {
+	return "", errors.New("homedir.GetRuntimeDir() is not supported on this system")
+}
+
+// StickRuntimeDirContents is unsupported on non-linux system.
+func StickRuntimeDirContents(files []string) ([]string, error) {
+	return nil, errors.New("homedir.StickRuntimeDirContents() is not supported on this system")
+}
+
+// GetDataHome is unsupported on non-linux system.
+func GetDataHome() (string, error) {
+	return "", errors.New("homedir.GetDataHome() is not supported on this system")
+}
+
+// GetConfigHome is unsupported on non-linux system.
+func GetConfigHome() (string, error) {
+	return "", errors.New("homedir.GetConfigHome() is not supported on this system")
+}
diff --git a/pkg/sysinfo/sysinfo_linux.go b/pkg/sysinfo/sysinfo_linux.go
index 1fcf08310c..040c5a4dd6 100644
--- a/pkg/sysinfo/sysinfo_linux.go
+++ b/pkg/sysinfo/sysinfo_linux.go
@@ -51,7 +51,9 @@ func New(quiet bool) *SysInfo {
 
 	// Check if AppArmor is supported.
 	if _, err := os.Stat("/sys/kernel/security/apparmor"); !os.IsNotExist(err) {
-		sysInfo.AppArmor = true
+		if _, err := ioutil.ReadFile("/sys/kernel/security/apparmor/profiles"); err == nil {
+			sysInfo.AppArmor = true
+		}
 	}
 
 	// Check if Seccomp is supported, via CONFIG_SECCOMP.
diff --git a/rootless/rootless.go b/rootless/rootless.go
new file mode 100644
index 0000000000..d5b07a033d
--- /dev/null
+++ b/rootless/rootless.go
@@ -0,0 +1,26 @@
+package rootless
+
+import (
+	"os"
+	"sync"
+)
+
+var (
+	runningWithNonRootUsername     bool
+	runningWithNonRootUsernameOnce sync.Once
+)
+
+// RunningWithNonRootUsername returns true if we $USER is set to a non-root value,
+// regardless to the UID/EUID value.
+//
+// The value of this variable is mostly used for configuring default paths.
+// If the value is true, $HOME and $XDG_RUNTIME_DIR should be honored for setting up the default paths.
+// If false (not only EUID==0 but also $USER==root), $HOME and $XDG_RUNTIME_DIR should be ignored
+// even if we are in a user namespace.
+func RunningWithNonRootUsername() bool {
+	runningWithNonRootUsernameOnce.Do(func() {
+		u := os.Getenv("USER")
+		runningWithNonRootUsername = u != "" && u != "root"
+	})
+	return runningWithNonRootUsername
+}
diff --git a/rootless/specconv/specconv_linux.go b/rootless/specconv/specconv_linux.go
new file mode 100644
index 0000000000..52f74b9457
--- /dev/null
+++ b/rootless/specconv/specconv_linux.go
@@ -0,0 +1,38 @@
+package specconv
+
+import (
+	"io/ioutil"
+	"strconv"
+
+	"github.com/opencontainers/runtime-spec/specs-go"
+)
+
+// ToRootless converts spec to be compatible with "rootless" runc.
+// * Remove cgroups (will be supported in separate PR when delegation permission is configured)
+// * Fix up OOMScoreAdj
+func ToRootless(spec *specs.Spec) error {
+	return toRootless(spec, getCurrentOOMScoreAdj())
+}
+
+func getCurrentOOMScoreAdj() int {
+	b, err := ioutil.ReadFile("/proc/self/oom_score_adj")
+	if err != nil {
+		return 0
+	}
+	i, err := strconv.Atoi(string(b))
+	if err != nil {
+		return 0
+	}
+	return i
+}
+
+func toRootless(spec *specs.Spec, currentOOMScoreAdj int) error {
+	// Remove cgroup settings.
+	spec.Linux.Resources = nil
+	spec.Linux.CgroupsPath = ""
+
+	if spec.Process.OOMScoreAdj != nil && *spec.Process.OOMScoreAdj < currentOOMScoreAdj {
+		*spec.Process.OOMScoreAdj = currentOOMScoreAdj
+	}
+	return nil
+}