diff --git a/api/server/router/plugin/backend.go b/api/server/router/plugin/backend.go
index fee78195d6..ab006b2256 100644
--- a/api/server/router/plugin/backend.go
+++ b/api/server/router/plugin/backend.go
@@ -20,5 +20,6 @@ type Backend interface {
 	Privileges(ctx context.Context, ref reference.Named, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error)
 	Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
 	Push(ctx context.Context, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, outStream io.Writer) error
+	Upgrade(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
 	CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *enginetypes.PluginCreateOptions) error
 }
diff --git a/api/server/router/plugin/plugin.go b/api/server/router/plugin/plugin.go
index 9aa82f338c..e4ea9e23bf 100644
--- a/api/server/router/plugin/plugin.go
+++ b/api/server/router/plugin/plugin.go
@@ -32,6 +32,7 @@ func (r *pluginRouter) initRoutes() {
 		router.NewPostRoute("/plugins/{name:.*}/disable", r.disablePlugin),
 		router.Cancellable(router.NewPostRoute("/plugins/pull", r.pullPlugin)),
 		router.Cancellable(router.NewPostRoute("/plugins/{name:.*}/push", r.pushPlugin)),
+		router.Cancellable(router.NewPostRoute("/plugins/{name:.*}/upgrade", r.upgradePlugin)),
 		router.NewPostRoute("/plugins/{name:.*}/set", r.setPlugin),
 		router.NewPostRoute("/plugins/create", r.createPlugin),
 	}
diff --git a/api/server/router/plugin/plugin_routes.go b/api/server/router/plugin/plugin_routes.go
index 2d3eb8fea1..693fa95baf 100644
--- a/api/server/router/plugin/plugin_routes.go
+++ b/api/server/router/plugin/plugin_routes.go
@@ -100,6 +100,45 @@ func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter
 	return httputils.WriteJSON(w, http.StatusOK, privileges)
 }
 
+func (pr *pluginRouter) upgradePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return errors.Wrap(err, "failed to parse form")
+	}
+
+	var privileges types.PluginPrivileges
+	dec := json.NewDecoder(r.Body)
+	if err := dec.Decode(&privileges); err != nil {
+		return errors.Wrap(err, "failed to parse privileges")
+	}
+	if dec.More() {
+		return errors.New("invalid privileges")
+	}
+
+	metaHeaders, authConfig := parseHeaders(r.Header)
+	ref, tag, err := parseRemoteRef(r.FormValue("remote"))
+	if err != nil {
+		return err
+	}
+
+	name, err := getName(ref, tag, vars["name"])
+	if err != nil {
+		return err
+	}
+	w.Header().Set("Docker-Plugin-Name", name)
+
+	w.Header().Set("Content-Type", "application/json")
+	output := ioutils.NewWriteFlusher(w)
+
+	if err := pr.backend.Upgrade(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
+		if !output.Flushed() {
+			return err
+		}
+		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
+	}
+
+	return nil
+}
+
 func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 	if err := httputils.ParseForm(r); err != nil {
 		return errors.Wrap(err, "failed to parse form")
@@ -115,40 +154,14 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
 	}
 
 	metaHeaders, authConfig := parseHeaders(r.Header)
-
 	ref, tag, err := parseRemoteRef(r.FormValue("remote"))
 	if err != nil {
 		return err
 	}
 
-	name := r.FormValue("name")
-	if name == "" {
-		if _, ok := ref.(reference.Canonical); ok {
-			trimmed := reference.TrimNamed(ref)
-			if tag != "" {
-				nt, err := reference.WithTag(trimmed, tag)
-				if err != nil {
-					return err
-				}
-				name = nt.String()
-			} else {
-				name = reference.WithDefaultTag(trimmed).String()
-			}
-		} else {
-			name = ref.String()
-		}
-	} else {
-		localRef, err := reference.ParseNamed(name)
-		if err != nil {
-			return err
-		}
-		if _, ok := localRef.(reference.Canonical); ok {
-			return errors.New("cannot use digest in plugin tag")
-		}
-		if distreference.IsNameOnly(localRef) {
-			// TODO: log change in name to out stream
-			name = reference.WithDefaultTag(localRef).String()
-		}
+	name, err := getName(ref, tag, r.FormValue("name"))
+	if err != nil {
+		return err
 	}
 	w.Header().Set("Docker-Plugin-Name", name)
 
@@ -165,6 +178,38 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
 	return nil
 }
 
