diff --git a/.gitignore b/.gitignore
index 3cc39c1e3..52a6791f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,4 @@ _dist
 workflow-cli
 coverage.txt
 .DS_Store
+.idea/*
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 6eb00bfae..cfe581915 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -99,6 +99,11 @@ type Commander interface {
 	PrintErr(...interface{}) (int, error)
 	PrintErrf(string, ...interface{}) (int, error)
 	Version(bool) error
+	VolumesCreate(string, string, string) error
+	VolumesDelete(string, string) error
+	VolumesList(string, int) error
+	VolumesMount(string, string, []string) error
+	VolumesUnmount(string, string, []string) error
 }
 
 // DryccCmd is an implementation of Commander.
diff --git a/cmd/volumes.go b/cmd/volumes.go
new file mode 100644
index 000000000..79e3a0b68
--- /dev/null
+++ b/cmd/volumes.go
@@ -0,0 +1,175 @@
+package cmd
+
+import (
+	"fmt"
+	"io"
+	"regexp"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/volumes"
+)
+
+// VolumesList list volumes in the application
+func (d *DryccCmd) VolumesList(appID string, results int) error {
+	s, appID, err := load(d.ConfigFile, appID)
+
+	if err != nil {
+		return err
+	}
+
+	if results == defaultLimit {
+		results = s.Limit
+	}
+	fmt.Println("-----app-----", appID)
+	volumes, count, err := volumes.List(s.Client, appID, results)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	if count == 0 {
+		d.Println("Could not find any volume")
+	} else {
+		printVolumes(d, appID, volumes, d.WOut)
+	}
+	return nil
+}
+
+// printVolumes format volume data
+func printVolumes(d *DryccCmd, appID string, volumes api.Volumes, wOut io.Writer) {
+	//volumes := ps.ByType(input)
+
+	fmt.Fprintf(wOut, "=== %s Volumes\n", appID)
+
+	for _, volume := range volumes {
+		fmt.Fprintf(wOut, "--- %s %s\n", volume.Name, volume.Size)
+		for k, v := range volume.Path {
+			fmt.Println(k, v)
+		}
+	}
+}
+
+// VolumesCreate create a volume for the application
+func (d *DryccCmd) VolumesCreate(appID, name string, size string) error {
+	s, appID, err := load(d.ConfigFile, appID)
+
+	if err != nil {
+		return err
+	}
+
+	d.Printf("Creating %s to %s... ", name, appID)
+
+	quit := progress(d.WOut)
+	volume := api.Volume{
+		Name: name,
+		Size: size,
+	}
+	_, err = volumes.Create(s.Client, appID, volume)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Println("done")
+	return nil
+}
+
+// VolumesDelete delete a volume from the application
+func (d *DryccCmd) VolumesDelete(appID, name string) error {
+	s, appID, err := load(d.ConfigFile, appID)
+
+	if err != nil {
+		return err
+	}
+
+	d.Printf("Deleting %s from %s... ", name, appID)
+
+	quit := progress(d.WOut)
+	err = volumes.Delete(s.Client, appID, name)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Println("done")
+	return nil
+}
+
+// VolumesMount mount a volume to process of the application
+func (d *DryccCmd) VolumesMount(appID string, name string, volumeVars []string) error {
+	s, appID, err := load(d.ConfigFile, appID)
+
+	if err != nil {
+		return err
+	}
+
+	volumeMap, err := parseVolume(volumeVars)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Mounting volume... ")
+
+	quit := progress(d.WOut)
+	volumeObj := api.Volume{Path: volumeMap}
+	_, err = volumes.Mount(s.Client, appID, name, volumeObj)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+
+	return nil
+}
+
+// VolumesUnmount unmount a volume from process of the application
+func (d *DryccCmd) VolumesUnmount(appID string, name string, volumeVars []string) error {
+	s, appID, err := load(d.ConfigFile, appID)
+
+	if err != nil {
+		return err
+	}
+
+	// volumeMap, err := parseVolume(volumeVars)
+	valuesMap := make(map[string]interface{})
+	for _, volumeVar := range volumeVars {
+		valuesMap[volumeVar] = nil
+	}
+	if err != nil {
+		return err
+	}
+
+	d.Print("Unmounting volume... ")
+
+	quit := progress(d.WOut)
+	volumeObj := api.Volume{Path: valuesMap}
+	_, err = volumes.Mount(s.Client, appID, name, volumeObj)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+
+	return nil
+}
+
+func parseVolume(volumeVars []string) (map[string]interface{}, error) {
+	volumeMap := make(map[string]interface{})
+
+	regex := regexp.MustCompile(`^([A-z_]+[A-z0-9_]*)=([\s\S]*)$`)
+	for _, volume := range volumeVars {
+		if regex.MatchString(volume) {
+			captures := regex.FindStringSubmatch(volume)
+			volumeMap[captures[1]] = captures[2]
+		} else {
+			return nil, fmt.Errorf("'%s' does not match the pattern 'key=var', ex: MODE=test", volume)
+		}
+	}
+
+	return volumeMap, nil
+}
diff --git a/cmd/volumes_test.go b/cmd/volumes_test.go
new file mode 100644
index 000000000..762e6ea2d
--- /dev/null
+++ b/cmd/volumes_test.go
@@ -0,0 +1,183 @@
+package cmd
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/arschles/assert"
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/workflow-cli/pkg/testutil"
+)
+
+func TestVolumesList(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/example-go/volumes/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+    "count": 1,
+    "next": null,
+    "previous": null,
+    "results": [
+		{
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+			"owner": "test",
+			"app": "example-go",
+			"name": "myvolume",
+			"size": "500M",
+			"path": {},
+			"created": "2020-08-26T00:00:00UTC",
+			"updated": "2020-08-26T00:00:00UTC"
+		}
+    ]
+}`)
+	})
+
+	err = cmdr.VolumesList("example-go", -1)
+	assert.NoErr(t, err)
+
+	assert.Equal(t, b.String(), `=== example-go Volumes
+--- myvolume 500M
+`, "output")
+}
+
+func TestVolumesCreate(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/example-go/volumes/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.AssertBody(t, api.Volume{Name: "myvolume", Size: "500M"}, r)
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusCreated)
+		// Body isn't used by CLI, so it isn't set.
+		w.Write([]byte("{}"))
+	})
+
+	err = cmdr.VolumesCreate("example-go", "myvolume", "500M")
+	assert.NoErr(t, err)
+
+	assert.Equal(t, testutil.StripProgress(b.String()), "Creating myvolume to example-go... done\n", "output")
+}
+
+func TestVolumesDelete(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	err = cmdr.VolumesDelete("example-go", "myvolume")
+	assert.NoErr(t, err)
+
+	assert.Equal(t, testutil.StripProgress(b.String()), "Deleting myvolume from example-go... done\n", "output")
+}
+
+func TestVolumesMount(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+
+	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/path/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		if r.Method == "PATCH" {
+			testutil.AssertBody(t, api.Volume{
+				Path: map[string]interface{}{
+					"cmd": "/data/cmd1",
+				},
+			}, r)
+		}
+		fmt.Fprintf(w, `{
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+			"owner": "test",
+			"app": "example-go",
+			"name": "myvolume",
+			"size": "500M",
+			"path": {"cmd": "/data/cmd1"},
+			"created": "2020-08-26T00:00:00UTC",
+			"updated": "2020-08-26T00:00:00UTC"
+}`)
+	})
+
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	err = cmdr.VolumesMount("example-go", "myvolume", []string{"cmd=/data/cmd1"})
+	assert.NoErr(t, err)
+
+	assert.Equal(t, testutil.StripProgress(b.String()), `Mounting volume... done
+
+`, "output")
+	//
+	//=== example-go Volumes
+	//--- myvolume 500M
+	//cmd /data/cmd1
+}
+
+func TestVolumesUnmount(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+
+	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/path/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		if r.Method == "PATCH" {
+			testutil.AssertBody(t, api.Volume{
+				Path: map[string]interface{}{
+					"cmd": nil,
+				},
+			}, r)
+		}
+		fmt.Fprintf(w, `{
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+			"owner": "test",
+			"app": "example-go",
+			"name": "myvolume",
+			"size": "500M",
+			"path": {},
+			"created": "2020-08-26T00:00:00UTC",
+			"updated": "2020-08-26T00:00:00UTC"
+}`)
+	})
+
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	err = cmdr.VolumesUnmount("example-go", "myvolume", []string{"cmd"})
+	assert.NoErr(t, err)
+
+	assert.Equal(t, testutil.StripProgress(b.String()), `Unmounting volume... done
+
+`, "output")
+
+	//=== example-go Volumes
+	//--- myvolume 500M
+}
diff --git a/drycc.go b/drycc.go
index 7f9fc315a..b41f988dc 100644
--- a/drycc.go
+++ b/drycc.go
@@ -160,6 +160,8 @@ Use 'git push drycc master' to deploy to an application.
 		err = parser.Version(argv, &cmdr)
 	case "whitelist":
 		err = parser.Whitelist(argv, &cmdr)
+	case "volumes":
+		err = parser.Volumes(argv, &cmdr)
 	default:
 		env := os.Environ()
 
diff --git a/parser/volumes.go b/parser/volumes.go
new file mode 100644
index 000000000..c817471d8
--- /dev/null
+++ b/parser/volumes.go
@@ -0,0 +1,194 @@
+package parser
+
+import (
+	docopt "github.com/docopt/docopt-go"
+	"github.com/drycc/workflow-cli/cmd"
+)
+
+// Volumes commands to their specific function.
+func Volumes(argv []string, cmdr cmd.Commander) error {
+	usage := `
+Valid commands for volumes:
+
+volumes:create           create a volume for the application
+volumes:list             list volumes in the application
+volumes:delete           delete a volume from the application
+volumes:mount            mount a volume to process of the application
+volumes:unmount          unmount a volume from process of the application
+
+Use 'drycc help [command]' to learn more.
+`
+
+	switch argv[0] {
+	case "volumes:create":
+		return volumesCreate(argv, cmdr)
+	case "volumes:list":
+		return volumesList(argv, cmdr)
+	case "volumes:delete":
+		return volumesDelete(argv, cmdr)
+	case "volumes:mount":
+		return volumesMount(argv, cmdr)
+	case "volumes:unmount":
+		return volumesUnmount(argv, cmdr)
+	default:
+		if printHelp(argv, usage) {
+			return nil
+		}
+
+		if argv[0] == "volumes" {
+			argv[0] = "volumes:list"
+			return volumesList(argv, cmdr)
+		}
+
+		PrintUsage(cmdr)
+		return nil
+	}
+}
+
+func volumesCreate(argv []string, cmdr cmd.Commander) error {
+	usage := `
+Create a volume for the application.
+
+Usage: drycc volumes:create <name> <size> [options]
+
+Arguments:
+  <name>
+    the volume name.
+  <size>
+    the volume size, such as '500M'.
+
+Options:
+  -a --app=<app>
+    the uniquely identifiable name for the application.
+`
+
+	args, err := docopt.Parse(usage, argv, true, "", false, true)
+
+	if err != nil {
+		return err
+	}
+
+	app := safeGetValue(args, "--app")
+	name := safeGetValue(args, "<name>")
+	size := safeGetValue(args, "<size>")
+
+	return cmdr.VolumesCreate(app, name, size)
+}
+
+func volumesList(argv []string, cmdr cmd.Commander) error {
+	usage := `
+List volumes in the application.
+
+Usage: drycc volumes:list [options]
+
+Options:
+  -a --app=<app>
+    the uniquely identifiable name for the application.
+  -l --limit=<num>
+    the maximum number of results to display, defaults to config setting
+`
+
+	args, err := docopt.Parse(usage, argv, true, "", false, true)
+
+	if err != nil {
+		return err
+	}
+
+	results, err := responseLimit(safeGetValue(args, "--limit"))
+
+	if err != nil {
+		return err
+	}
+	app := safeGetValue(args, "--app")
+
+	return cmdr.VolumesList(app, results)
+}
+
+func volumesDelete(argv []string, cmdr cmd.Commander) error {
+
+	usage := `
+Delete a volume from the application.
+
+Usage: drycc volumes:delete <name> [options]
+
+Arguments:
+  <name>
+    the volume name to be removed.
+
+Options:
+  -a --app=<app>
+    the uniquely identifiable name for the application.
+`
+
+	args, err := docopt.Parse(usage, argv, true, "", false, true)
+
+	if err != nil {
+		return err
+	}
+
+	app := safeGetValue(args, "--app")
+	name := safeGetValue(args, "<name>")
+
+	return cmdr.VolumesDelete(app, name)
+}
+
+func volumesMount(argv []string, cmdr cmd.Commander) error {
+	usage := `
+Mount a volume for an application.
+
+Usage: drycc volumes:mount <name> <type>=<path>... [options]
+
+Arguments:
+  <name>
+    the volume name.
+  <type>
+    the process name as defined in your Procfile, such as 'web' or 'worker'.
+    Note that Dockerfile apps have a default 'cmd' process type.
+  <path>
+    the filesystem path.
+
+Options:
+  -a --app=<app>
+    the uniquely identifiable name for the application.
+`
+
+	args, err := docopt.Parse(usage, argv, true, "", false, true)
+
+	if err != nil {
+		return err
+	}
+
+	app := safeGetValue(args, "--app")
+	name := safeGetValue(args, "<name>")
+
+	return cmdr.VolumesMount(app, name, args["<type>=<path>"].([]string))
+}
+
+func volumesUnmount(argv []string, cmdr cmd.Commander) error {
+	usage := `
+Unmount a volume for an application.
+
+Usage: drycc volumes:unmount <name> <type>... [options]
+
+Arguments:
+  <name>
+    the volume name.
+  <type>
+    the process name as defined in your Procfile, such as 'web' or 'worker'.
+    Note that Dockerfile apps have a default 'cmd' process type.
+
+Options:
+  -a --app=<app>
+    the uniquely identifiable name for the application.
+`
+	args, err := docopt.Parse(usage, argv, true, "", false, true)
+
+	if err != nil {
+		return err
+	}
+
+	app := safeGetValue(args, "--app")
+	name := safeGetValue(args, "<name>")
+
+	return cmdr.VolumesUnmount(app, name, args["<type>"].([]string))
+}
diff --git a/parser/volumes_test.go b/parser/volumes_test.go
new file mode 100644
index 000000000..4659d9135
--- /dev/null
+++ b/parser/volumes_test.go
@@ -0,0 +1,90 @@
+package parser
+
+import (
+	"bytes"
+	"errors"
+	"testing"
+
+	"github.com/arschles/assert"
+	"github.com/drycc/workflow-cli/pkg/testutil"
+)
+
+// Create fake implementations of each method that return the argument
+// we expect to have called the function (as an error to satisfy the interface).
+
+func (d FakeDryccCmd) VolumesList(string, int) error {
+	return errors.New("volumes:list")
+}
+
+func (d FakeDryccCmd) VolumesCreate(string, string, string) error {
+	return errors.New("volumes:create")
+}
+
+func (d FakeDryccCmd) VolumesDelete(string, string) error {
+	return errors.New("volumes:delete")
+}
+
+func (d FakeDryccCmd) VolumesMount(string, string, []string) error {
+	return errors.New("volumes:mount")
+}
+
+func (d FakeDryccCmd) VolumesUnmount(string, string, []string) error {
+	return errors.New("volumes:unmount")
+}
+
+func TestVolumes(t *testing.T) {
+	t.Parallel()
+
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
+
+	// cases defines the arguments and expected return of the call.
+	// if expected is "", it defaults to args[0].
+	cases := []struct {
+		args     []string
+		expected string
+	}{
+		{
+			args:     []string{"volumes:create", "myvolume", "500M"},
+			expected: "",
+		},
+		{
+			args:     []string{"volumes:list"},
+			expected: "",
+		},
+		{
+			args:     []string{"volumes:delete", "myvolume"},
+			expected: "",
+		},
+		{
+			args:     []string{"volumes:mount", "myvolume", "cmd=data/cmd1"},
+			expected: "",
+		},
+		{
+			args:     []string{"volumes:unmount", "myvolume", "cmd"},
+			expected: "",
+		},
+		{
+			args:     []string{"volumes"},
+			expected: "volumes:list",
+		},
+	}
+
+	// For each case, check that calling the route with the arguments
+	// returns the expected error, which is args[0] if not provided.
+	for _, c := range cases {
+		var expected string
+		if c.expected == "" {
+			expected = c.args[0]
+		} else {
+			expected = c.expected
+		}
+		err = Volumes(c.args, cmdr)
+		assert.Err(t, errors.New(expected), err)
+	}
+}
