diff --git a/dockersetup.py b/dockersetup.py
index e58cff2..eb02f33 100755
--- a/dockersetup.py
+++ b/dockersetup.py
@@ -30,6 +30,7 @@ if sys.version_info < min_version:
 
 import os
 import argparse
+import functools
 import re
 import subprocess
 import time
@@ -61,7 +62,7 @@ def get_args():
     parser.add_argument('--email-ssl', action="store_true", default=False, help='Use SSL when connecting to email host')
     parser.add_argument('--email-tls', action="store_true", default=False, help='Use TLS when connecting to email host')
     parser.add_argument('-m', '--portmapping', type=str, help='Port mapping in the format HOST:CONTAINER. Default is %(default)s', required=False, default='8080:80,8081:443')
-    parser.add_argument('--project-name', type=str, help='docker-compose project name to use')
+    parser.add_argument('--project-name', type=str, help='Docker Compose project name to use')
     parser.add_argument('--no-https', action="store_true", default=False, help='Disable HTTPS (HTTP only) for web server')
     parser.add_argument('--cert', type=str, help='Existing SSL certificate to use for HTTPS web serving', required=False)
     parser.add_argument('--cert-key', type=str, help='Existing SSL certificate key to use for HTTPS web serving', required=False)
@@ -496,7 +497,7 @@ def setup_https(hostname, http_port, https_port, letsencrypt, letsencrypt_produc
     edit_nginx_ssl_conf(hostname, https_port, container_cert_dir, certfile, keyfile)
 
     if letsencrypt:
-        return_code = subprocess.call(['docker-compose', 'up', '-d', '--build', 'layersweb'], shell=False)
+        return_code = subprocess.call(compose('up', '-d', '--build', 'layersweb'), shell=False)
         if return_code != 0:
             print("docker-compose up layersweb failed")
             sys.exit(1)
@@ -515,7 +516,7 @@ def setup_https(hostname, http_port, https_port, letsencrypt, letsencrypt_produc
             shutil.rmtree(os.path.join(local_cert_dir, 'live'))
 
             # Create a test file and fetch it to ensure web server is working (for http)
-            return_code = subprocess.call("docker-compose exec -T layersweb /bin/sh -c 'mkdir -p /var/www/certbot/.well-known/acme-challenge/ ; echo something > /var/www/certbot/.well-known/acme-challenge/test.txt'", shell=True)
+            return_code = subprocess.call(compose_sh() + " exec -T layersweb /bin/sh -c 'mkdir -p /var/www/certbot/.well-known/acme-challenge/ ; echo something > /var/www/certbot/.well-known/acme-challenge/test.txt'", shell=True)
             if return_code != 0:
                 print("Creating test file failed")
                 sys.exit(1)
@@ -523,7 +524,7 @@ def setup_https(hostname, http_port, https_port, letsencrypt, letsencrypt_produc
             if return_code != 0:
                 print("Reading test file from web server failed")
                 sys.exit(1)
-            return_code = subprocess.call(['docker-compose', 'exec', '-T', 'layersweb', '/bin/sh', '-c', 'rm -rf /var/www/certbot/.well-known'], shell=False)
+            return_code = subprocess.call(compose('exec', '-T', 'layersweb', '/bin/sh', '-c', 'rm -rf /var/www/certbot/.well-known'), shell=False)
             if return_code != 0:
                 print("Removing test file failed")
                 sys.exit(1)
@@ -536,7 +537,7 @@ def setup_https(hostname, http_port, https_port, letsencrypt, letsencrypt_produc
             email_arg = '--email %s' % quote(emailaddr)
         else:
             email_arg = '--register-unsafely-without-email'
-        return_code = subprocess.call('docker-compose run --rm --entrypoint "\
+        return_code = subprocess.call(compose_sh() + ' run --rm --entrypoint "\
   certbot certonly --webroot -w /var/www/certbot \
     %s \
     %s \
@@ -549,7 +550,7 @@ def setup_https(hostname, http_port, https_port, letsencrypt, letsencrypt_produc
             sys.exit(1)
 
         # Stop web server (so it can effectively be restarted with the new certificate)
-        return_code = subprocess.call(['docker-compose', 'stop', 'layersweb'], shell=False)
+        return_code = subprocess.call(compose('stop', 'layersweb'), shell=False)
         if return_code != 0:
             print("docker-compose stop failed")
             sys.exit(1)
@@ -613,7 +614,7 @@ def write_service_env_files(dbpassword, dbapassword, secretkey, rmqpassword, ema
 
 
 def check_connectivity(app_dir='/opt'):
-    return_code = subprocess.call(['docker-compose', 'run', '--rm', 'layersapp', app_dir + '/connectivity_check.sh'], shell=False)
+    return_code = subprocess.call(compose('run', '--rm', 'layersapp', app_dir + '/connectivity_check.sh'), shell=False)
     if return_code != 0:
         print("Connectivity check failed - if you are behind a proxy, please check that you have correctly specified the proxy settings on the command line (see --help for details)")
         sys.exit(1)
@@ -631,6 +632,24 @@ def writefile(filename, data):
         f.write(data)
 
 
+# The Docker Compose command to use: the "docker compose" plugin that current
+# Docker installs provide, or else the older standalone docker-compose
+@functools.lru_cache(maxsize=None)
+def compose_command():
+    try:
+        if subprocess.call(['docker', 'compose', 'version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
+            return ('docker', 'compose')
+    except FileNotFoundError:
+        pass
+    return ('docker-compose',)
+
+def compose(*args):
+    return list(compose_command()) + list(args)
+
+def compose_sh():
+    return ' '.join(compose_command())
+
+
 # Read the database root password of an existing installation, from
 # .env.local.db or, for installations made before it existed, from
 # docker-compose.yml. Returns the development default if neither has one.
@@ -775,7 +794,7 @@ provide information as needed.
                 print('Entered email address is not valid')
 
     if args.reinstall or args.uninstall:
-        return_code = subprocess.call(['docker-compose', 'down', '-v'], shell=False)
+        return_code = subprocess.call(compose('down', '-v'), shell=False)
 
     if args.uninstall:
         # We're done
@@ -807,7 +826,7 @@ provide information as needed.
             setup_https(args.hostname, http_port, https_port, args.letsencrypt, args.letsencrypt_production, args.cert, args.cert_key, emailaddr, args.app_dir)
 
     ## Start up containers
-    return_code = subprocess.call(['docker-compose', 'up', '-d', '--build'], shell=False)
+    return_code = subprocess.call(compose('up', '-d', '--build'), shell=False)
     if return_code != 0:
         print("docker-compose up failed")
         sys.exit(1)
@@ -818,7 +837,7 @@ provide information as needed.
 
     # Get real project name (if only there were a reasonable way to do this... ugh)
     real_project_name = ''
-    output = subprocess.check_output(['docker-compose', 'ps', '-q'], shell=False)
+    output = subprocess.check_output(compose('ps', '-q'), shell=False)
     if output:
         output = output.decode('utf-8')
         for contid in output.splitlines():
@@ -844,7 +863,7 @@ provide information as needed.
         env = os.environ.copy()
         env['MYSQL_PWD'] = dbapassword
         # Dummy command, we just want to establish that the db can be connected to
-        return_code = subprocess.call("echo | docker-compose exec -T -e MYSQL_PWD layersdb mariadb -uroot layersdb", shell=True, env=env)
+        return_code = subprocess.call("echo | " + compose_sh() + " exec -T -e MYSQL_PWD layersdb mariadb -uroot layersdb", shell=True, env=env)
         if return_code == 0:
             break
         else:
@@ -866,7 +885,7 @@ provide information as needed.
                 catcmd = 'cat'
             env = os.environ.copy()
             env['MYSQL_PWD'] = dbapassword
-            return_code = subprocess.call("%s %s | docker-compose exec -T -e MYSQL_PWD layersdb mariadb -uroot layersdb" % (catcmd, quote(args.databasefile)), shell=True, env=env)
+            return_code = subprocess.call("%s %s | %s exec -T -e MYSQL_PWD layersdb mariadb -uroot layersdb" % (catcmd, quote(args.databasefile), compose_sh()), shell=True, env=env)
             if return_code != 0:
                 print("Database import failed")
                 sys.exit(1)
@@ -876,7 +895,7 @@ provide information as needed.
         env = os.environ.copy()
         env['DATABASE_USER'] = 'root'
         env['DATABASE_PASSWORD'] = dbapassword
-        return_code = subprocess.call(['docker-compose', 'run', '--rm', '-e', 'DATABASE_USER', '-e', 'DATABASE_PASSWORD', 'layersapp', args.app_dir + '/migrate.sh'], shell=False, env=env)
+        return_code = subprocess.call(compose('run', '--rm', '-e', 'DATABASE_USER', '-e', 'DATABASE_PASSWORD', 'layersapp', args.app_dir + '/migrate.sh'), shell=False, env=env)
         if return_code != 0:
             print("Applying migrations failed")
             sys.exit(1)
@@ -894,7 +913,7 @@ provide information as needed.
             # (avoids password being visible through ps or /proc/<pid>/cmdline)
             env = os.environ.copy()
             env['MYSQL_PWD'] = dbapassword
-            return_code = subprocess.call("docker-compose exec -T -e MYSQL_PWD layersdb mariadb -uroot layersdb < " + quote(sqlscriptfile), shell=True, env=env)
+            return_code = subprocess.call(compose_sh() + " exec -T -e MYSQL_PWD layersdb mariadb -uroot layersdb < " + quote(sqlscriptfile), shell=True, env=env)
             if return_code != 0:
                 print("Creating database user failed")
                 sys.exit(1)
@@ -916,7 +935,7 @@ provide information as needed.
                 sys.exit(1)
 
     ## Generate static assets. Run this command again to regenerate at any time (when static assets in the code are updated)
-    return_code = subprocess.call("docker-compose run --rm -e STATIC_ROOT=/usr/share/nginx/html -v %s_layersstatic:/usr/share/nginx/html layersapp %s/layerindex/manage.py collectstatic --noinput" % (quote(real_project_name), args.app_dir), shell = True)
+    return_code = subprocess.call(compose_sh() + " run --rm -e STATIC_ROOT=/usr/share/nginx/html -v %s_layersstatic:/usr/share/nginx/html layersapp %s/layerindex/manage.py collectstatic --noinput" % (quote(real_project_name), args.app_dir), shell = True)
     if return_code != 0:
         print("Collecting static files failed")
         sys.exit(1)
@@ -937,12 +956,12 @@ provide information as needed.
     if not args.update:
         if not args.databasefile:
             ## Set site name
-            return_code = subprocess.call(['docker-compose', 'run', '--rm', 'layersapp', args.app_dir + '/layerindex/layerindex/tools/site_name.py', host, 'OpenEmbedded Layer Index'], shell=False)
+            return_code = subprocess.call(compose('run', '--rm', 'layersapp', args.app_dir + '/layerindex/layerindex/tools/site_name.py', host, 'OpenEmbedded Layer Index'), shell=False)
 
         if not args.no_admin_user:
             ## For a fresh database, create an admin account
             print("Creating database superuser. Input user name and password when prompted.")
-            return_code = subprocess.call(['docker-compose', 'run', '--rm', 'layersapp', args.app_dir + '/layerindex/manage.py', 'createsuperuser', '--email', emailaddr], shell=False)
+            return_code = subprocess.call(compose('run', '--rm', 'layersapp', args.app_dir + '/layerindex/manage.py', 'createsuperuser', '--email', emailaddr), shell=False)
             if return_code != 0:
                 print("Creating superuser failed")
                 sys.exit(1)
@@ -953,7 +972,7 @@ provide information as needed.
     else:
         if args.project_name:
             print("")
-            print("NOTE: you may need to use -p %s (or set COMPOSE_PROJECT_NAME=\"%s\" ) if running docker-compose directly in future" % (args.project_name, args.project_name))
+            print("NOTE: you may need to use -p %s (or set COMPOSE_PROJECT_NAME=\"%s\" ) if running %s directly in future" % (args.project_name, args.project_name, compose_sh()))
         print("")
         print("The application should now be accessible at %s://%s" % (protocol, host))
         print("")
diff --git a/tests/test_dockersetup.py b/tests/test_dockersetup.py
index 3a6d8ff..7958946 100644
--- a/tests/test_dockersetup.py
+++ b/tests/test_dockersetup.py
@@ -156,3 +156,28 @@ def test_nginx_ssl_conf_round_trip(workdir):
     assert 'layers.openembedded.org' in (workdir / 'docker' / 'nginx-ssl.conf').read_text()
     assert dockersetup.read_nginx_ssl_conf('/opt/cert') == \
         ('layers.example.com', '8081', '/opt/cert', 'cert.pem', 'key.pem')
+
+
+@pytest.mark.parametrize('result, expected', [
+    (0, ['docker', 'compose', 'up']),
+    (1, ['docker-compose', 'up']),
+    (FileNotFoundError(), ['docker-compose', 'up']),
+])
+def test_compose_command(monkeypatch, result, expected):
+    calls = []
+
+    def fake_call(cmd, **kwargs):
+        calls.append(cmd)
+        if isinstance(result, Exception):
+            raise result
+        return result
+
+    monkeypatch.setattr(dockersetup.subprocess, 'call', fake_call)
+    dockersetup.compose_command.cache_clear()
+    try:
+        assert dockersetup.compose('up') == expected
+        assert dockersetup.compose_sh() == ' '.join(expected[:-1])
+        # Detected once and then reused
+        assert calls == [['docker', 'compose', 'version']]
+    finally:
+        dockersetup.compose_command.cache_clear()