+func getName(ref reference.Named, tag, name string) (string, error) {
+	if name == "" {
+		if _, ok := ref.(reference.Canonical); ok {
+			trimmed := reference.TrimNamed(ref)
+			if tag != "" {
+				nt, err := reference.WithTag(trimmed, tag)
+				if err != nil {
+					return "", err
+				}
+				name = nt.String()
+			} else {
+				name = reference.WithDefaultTag(trimmed).String()
+			}
+		} else {
+			name = ref.String()
+		}
+	} else {
+		localRef, err := reference.ParseNamed(name)
+		if err != nil {
+			return "", err
+		}
+		if _, ok := localRef.(reference.Canonical); ok {
+			return "", errors.New("cannot use digest in plugin tag")
+		}
+		if distreference.IsNameOnly(localRef) {
+			// TODO: log change in name to out stream
+			name = reference.WithDefaultTag(localRef).String()
+		}
+	}
+	return name, nil
+}
+
 func (pr *pluginRouter) createPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 	if err := httputils.ParseForm(r); err != nil {
 		return err
diff --git a/api/swagger.yaml b/api/swagger.yaml
index dfa463cf12..7aeba0aa59 100644
--- a/api/swagger.yaml
+++ b/api/swagger.yaml
@@ -1379,6 +1379,10 @@ definitions:
             type: "array"
             items:
               $ref: "#/definitions/PluginDevice"
+      PluginReference:
+        description: "plugin remote reference used to push/pull the plugin"
+        type: "string"
+        x-nullable: false
       Config:
         description: "The config of a plugin."
         type: "object"
diff --git a/api/types/plugin.go b/api/types/plugin.go
index 46f47be26f..6cc7a23b02 100644
--- a/api/types/plugin.go
+++ b/api/types/plugin.go
@@ -22,6 +22,9 @@ type Plugin struct {
 	// Required: true
 	Name string `json:"Name"`
 
+	// plugin remote reference used to push/pull the plugin
+	PluginReference string `json:"PluginReference,omitempty"`
+
 	// settings
 	// Required: true
 	Settings PluginSettings `json:"Settings"`
diff --git a/cli/command/plugin/cmd.go b/cli/command/plugin/cmd.go
index 2173943f89..92c990a975 100644
--- a/cli/command/plugin/cmd.go
+++ b/cli/command/plugin/cmd.go
@@ -25,6 +25,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command {
 		newSetCommand(dockerCli),
 		newPushCommand(dockerCli),
 		newCreateCommand(dockerCli),
+		newUpgradeCommand(dockerCli),
 	)
 	return cmd
 }
diff --git a/cli/command/plugin/install.go b/cli/command/plugin/install.go
index a64dc2525a..2c3170c54a 100644
--- a/cli/command/plugin/install.go
+++ b/cli/command/plugin/install.go
@@ -16,15 +16,22 @@ import (
 	"github.com/docker/docker/reference"
 	"github.com/docker/docker/registry"
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 	"golang.org/x/net/context"
 )
 
 type pluginOptions struct {
-	name       string
-	alias      string
-	grantPerms bool
-	disable    bool
-	args       []string
+	remote          string
+	localName       string
+	grantPerms      bool
+	disable         bool
+	args            []string
+	skipRemoteCheck bool
+}
+
+func loadPullFlags(opts *pluginOptions, flags *pflag.FlagSet) {
+	flags.BoolVar(&opts.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
+	command.AddTrustedFlags(flags, true)
 }
 
 func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
@@ -34,7 +41,7 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
 		Short: "Install a plugin",
 		Args:  cli.RequiresMinArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
-			options.name = args[0]
+			options.remote = args[0]
 			if len(args) > 1 {
 				options.args = args[1:]
 			}
@@ -43,12 +50,9 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
 	}
 
 	flags := cmd.Flags()
-	flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
+	loadPullFlags(&options, flags)
 	flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install")
-	flags.StringVar(&options.alias, "alias", "", "Local name for plugin")
-
-	command.AddTrustedFlags(flags, true)
-
+	flags.StringVar(&options.localName, "alias", "", "Local name for plugin")
 	return cmd
 }
 
@@ -84,60 +88,48 @@ func newRegistryService() registry.Service {
 	}
 }
 
-func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
+func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts pluginOptions, cmdName string) (types.PluginInstallOptions, error) {
 	// Parse name using distribution reference package to support name
 	// containing both tag and digest. Names with both tag and digest
 	// will be treated by the daemon as a pull by digest with
 	// an alias for the tag (if no alias is provided).
-	ref, err := distreference.ParseNamed(opts.name)
+	ref, err := distreference.ParseNamed(opts.remote)
 	if err != nil {
-		return err
+		return types.PluginInstallOptions{}, err
 	}
 
-	alias := ""
-	if opts.alias != "" {
-		aref, err := reference.ParseNamed(opts.alias)
-		if err != nil {
-			return err
-		}
-		aref = reference.WithDefaultTag(aref)
-		if _, ok := aref.(reference.NamedTagged); !ok {
-			return fmt.Errorf("invalid name: %s", opts.alias)
-		}
-		alias = aref.String()
-	}
-	ctx := context.Background()
-
 	index, err := getRepoIndexFromUnnormalizedRef(ref)
 	if err != nil {
-		return err
+		return types.PluginInstallOptions{}, err
 	}
 
+	repoInfoIndex, err := getRepoIndexFromUnnormalizedRef(ref)
+	if err != nil {
+		return types.PluginInstallOptions{}, err
+	}
 	remote := ref.String()
 
 	_, isCanonical := ref.(distreference.Canonical)
 	if command.IsTrusted() && !isCanonical {
-		if alias == "" {
-			alias = ref.String()
-		}
 		var nt reference.NamedTagged
 		named, err := reference.ParseNamed(ref.Name())
 		if err != nil {
-			return err
+			return types.PluginInstallOptions{}, err
 		}
 		if tagged, ok := ref.(distreference.Tagged); ok {
 			nt, err = reference.WithTag(named, tagged.Tag())
 			if err != nil {
-				return err
+				return types.PluginInstallOptions{}, err
 			}
 		} else {
 			named = reference.WithDefaultTag(named)
 			nt = named.(reference.NamedTagged)
 		}
 
+		ctx := context.Background()
 		trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService())
 		if err != nil {
-			return err
+			return types.PluginInstallOptions{}, err
 		}
 		remote = trusted.String()
 	}
@@ -146,23 +138,44 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
 
 	encodedAuth, err := command.EncodeAuthToBase64(authConfig)
 	if err != nil {
-		return err
+		return types.PluginInstallOptions{}, err
 	}
 
-	registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, index, "plugin install")
+	registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfoIndex, cmdName)
 
 	options := types.PluginInstallOptions{
 		RegistryAuth:          encodedAuth,
 		RemoteRef:             remote,
 		Disabled:              opts.disable,
 		AcceptAllPermissions:  opts.grantPerms,
-		AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name),
+		AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.remote),
 		// TODO: Rename PrivilegeFunc, it has nothing to do with privileges
 		PrivilegeFunc: registryAuthFunc,
 		Args:          opts.args,
 	}
+	return options, nil
+}
 
-	responseBody, err := dockerCli.Client().PluginInstall(ctx, alias, options)
+func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
+	var localName string
+	if opts.localName != "" {
+		aref, err := reference.ParseNamed(opts.localName)
+		if err != nil {
+			return err
+		}
+		aref = reference.WithDefaultTag(aref)
+		if _, ok := aref.(reference.NamedTagged); !ok {
+			return fmt.Errorf("invalid name: %s", opts.localName)
+		}
+		localName = aref.String()
+	}
+
+	ctx := context.Background()
+	options, err := buildPullConfig(ctx, dockerCli, opts, "plugin install")
+	if err != nil {
+		return err
+	}
+	responseBody, err := dockerCli.Client().PluginInstall(ctx, localName, options)
 	if err != nil {
 		if strings.Contains(err.Error(), "target is image") {
 			return errors.New(err.Error() + " - Use `docker image pull`")
@@ -173,7 +186,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
 	if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
 		return err
 	}
-	fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.name) // todo: return proper values from the API for this result
+	fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.remote) // todo: return proper values from the API for this result
 	return nil
 }
 
diff --git a/cli/command/plugin/upgrade.go b/cli/command/plugin/upgrade.go
new file mode 100644
index 0000000000..d212cd7e52
--- /dev/null
+++ b/cli/command/plugin/upgrade.go
@@ -0,0 +1,100 @@
+package plugin
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/pkg/jsonmessage"
+	"github.com/docker/docker/reference"
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+)
+
+func newUpgradeCommand(dockerCli *command.DockerCli) *cobra.Command {
+	var options pluginOptions
+	cmd := &cobra.Command{
+		Use:   "upgrade [OPTIONS] PLUGIN [REMOTE]",
+		Short: "Upgrade an existing plugin",
+		Args:  cli.RequiresRangeArgs(1, 2),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			options.localName = args[0]
+			if len(args) == 2 {
+				options.remote = args[1]
+			}
+			return runUpgrade(dockerCli, options)
+		},
+	}
+
+	flags := cmd.Flags()
+	loadPullFlags(&options, flags)
+	flags.BoolVar(&options.skipRemoteCheck, "skip-remote-check", false, "Do not check if specified remote plugin matches existing plugin image")
+	return cmd
+}
+
+func runUpgrade(dockerCli *command.DockerCli, opts pluginOptions) error {
+	ctx := context.Background()
+	p, _, err := dockerCli.Client().PluginInspectWithRaw(ctx, opts.localName)
+	if err != nil {
+		return fmt.Errorf("error reading plugin data: %v", err)
+	}
+
+	if p.Enabled {
+		return fmt.Errorf("the plugin must be disabled before upgrading")
+	}
+
+	opts.localName = p.Name
+	if opts.remote == "" {
+		opts.remote = p.PluginReference
+	}
+	remote, err := reference.ParseNamed(opts.remote)
+	if err != nil {
+		return errors.Wrap(err, "error parsing remote upgrade image reference")
+	}
+	remote = reference.WithDefaultTag(remote)
+
+	old, err := reference.ParseNamed(p.PluginReference)
+	if err != nil {
+		return errors.Wrap(err, "error parsing current image reference")
+	}
+	old = reference.WithDefaultTag(old)
+
+	fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, old, remote)
+	if !opts.skipRemoteCheck && remote.String() != old.String() {
+		_, err := fmt.Fprint(dockerCli.Out(), "Plugin images do not match, are you sure? ")
+		if err != nil {
+			return errors.Wrap(err, "error writing to stdout")
+		}
+
+		rdr := bufio.NewReader(dockerCli.In())
+		line, _, err := rdr.ReadLine()
+		if err != nil {
+			return errors.Wrap(err, "error reading from stdin")
+		}
+		if strings.ToLower(string(line)) != "y" {
+			return errors.New("canceling upgrade request")
+		}
+	}
+
+	options, err := buildPullConfig(ctx, dockerCli, opts, "plugin upgrade")
+	if err != nil {
+		return err
+	}
+
+	responseBody, err := dockerCli.Client().PluginUpgrade(ctx, opts.localName, options)
+	if err != nil {
+		if strings.Contains(err.Error(), "target is image") {
+			return errors.New(err.Error() + " - Use `docker image pull`")
+		}
+		return err
+	}
+	defer responseBody.Close()
+	if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
+		return err
+	}
+	fmt.Fprintf(dockerCli.Out(), "Upgraded plugin %s to %s\n", opts.localName, opts.remote) // todo: return proper values from the API for this result
+	return nil
+}
diff --git a/client/interface.go b/client/interface.go
index 924b22bc04..05978039b7 100644
--- a/client/interface.go
+++ b/client/interface.go
@@ -112,6 +112,7 @@ type PluginAPIClient interface {
 	PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error
 	PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error
 	PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
+	PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
 	PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error)
 	PluginSet(ctx context.Context, name string, args []string) error
 	PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error)
diff --git a/client/plugin_install.go b/client/plugin_install.go
index b305780cfb..3217c4cf39 100644
--- a/client/plugin_install.go
+++ b/client/plugin_install.go
@@ -20,43 +20,15 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types
 	}
 	query.Set("remote", options.RemoteRef)
 
-	resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
-	if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
-		// todo: do inspect before to check existing name before checking privileges
-		newAuthHeader, privilegeErr := options.PrivilegeFunc()
-		if privilegeErr != nil {
-			ensureReaderClosed(resp)
-			return nil, privilegeErr
-		}
-		options.RegistryAuth = newAuthHeader
-		resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
-	}
+	privileges, err := cli.checkPluginPermissions(ctx, query, options)
 	if err != nil {
-		ensureReaderClosed(resp)
 		return nil, err
 	}
 
-	var privileges types.PluginPrivileges
-	if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
-		ensureReaderClosed(resp)
-		return nil, err
-	}
-	ensureReaderClosed(resp)
-
-	if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
-		accept, err := options.AcceptPermissionsFunc(privileges)
-		if err != nil {
-			return nil, err
-		}
-		if !accept {
-			return nil, pluginPermissionDenied{options.RemoteRef}
-		}
-	}
-
 	// set name for plugin pull, if empty should default to remote reference
 	query.Set("name", name)
 
-	resp, err = cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth)
+	resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth)
 	if err != nil {
 		return nil, err
 	}
@@ -103,3 +75,39 @@ func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileg
 	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
 	return cli.post(ctx, "/plugins/pull", query, privileges, headers)
 }
+
+func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options types.PluginInstallOptions) (types.PluginPrivileges, error) {
+	resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
+	if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
+		// todo: do inspect before to check existing name before checking privileges
+		newAuthHeader, privilegeErr := options.PrivilegeFunc()
+		if privilegeErr != nil {
+			ensureReaderClosed(resp)
+			return nil, privilegeErr
+		}
+		options.RegistryAuth = newAuthHeader
+		resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
+	}
+	if err != nil {
+		ensureReaderClosed(resp)
+		return nil, err
+	}
+
+	var privileges types.PluginPrivileges
+	if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
+		ensureReaderClosed(resp)
+		return nil, err
+	}
+	ensureReaderClosed(resp)
+
+	if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
+		accept, err := options.AcceptPermissionsFunc(privileges)
+		if err != nil {
+			return nil, err
+		}
+		if !accept {
+			return nil, pluginPermissionDenied{options.RemoteRef}
+		}
+	}
+	return privileges, nil
+}
diff --git a/client/plugin_upgrade.go b/client/plugin_upgrade.go
new file mode 100644
index 0000000000..95a4356b97
--- /dev/null
+++ b/client/plugin_upgrade.go
@@ -0,0 +1,37 @@
+package client
+
+import (
+	"fmt"
+	"io"
+	"net/url"
+
+	"github.com/docker/distribution/reference"
+	"github.com/docker/docker/api/types"
+	"github.com/pkg/errors"
+	"golang.org/x/net/context"
+)
+
+// PluginUpgrade upgrades a plugin
+func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) {
+	query := url.Values{}
+	if _, err := reference.ParseNamed(options.RemoteRef); err != nil {
+		return nil, errors.Wrap(err, "invalid remote reference")
+	}
+	query.Set("remote", options.RemoteRef)
+
+	privileges, err := cli.checkPluginPermissions(ctx, query, options)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := cli.tryPluginUpgrade(ctx, query, privileges, name, options.RegistryAuth)
+	if err != nil {
+		return nil, err
+	}
+	return resp.body, nil
+}
+
+func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privileges types.PluginPrivileges, name, registryAuth string) (serverResponse, error) {
+	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
+	return cli.post(ctx, fmt.Sprintf("/plugins/%s/upgrade", name), query, privileges, headers)
+}
diff --git a/docs/reference/commandline/plugin_create.md b/docs/reference/commandline/plugin_create.md
index f1593f05f4..9d4e99e56a 100644
--- a/docs/reference/commandline/plugin_create.md
+++ b/docs/reference/commandline/plugin_create.md
@@ -57,3 +57,4 @@ The plugin can subsequently be enabled for local use or pushed to the public reg
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)
diff --git a/docs/reference/commandline/plugin_disable.md b/docs/reference/commandline/plugin_disable.md
index e8d16c4253..451f1ace9c 100644
--- a/docs/reference/commandline/plugin_disable.md
+++ b/docs/reference/commandline/plugin_disable.md
@@ -63,3 +63,4 @@ ID                  NAME                             TAG                 DESCRIP
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)
diff --git a/docs/reference/commandline/plugin_enable.md b/docs/reference/commandline/plugin_enable.md
index 060592ef07..df8bee3af5 100644
--- a/docs/reference/commandline/plugin_enable.md
+++ b/docs/reference/commandline/plugin_enable.md
@@ -62,3 +62,4 @@ ID                  NAME                             TAG                 DESCRIP
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)
diff --git a/docs/reference/commandline/plugin_inspect.md b/docs/reference/commandline/plugin_inspect.md
index d40cd40e75..fdcc030c43 100644
--- a/docs/reference/commandline/plugin_inspect.md
+++ b/docs/reference/commandline/plugin_inspect.md
@@ -37,6 +37,7 @@ $ docker plugin inspect tiborvass/sample-volume-plugin:latest
 {
   "Id": "8c74c978c434745c3ade82f1bc0acf38d04990eaf494fa507c16d9f1daa99c21",
   "Name": "tiborvass/sample-volume-plugin:latest",
+  "PluginReference": "tiborvas/sample-volume-plugin:latest",
   "Enabled": true,
   "Config": {
     "Mounts": [
@@ -160,3 +161,4 @@ $ docker plugin inspect -f '{{.Id}}' tiborvass/sample-volume-plugin:latest
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)
diff --git a/docs/reference/commandline/plugin_install.md b/docs/reference/commandline/plugin_install.md
index 78dd23825f..0601193ce0 100644
--- a/docs/reference/commandline/plugin_install.md
+++ b/docs/reference/commandline/plugin_install.md
@@ -68,3 +68,4 @@ ID                  NAME                  TAG                 DESCRIPTION
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)
diff --git a/docs/reference/commandline/plugin_ls.md b/docs/reference/commandline/plugin_ls.md
index e436213ecc..7a3426d95f 100644
--- a/docs/reference/commandline/plugin_ls.md
+++ b/docs/reference/commandline/plugin_ls.md
@@ -50,3 +50,4 @@ ID                  NAME                             TAG                 DESCRIP
 * [plugin push](plugin_push.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)
diff --git a/docs/reference/commandline/plugin_push.md b/docs/reference/commandline/plugin_push.md
index f869e43f92..e61d10994c 100644
--- a/docs/reference/commandline/plugin_push.md
+++ b/docs/reference/commandline/plugin_push.md
@@ -47,3 +47,4 @@ $ docker plugin push user/plugin
 * [plugin ls](plugin_ls.md)
 * [plugin rm](plugin_rm.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)
diff --git a/docs/reference/commandline/plugin_rm.md b/docs/reference/commandline/plugin_rm.md
index 31029324b6..323ce83f3c 100644
--- a/docs/reference/commandline/plugin_rm.md
+++ b/docs/reference/commandline/plugin_rm.md
@@ -53,3 +53,4 @@ tiborvass/sample-volume-plugin
 * [plugin ls](plugin_ls.md)
 * [plugin push](plugin_push.md)
 * [plugin set](plugin_set.md)
+* [plugin upgrade](plugin_upgrade.md)
diff --git a/docs/reference/commandline/plugin_upgrade.md b/docs/reference/commandline/plugin_upgrade.md
new file mode 100644
index 0000000000..20efc577aa
--- /dev/null
+++ b/docs/reference/commandline/plugin_upgrade.md
@@ -0,0 +1,84 @@
+---
+title: "plugin upgrade"
+description: "the plugin upgrade command description and usage"
+keywords: "plugin, upgrade"
+---
+
+<!-- This file is maintained within the docker/docker Github
+     repository at https://github.com/docker/docker/. Make all
+     pull requests against that repo. If you see this file in
+     another repository, consider it read-only there, as it will
+     periodically be overwritten by the definitive file. Pull
+     requests which include edits to this file in other repositories
+     will be rejected.
+-->
+
+# plugin upgrade
+
+```markdown
+Usage:  docker plugin upgrade [OPTIONS] PLUGIN [REMOTE]
+
+Upgrade a plugin
+
+Options:
+      --disable-content-trust   Skip image verification (default true)
+      --grant-all-permissions   Grant all permissions necessary to run the plugin
+      --help                    Print usage
+      --skip-remote-check       Do not check if specified remote plugin matches existing plugin image
+```
+
+Upgrades an existing plugin to the specified remote plugin image. If no remote
+is specified, Docker will re-pull the current image and use the updated version.
+All existing references to the plugin will continue to work.
+The plugin must be disabled before running the upgrade.
+
+The following example installs `vieus/sshfs` plugin, uses it to create and use
+a volume, then upgrades the plugin.
+
+```bash
+$ docker plugin install vieux/sshfs DEBUG=1
+
+Plugin "vieux/sshfs:next" is requesting the following privileges:
+ - network: [host]
+ - device: [/dev/fuse]
+ - capabilities: [CAP_SYS_ADMIN]
+Do you grant the above permissions? [y/N] y
+vieux/sshfs:next
+
+$ docker volume create -d vieux/sshfs:next -o sshcmd=root@1.2.3.4:/tmp/shared -o password=XXX sshvolume
+sshvolume
+$ docker run -it -v sshvolume:/data alpine sh -c "touch /data/hello"
+$ docker plugin disable -f vieux/sshfs:next
+viex/sshfs:next
+
+# Here docker volume ls doesn't show 'sshfsvolume', since the plugin is disabled
+$ docker volume ls
+DRIVER              VOLUME NAME
+
+$ docker plugin upgrade vieux/sshfs:next vieux/sshfs:next
+Plugin "vieux/sshfs:next" is requesting the following privileges:
+ - network: [host]
+ - device: [/dev/fuse]
+ - capabilities: [CAP_SYS_ADMIN]
+Do you grant the above permissions? [y/N] y
+Upgrade plugin vieux/sshfs:next to vieux/sshfs:next
+$ docker plugin enable vieux/sshfs:next
+viex/sshfs:next
+$ docker volume ls
+DRIVER              VOLUME NAME
+viuex/sshfs:next    sshvolume
+$ docker run -it -v sshvolume:/data alpine sh -c "ls /data"
+hello
+```
+
+## Related information
+
+* [plugin create](plugin_create.md)
+* [plugin disable](plugin_disable.md)
+* [plugin enable](plugin_enable.md)
+* [plugin inspect](plugin_inspect.md)
+* [plugin install](plugin_install.md)
+* [plugin ls](plugin_ls.md)
+* [plugin push](plugin_push.md)
+* [plugin rm](plugin_rm.md)
+* [plugin set](plugin_set.md)
diff --git a/integration-cli/docker_cli_plugins_test.go b/integration-cli/docker_cli_plugins_test.go
index fd8f1a11fa..6376da78c1 100644
--- a/integration-cli/docker_cli_plugins_test.go
+++ b/integration-cli/docker_cli_plugins_test.go
@@ -359,3 +359,30 @@ func (s *DockerTrustSuite) TestPluginUntrustedInstall(c *check.C) {
 	c.Assert(err, check.NotNil, check.Commentf(out))
 	c.Assert(string(out), checker.Contains, "Error: remote trust data does not exist", check.Commentf(out))
 }
+
+func (s *DockerSuite) TestPluginUpgrade(c *check.C) {
+	testRequires(c, DaemonIsLinux, Network, SameHostDaemon, IsAmd64)
+	plugin := "cpuguy83/docker-volume-driver-plugin-local:latest"
+	pluginV2 := "cpuguy83/docker-volume-driver-plugin-local:v2"
+
+	dockerCmd(c, "plugin", "install", "--grant-all-permissions", plugin)
+	out, _, err := dockerCmdWithError("plugin", "upgrade", "--grant-all-permissions", plugin, pluginV2)
+	c.Assert(err, checker.NotNil, check.Commentf(out))
+	c.Assert(out, checker.Contains, "disabled before upgrading")
+
+	out, _ = dockerCmd(c, "plugin", "inspect", "--format={{.ID}}", plugin)
+	id := strings.TrimSpace(out)
+
+	// make sure "v2" does not exists
+	_, err = os.Stat(filepath.Join(dockerBasePath, "plugins", id, "rootfs", "v2"))
+	c.Assert(os.IsNotExist(err), checker.True, check.Commentf(out))
+
+	dockerCmd(c, "plugin", "disable", plugin)
+	dockerCmd(c, "plugin", "upgrade", "--grant-all-permissions", "--skip-remote-check", plugin, pluginV2)
+
+	// make sure "v2" file exists
+	_, err = os.Stat(filepath.Join(dockerBasePath, "plugins", id, "rootfs", "v2"))
+	c.Assert(err, checker.IsNil)
+
+	dockerCmd(c, "plugin", "enable", plugin)
+}
diff --git a/plugin/backend_linux.go b/plugin/backend_linux.go
index 3426eba08f..97046ee185 100644
--- a/plugin/backend_linux.go
+++ b/plugin/backend_linux.go
@@ -209,6 +209,60 @@ func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHead
 	return computePrivileges(config)
 }
 
+// Upgrade upgrades a plugin
+func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) {
+	p, err := pm.config.Store.GetV2Plugin(name)
+	if err != nil {
+		return errors.Wrap(err, "plugin must be installed before upgrading")
+	}
+
+	if p.IsEnabled() {
+		return fmt.Errorf("plugin must be disabled before upgrading")
+	}
+
+	pm.muGC.RLock()
+	defer pm.muGC.RUnlock()
+
+	// revalidate because Pull is public
+	nameref, err := reference.ParseNamed(name)
+	if err != nil {
+		return errors.Wrapf(err, "failed to parse %q", name)
+	}
+	name = reference.WithDefaultTag(nameref).String()
+
+	tmpRootFSDir, err := ioutil.TempDir(pm.tmpDir(), ".rootfs")
+	defer os.RemoveAll(tmpRootFSDir)
+
+	dm := &downloadManager{
+		tmpDir:    tmpRootFSDir,
+		blobStore: pm.blobStore,
+	}
+
+	pluginPullConfig := &distribution.ImagePullConfig{
+		Config: distribution.Config{
+			MetaHeaders:      metaHeader,
+			AuthConfig:       authConfig,
+			RegistryService:  pm.config.RegistryService,
+			ImageEventLogger: pm.config.LogPluginEvent,
+			ImageStore:       dm,
+		},
+		DownloadManager: dm, // todo: reevaluate if possible to substitute distribution/xfer dependencies instead
+		Schema2Types:    distribution.PluginTypes,
+	}
+
+	err = pm.pull(ctx, ref, pluginPullConfig, outStream)
+	if err != nil {
+		go pm.GC()
+		return err
+	}
+
+	if err := pm.upgradePlugin(p, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
+		return err
+	}
+	p.PluginObj.PluginReference = ref.String()
+	return nil
+}
+
 // Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
 func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) {
 	pm.muGC.RLock()
@@ -251,9 +305,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
 		return err
 	}
 
-	if _, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
+	p, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges)
+	if err != nil {
 		return err
 	}
+	p.PluginObj.PluginReference = ref.String()
 
 	return nil
 }
@@ -536,7 +592,8 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
 	if _, ok := ref.(reference.Canonical); ok {
 		return errors.Errorf("canonical references are not permitted")
 	}
-	name := reference.WithDefaultTag(ref).String()
+	taggedRef := reference.WithDefaultTag(ref)
+	name := taggedRef.String()
 
 	if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin()
 		return err
@@ -618,6 +675,7 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
 	if err != nil {
 		return err
 	}
+	p.PluginObj.PluginReference = taggedRef.String()
 
 	pm.config.LogPluginEvent(p.PluginObj.ID, name, "create")
 
diff --git a/plugin/backend_unsupported.go b/plugin/backend_unsupported.go
index becb361fe2..66e6dab9e8 100644
--- a/plugin/backend_unsupported.go
+++ b/plugin/backend_unsupported.go
@@ -39,6 +39,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
 	return errNotSupported
 }
 
+// Upgrade pulls a plugin, check if the correct privileges are provided and install the plugin.
+func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) error {
+	return errNotSupported
+}
+
 // List displays the list of plugins and associated metadata.
 func (pm *Manager) List() ([]types.Plugin, error) {
 	return nil, errNotSupported
diff --git a/plugin/manager_linux.go b/plugin/manager_linux.go
index 4e3c98de30..f13b43c415 100644
--- a/plugin/manager_linux.go
+++ b/plugin/manager_linux.go
@@ -149,37 +149,91 @@ func (pm *Manager) Shutdown() {
 	}
 }
 
-// createPlugin creates a new plugin. take lock before calling.
-func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges) (p *v2.Plugin, err error) {
-	if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
-		return nil, err
+func (pm *Manager) upgradePlugin(p *v2.Plugin, configDigest digest.Digest, blobsums []digest.Digest, tmpRootFSDir string, privileges *types.PluginPrivileges) (err error) {
+	config, err := pm.setupNewPlugin(configDigest, blobsums, privileges)
+	if err != nil {
+		return err
 	}
 
+	pdir := filepath.Join(pm.config.Root, p.PluginObj.ID)
+	orig := filepath.Join(pdir, "rootfs")
+	backup := orig + "-old"
+	if err := os.Rename(orig, backup); err != nil {
+		return err
+	}
+
+	defer func() {
+		if err != nil {
+			if rmErr := os.RemoveAll(orig); rmErr != nil && !os.IsNotExist(rmErr) {
+				logrus.WithError(rmErr).WithField("dir", backup).Error("error cleaning up after failed upgrade")
+				return
+			}
+
+			if err := os.Rename(backup, orig); err != nil {
+				err = errors.Wrap(err, "error restoring old plugin root on upgrade failure")
+			}
+			if rmErr := os.RemoveAll(tmpRootFSDir); rmErr != nil && !os.IsNotExist(rmErr) {
+				logrus.WithError(rmErr).WithField("plugin", p.Name()).Errorf("error cleaning up plugin upgrade dir: %s", tmpRootFSDir)
+			}
+		} else {
+			if rmErr := os.RemoveAll(backup); rmErr != nil && !os.IsNotExist(rmErr) {
+				logrus.WithError(rmErr).WithField("dir", backup).Error("error cleaning up old plugin root after successful upgrade")
+			}
+
+			p.Config = configDigest
+			p.Blobsums = blobsums
+		}
+	}()
+
+	if err := os.Rename(tmpRootFSDir, orig); err != nil {
+		return errors.Wrap(err, "error upgrading")
+	}
+
+	p.PluginObj.Config = config
+	err = pm.save(p)
+	return errors.Wrap(err, "error saving upgraded plugin config")
+}
+
+func (pm *Manager) setupNewPlugin(configDigest digest.Digest, blobsums []digest.Digest, privileges *types.PluginPrivileges) (types.PluginConfig, error) {
 	configRC, err := pm.blobStore.Get(configDigest)
 	if err != nil {
-		return nil, err
+		return types.PluginConfig{}, err
 	}
 	defer configRC.Close()
 
 	var config types.PluginConfig
 	dec := json.NewDecoder(configRC)
 	if err := dec.Decode(&config); err != nil {
-		return nil, errors.Wrapf(err, "failed to parse config")
+		return types.PluginConfig{}, errors.Wrapf(err, "failed to parse config")
 	}
 	if dec.More() {
-		return nil, errors.New("invalid config json")
+		return types.PluginConfig{}, errors.New("invalid config json")
 	}
 
 	requiredPrivileges, err := computePrivileges(config)
 	if err != nil {
-		return nil, err
+		return types.PluginConfig{}, err
 	}
 	if privileges != nil {
 		if err := validatePrivileges(requiredPrivileges, *privileges); err != nil {
-			return nil, err
+			return types.PluginConfig{}, err
 		}
 	}
 
+	return config, nil
+}
+
+// createPlugin creates a new plugin. take lock before calling.
+func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges) (p *v2.Plugin, err error) {
+	if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
+		return nil, err
+	}
+
+	config, err := pm.setupNewPlugin(configDigest, blobsums, privileges)
+	if err != nil {
+		return nil, err
+	}
+
 	p = &v2.Plugin{
 		PluginObj: types.Plugin{
 			Name:   name,