diff mbox series

[yocto-patches,layerindex-web,v3,1/4] layerindex: Add app dir support for configurable container

Message ID 20260609164032.1415124-2-sandeep.gundlupet-raju@amd.com
State Accepted, archived
Commit 67183015756fa364aadc32b6b0b4a52dcb33437f
Delegated to: Tim Orling
Headers show
Series Add app dir support | expand

Commit Message

Sandeep Gundlupet Raju June 9, 2026, 4:40 p.m. UTC
Add a new --app-dir command-line argument (default: /opt) to allow the
base directory used inside the containers to be configurable instead of
being hardcoded to /opt.

Changes:
- dockersetup.py: add --app-dir argument; write APP_DIR to .env file via
  new write_env_file() so docker-compose passes it as a build arg; update
  edit_dockercompose() to expand build: block and inject APP_DIR build arg
  reference, and rewrite volume mounts and celery --workdir accordingly;
  propagate app_dir through setup_https(), check_connectivity(), and all
  direct docker-compose run call sites
- Dockerfile: add ARG APP_DIR=/opt + ENV APP_DIR=${APP_DIR}; replace all
  hardcoded /opt paths with ${APP_DIR} in COPY, RUN and CMD instructions
- docker/migrate.sh: replace /opt with ${APP_DIR:-/opt}
- docker/refreshlayers.sh: replace /opt with ${APP_DIR:-/opt}
- docker/updatelayers.sh: replace /opt with ${APP_DIR:-/opt}
- Pass app_dir to edit_settings_py() and replace the hardcoded
  LAYER_FETCH_DIR = "/opt/workdir" with app_dir + "/workdir", so
  the fetch directory respects the --app-dir option.

AI-Generated: GitHub Copilot (Claude Sonnet 4.6)

Signed-off-by: Sandeep Gundlupet Raju <sandeep.gundlupet-raju@amd.com>
---
 Dockerfile              | 24 ++++++------
 docker/migrate.sh       |  2 +-
 docker/refreshlayers.sh |  2 +-
 docker/updatelayers.sh  |  2 +-
 dockersetup.py          | 86 +++++++++++++++++++++++++++++++++--------
 5 files changed, 85 insertions(+), 31 deletions(-)

Comments

Tim Orling June 10, 2026, 6:51 p.m. UTC | #1
merged.

On Tue, Jun 9, 2026 at 9:40 AM Sandeep Gundlupet Raju <
sandeep.gundlupet-raju@amd.com> wrote:

> Add a new --app-dir command-line argument (default: /opt) to allow the
> base directory used inside the containers to be configurable instead of
> being hardcoded to /opt.
>
> Changes:
> - dockersetup.py: add --app-dir argument; write APP_DIR to .env file via
>   new write_env_file() so docker-compose passes it as a build arg; update
>   edit_dockercompose() to expand build: block and inject APP_DIR build arg
>   reference, and rewrite volume mounts and celery --workdir accordingly;
>   propagate app_dir through setup_https(), check_connectivity(), and all
>   direct docker-compose run call sites
> - Dockerfile: add ARG APP_DIR=/opt + ENV APP_DIR=${APP_DIR}; replace all
>   hardcoded /opt paths with ${APP_DIR} in COPY, RUN and CMD instructions
> - docker/migrate.sh: replace /opt with ${APP_DIR:-/opt}
> - docker/refreshlayers.sh: replace /opt with ${APP_DIR:-/opt}
> - docker/updatelayers.sh: replace /opt with ${APP_DIR:-/opt}
> - Pass app_dir to edit_settings_py() and replace the hardcoded
>   LAYER_FETCH_DIR = "/opt/workdir" with app_dir + "/workdir", so
>   the fetch directory respects the --app-dir option.
>
> AI-Generated: GitHub Copilot (Claude Sonnet 4.6)
>
> Signed-off-by: Sandeep Gundlupet Raju <sandeep.gundlupet-raju@amd.com>
> ---
>  Dockerfile              | 24 ++++++------
>  docker/migrate.sh       |  2 +-
>  docker/refreshlayers.sh |  2 +-
>  docker/updatelayers.sh  |  2 +-
>  dockersetup.py          | 86 +++++++++++++++++++++++++++++++++--------
>  5 files changed, 85 insertions(+), 31 deletions(-)
>
> diff --git a/Dockerfile b/Dockerfile
> index 72f57d2..8e6e508 100644
> --- a/Dockerfile
> +++ b/Dockerfile
> @@ -3,6 +3,8 @@
>  FROM ubuntu:jammy
>  LABEL maintainer="Michael Halstead <mhalstead@linuxfoundation.org>"
>
> +ARG APP_DIR=/opt
> +ENV APP_DIR=${APP_DIR}
>  ENV PYTHONUNBUFFERED=1 \
>      LANGUAGE=en_US \
>      LANG=en_US.UTF-8 \
> @@ -55,22 +57,22 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update \
>         && rm -rf /var/lib/apt/lists/* \
>         && apt-get clean
>
> -COPY . /opt/layerindex
> -RUN rm -rf /opt/layerindex/docker
> -COPY docker/settings.py /opt/layerindex/settings.py
> -COPY docker/refreshlayers.sh /opt/refreshlayers.sh
> -COPY docker/updatelayers.sh /opt/updatelayers.sh
> -COPY docker/migrate.sh /opt/migrate.sh
> -COPY docker/connectivity_check.sh /opt/connectivity_check.sh
> +COPY . ${APP_DIR}/layerindex
> +RUN rm -rf ${APP_DIR}/layerindex/docker
> +COPY docker/settings.py ${APP_DIR}/layerindex/settings.py
> +COPY docker/refreshlayers.sh ${APP_DIR}/refreshlayers.sh
> +COPY docker/updatelayers.sh ${APP_DIR}/updatelayers.sh
> +COPY docker/migrate.sh ${APP_DIR}/migrate.sh
> +COPY docker/connectivity_check.sh ${APP_DIR}/connectivity_check.sh
>
> -RUN mkdir /opt/workdir \
> +RUN mkdir ${APP_DIR}/workdir \
>         && adduser --system --uid=500 layers \
> -       && chown -R layers /opt/workdir
> +       && chown -R layers ${APP_DIR}/workdir
>  USER layers
>
>  # Always copy in .gitconfig and proxy helper script (they need editing to
> be active)
>  COPY docker/.gitconfig /home/layers/.gitconfig
> -COPY docker/git-proxy /opt/bin/git-proxy
> +COPY docker/git-proxy ${APP_DIR}/bin/git-proxy
>
>  # Start Gunicorn
> -CMD ["/usr/local/bin/gunicorn", "wsgi:application", "--workers=4",
> "--bind=:5000", "--timeout=60", "--log-level=debug",
> "--chdir=/opt/layerindex"]
> +CMD ["/bin/sh", "-c", "/usr/local/bin/gunicorn wsgi:application
> --workers=4 --bind=:5000 --timeout=60 --log-level=debug
> --chdir=${APP_DIR}/layerindex"]
> diff --git a/docker/migrate.sh b/docker/migrate.sh
> index ca15368..bb9f646 100755
> --- a/docker/migrate.sh
> +++ b/docker/migrate.sh
> @@ -1,2 +1,2 @@
>  #!/bin/bash
> -python3 /opt/layerindex/manage.py migrate "$@"
> +python3 ${APP_DIR:-/opt}/layerindex/manage.py migrate "$@"
> diff --git a/docker/refreshlayers.sh b/docker/refreshlayers.sh
> index 0c550bd..5adaf86 100755
> --- a/docker/refreshlayers.sh
> +++ b/docker/refreshlayers.sh
> @@ -1,3 +1,3 @@
>  #!/bin/bash
> -update=/opt/layerindex/layerindex/update.py
> +update=${APP_DIR:-/opt}/layerindex/layerindex/update.py
>  $update -q -r
> diff --git a/docker/updatelayers.sh b/docker/updatelayers.sh
> index 21640ba..6f0e8f0 100755
> --- a/docker/updatelayers.sh
> +++ b/docker/updatelayers.sh
> @@ -1,3 +1,3 @@
>  #!/bin/bash
> -update=/opt/layerindex/layerindex/update.py
> +update=${APP_DIR:-/opt}/layerindex/layerindex/update.py
>  $update -q
> diff --git a/dockersetup.py b/dockersetup.py
> index bc7478c..3a74eb3 100755
> --- a/dockersetup.py
> +++ b/dockersetup.py
> @@ -70,6 +70,7 @@ def get_args():
>      parser.add_argument('--no-migrate', action="store_true",
> default=False, help='Skip running database migrations')
>      parser.add_argument('--no-admin-user', action="store_true",
> default=False, help='Skip adding admin user')
>      parser.add_argument('--no-connectivity', action="store_true",
> default=False, help='Skip checking external network connectivity')
> +    parser.add_argument('--app-dir', type=str, help='Base directory
> inside the container for application files. Default is %(default)s',
> required=False, default='/opt')
>
>      args = parser.parse_args()
>
> @@ -253,14 +254,16 @@ def yaml_comment(line):
>
>
>  # Add hostname, secret key, db info, and email host in docker-compose.yml
> -def edit_dockercompose(hostname, dbpassword, dbapassword, secretkey,
> rmqpassword, portmapping, letsencrypt, email_host, email_port, email_user,
> email_password, email_ssl, email_tls):
> +def edit_dockercompose(hostname, dbpassword, dbapassword, secretkey,
> rmqpassword, portmapping, letsencrypt, email_host, email_port, email_user,
> email_password, email_ssl, email_tls, app_dir='/opt'):
> +
> +    in_layersapp_build = False
>
>      def adjust_cert_mount_line(ln):
>          linesplit = ln.split(':')
>          if letsencrypt:
>              linesplit[1] = '/etc/letsencrypt'
>          else:
> -            linesplit[1] = '/opt/cert'
> +            linesplit[1] = app_dir + '/cert'
>          # This allows us to handle if there is a ":ro" or similar on the
> end
>          return ':'.join(linesplit)
>
> @@ -305,6 +308,33 @@ def edit_dockercompose(hostname, dbpassword,
> dbapassword, secretkey, rmqpassword
>                  newlines.append(ucline + '\n')
>              else:
>                  newlines.append(yaml_comment(line) + '\n')
> +        elif re.match(r'^\s+build:\s+\.$', line):
> +            # Convert inline 'build: .' to block form so we can pass
> APP_DIR as a build arg
> +            indent = line[:len(line) - len(line.lstrip())]
> +            newlines.append(indent + 'build:\n')
> +            newlines.append(indent + '  context: .\n')
> +            newlines.append(indent + '  args:\n')
> +            newlines.append(indent + '    - APP_DIR=${APP_DIR}\n')
> +            in_layersapp_build = True
> +            continue
> +        elif '- APP_DIR=' in line:
> +            # Keep APP_DIR referencing the .env file (do not embed value
> here)
> +            format = line[:line.find('- APP_DIR=')].replace('#', '')
> +            newlines.append(format + '- APP_DIR=${APP_DIR}\n')
> +            continue
> +        elif re.match(r'^\s+context:\s+\.$', line) and in_layersapp_build:
> +            in_layersapp_build = False
> +            newlines.append(line + '\n')
> +            continue
> +        elif ':/opt/workdir' in line:
> +            newlines.append(line.replace(':/opt/workdir', ':' + app_dir +
> '/workdir') + '\n')
> +            continue
> +        elif ':/opt/layerindex-task-logs' in line:
> +            newlines.append(line.replace(':/opt/layerindex-task-logs',
> ':' + app_dir + '/layerindex-task-logs') + '\n')
> +            continue
> +        elif '--workdir=/opt/layerindex' in line:
> +            newlines.append(line.replace('--workdir=/opt/layerindex',
> '--workdir=' + app_dir + '/layerindex') + '\n')
> +            continue
>          elif "layersweb:" in line:
>              in_layersweb = True
>              newlines.append(line + "\n")
> @@ -426,7 +456,7 @@ def edit_nginx_ssl_conf(hostname, https_port, certdir,
> certfile, keyfile):
>      writefile("docker/nginx-ssl-edited.conf", ''.join(newlines))
>
>
> -def edit_settings_py(emailaddr):
> +def edit_settings_py(emailaddr, app_dir='/opt'):
>      filedata = readfile('docker/settings.py')
>      newlines = []
>      lines = filedata.splitlines()
> @@ -444,6 +474,9 @@ def edit_settings_py(emailaddr):
>                  newlines.append("  ('Admin', '%s'),\n" % emailaddr)
>              newlines.append(")\n")
>              continue
> +        elif line.lstrip().startswith('LAYER_FETCH_DIR'):
> +            newlines.append("LAYER_FETCH_DIR = \"%s/workdir\"\n" %
> app_dir)
> +            continue
>          newlines.append(line + "\n")
>      writefile("docker/settings.py", ''.join(newlines))
>
> @@ -474,9 +507,9 @@ def edit_dockerfile_web(hostname, no_https):
>      writefile("Dockerfile.web", ''.join(newlines))
>
>
> -def setup_https(hostname, http_port, https_port, letsencrypt,
> letsencrypt_production, cert, cert_key, emailaddr):
> +def setup_https(hostname, http_port, https_port, letsencrypt,
> letsencrypt_production, cert, cert_key, emailaddr, app_dir='/opt'):
>      local_cert_dir = os.path.abspath('docker/certs')
> -    container_cert_dir = '/opt/cert'
> +    container_cert_dir = app_dir + '/cert'
>      if letsencrypt:
>          # Create dummy cert
>          container_cert_dir = '/etc/letsencrypt'
> @@ -578,8 +611,26 @@ def edit_options_file(project_name):
>          f.write('project_name=%s\n' % project_name)
>
>
> -def check_connectivity():
> -    return_code = subprocess.call(['docker-compose', 'run', '--rm',
> 'layersapp', '/opt/connectivity_check.sh'], shell=False)
> +def write_env_file(app_dir):
> +    """Write APP_DIR to .env so docker-compose passes it as a build
> arg."""
> +    env_vars = {}
> +    try:
> +        with open('.env', 'r') as f:
> +            for line in f:
> +                line = line.strip()
> +                if '=' in line and not line.startswith('#'):
> +                    k, v = line.split('=', 1)
> +                    env_vars[k.strip()] = v.strip()
> +    except FileNotFoundError:
> +        pass
> +    env_vars['APP_DIR'] = app_dir
> +    with open('.env', 'w') as f:
> +        for k, v in env_vars.items():
> +            f.write('%s=%s\n' % (k, v))
> +
> +
> +def check_connectivity(app_dir='/opt'):
> +    return_code = subprocess.call(['docker-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)
> @@ -741,24 +792,25 @@ if args.uninstall:
>  if args.update:
>      args.no_https = read_dockerfile_web()
>      if not args.no_https:
> -        container_cert_dir = '/opt/cert'
> +        container_cert_dir = args.app_dir + '/cert'
>          args.hostname, https_port, certdir, certfile, keyfile =
> read_nginx_ssl_conf(container_cert_dir)
>          edit_nginx_ssl_conf(args.hostname, https_port, certdir, certfile,
> keyfile)
>  else:
>      # Always edit these in case we switch from proxy to no proxy
>      edit_gitproxy(socks_proxy_host, socks_proxy_port, args.no_proxy)
>      edit_dockerfile(args.http_proxy, args.https_proxy, args.no_proxy)
> +    write_env_file(args.app_dir)
>
> -    edit_dockercompose(args.hostname, dbpassword, dbapassword, secretkey,
> rmqpassword, args.portmapping, args.letsencrypt, email_host, email_port,
> args.email_user, args.email_password, args.email_ssl, args.email_tls)
> +    edit_dockercompose(args.hostname, dbpassword, dbapassword, secretkey,
> rmqpassword, args.portmapping, args.letsencrypt, email_host, email_port,
> args.email_user, args.email_password, args.email_ssl, args.email_tls,
> args.app_dir)
>
>      edit_dockerfile_web(args.hostname, args.no_https)
>
> -    edit_settings_py(emailaddr)
> +    edit_settings_py(emailaddr, args.app_dir)
>
>      edit_options_file(args.project_name)
>
>      if not args.no_https:
> -        setup_https(args.hostname, http_port, https_port,
> args.letsencrypt, args.letsencrypt_production, args.cert, args.cert_key,
> emailaddr)
> +        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)
> @@ -768,7 +820,7 @@ if return_code != 0:
>
>  if not (args.update or args.no_connectivity):
>      ## Run connectivity check
> -    check_connectivity()
> +    check_connectivity(args.app_dir)
>
>  # Get real project name (if only there were a reasonable way to do
> this... ugh)
>  real_project_name = ''
> @@ -830,7 +882,7 @@ if not args.no_migrate:
>      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',
> '/opt/migrate.sh'], shell=False, env=env)
> +    return_code = subprocess.call(['docker-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)
> @@ -864,13 +916,13 @@ if not args.update:
>                  break
>      for volume in volumes:
>          volname = '%s_%s' % (real_project_name, volume)
> -        return_code = subprocess.call(['docker', 'run', '--rm', '-v',
> '%s:/opt/mount' % volname, 'debian:stretch', 'chown', '500', '/opt/mount'],
> shell=False)
> +        return_code = subprocess.call(['docker', 'run', '--rm', '-v',
> '%s:%s/mount' % (volname, args.app_dir), 'debian:stretch', 'chown', '500',
> args.app_dir + '/mount'], shell=False)
>          if return_code != 0:
>              print("Setting volume permissions for volume %s failed" %
> volume)
>              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 /opt/layerindex/manage.py collectstatic --noinput" %
> quote(real_project_name), shell = True)
> +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)
>  if return_code != 0:
>      print("Collecting static files failed")
>      sys.exit(1)
> @@ -891,12 +943,12 @@ else:
>  if not args.update:
>      if not args.databasefile:
>          ## Set site name
> -        return_code = subprocess.call(['docker-compose', 'run', '--rm',
> 'layersapp', '/opt/layerindex/layerindex/tools/site_name.py', host,
> 'OpenEmbedded Layer Index'], shell=False)
> +        return_code = subprocess.call(['docker-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', '/opt/layerindex/manage.py', 'createsuperuser', '--email',
> emailaddr], shell=False)
> +        return_code = subprocess.call(['docker-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)
> --
> 2.43.0
>
>
diff mbox series

Patch

diff --git a/Dockerfile b/Dockerfile
index 72f57d2..8e6e508 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,6 +3,8 @@ 
 FROM ubuntu:jammy
 LABEL maintainer="Michael Halstead <mhalstead@linuxfoundation.org>"
 
+ARG APP_DIR=/opt
+ENV APP_DIR=${APP_DIR}
 ENV PYTHONUNBUFFERED=1 \
     LANGUAGE=en_US \
     LANG=en_US.UTF-8 \
@@ -55,22 +57,22 @@  RUN DEBIAN_FRONTEND=noninteractive apt-get update \
 	&& rm -rf /var/lib/apt/lists/* \
 	&& apt-get clean
 
-COPY . /opt/layerindex
-RUN rm -rf /opt/layerindex/docker
-COPY docker/settings.py /opt/layerindex/settings.py
-COPY docker/refreshlayers.sh /opt/refreshlayers.sh
-COPY docker/updatelayers.sh /opt/updatelayers.sh
-COPY docker/migrate.sh /opt/migrate.sh
-COPY docker/connectivity_check.sh /opt/connectivity_check.sh
+COPY . ${APP_DIR}/layerindex
+RUN rm -rf ${APP_DIR}/layerindex/docker
+COPY docker/settings.py ${APP_DIR}/layerindex/settings.py
+COPY docker/refreshlayers.sh ${APP_DIR}/refreshlayers.sh
+COPY docker/updatelayers.sh ${APP_DIR}/updatelayers.sh
+COPY docker/migrate.sh ${APP_DIR}/migrate.sh
+COPY docker/connectivity_check.sh ${APP_DIR}/connectivity_check.sh
 
-RUN mkdir /opt/workdir \
+RUN mkdir ${APP_DIR}/workdir \
 	&& adduser --system --uid=500 layers \
-	&& chown -R layers /opt/workdir
+	&& chown -R layers ${APP_DIR}/workdir
 USER layers
 
 # Always copy in .gitconfig and proxy helper script (they need editing to be active)
 COPY docker/.gitconfig /home/layers/.gitconfig
-COPY docker/git-proxy /opt/bin/git-proxy
+COPY docker/git-proxy ${APP_DIR}/bin/git-proxy
 
 # Start Gunicorn
-CMD ["/usr/local/bin/gunicorn", "wsgi:application", "--workers=4", "--bind=:5000", "--timeout=60", "--log-level=debug", "--chdir=/opt/layerindex"]
+CMD ["/bin/sh", "-c", "/usr/local/bin/gunicorn wsgi:application --workers=4 --bind=:5000 --timeout=60 --log-level=debug --chdir=${APP_DIR}/layerindex"]
diff --git a/docker/migrate.sh b/docker/migrate.sh
index ca15368..bb9f646 100755
--- a/docker/migrate.sh
+++ b/docker/migrate.sh
@@ -1,2 +1,2 @@ 
 #!/bin/bash
-python3 /opt/layerindex/manage.py migrate "$@"
+python3 ${APP_DIR:-/opt}/layerindex/manage.py migrate "$@"
diff --git a/docker/refreshlayers.sh b/docker/refreshlayers.sh
index 0c550bd..5adaf86 100755
--- a/docker/refreshlayers.sh
+++ b/docker/refreshlayers.sh
@@ -1,3 +1,3 @@ 
 #!/bin/bash
-update=/opt/layerindex/layerindex/update.py
+update=${APP_DIR:-/opt}/layerindex/layerindex/update.py
 $update -q -r
diff --git a/docker/updatelayers.sh b/docker/updatelayers.sh
index 21640ba..6f0e8f0 100755
--- a/docker/updatelayers.sh
+++ b/docker/updatelayers.sh
@@ -1,3 +1,3 @@ 
 #!/bin/bash
-update=/opt/layerindex/layerindex/update.py
+update=${APP_DIR:-/opt}/layerindex/layerindex/update.py
 $update -q
diff --git a/dockersetup.py b/dockersetup.py
index bc7478c..3a74eb3 100755
--- a/dockersetup.py
+++ b/dockersetup.py
@@ -70,6 +70,7 @@  def get_args():
     parser.add_argument('--no-migrate', action="store_true", default=False, help='Skip running database migrations')
     parser.add_argument('--no-admin-user', action="store_true", default=False, help='Skip adding admin user')
     parser.add_argument('--no-connectivity', action="store_true", default=False, help='Skip checking external network connectivity')
+    parser.add_argument('--app-dir', type=str, help='Base directory inside the container for application files. Default is %(default)s', required=False, default='/opt')
 
     args = parser.parse_args()
 
@@ -253,14 +254,16 @@  def yaml_comment(line):
 
 
 # Add hostname, secret key, db info, and email host in docker-compose.yml
-def edit_dockercompose(hostname, dbpassword, dbapassword, secretkey, rmqpassword, portmapping, letsencrypt, email_host, email_port, email_user, email_password, email_ssl, email_tls):
+def edit_dockercompose(hostname, dbpassword, dbapassword, secretkey, rmqpassword, portmapping, letsencrypt, email_host, email_port, email_user, email_password, email_ssl, email_tls, app_dir='/opt'):
+
+    in_layersapp_build = False
 
     def adjust_cert_mount_line(ln):
         linesplit = ln.split(':')
         if letsencrypt:
             linesplit[1] = '/etc/letsencrypt'
         else:
-            linesplit[1] = '/opt/cert'
+            linesplit[1] = app_dir + '/cert'
         # This allows us to handle if there is a ":ro" or similar on the end
         return ':'.join(linesplit)
 
@@ -305,6 +308,33 @@  def edit_dockercompose(hostname, dbpassword, dbapassword, secretkey, rmqpassword
                 newlines.append(ucline + '\n')
             else:
                 newlines.append(yaml_comment(line) + '\n')
+        elif re.match(r'^\s+build:\s+\.$', line):
+            # Convert inline 'build: .' to block form so we can pass APP_DIR as a build arg
+            indent = line[:len(line) - len(line.lstrip())]
+            newlines.append(indent + 'build:\n')
+            newlines.append(indent + '  context: .\n')
+            newlines.append(indent + '  args:\n')
+            newlines.append(indent + '    - APP_DIR=${APP_DIR}\n')
+            in_layersapp_build = True
+            continue
+        elif '- APP_DIR=' in line:
+            # Keep APP_DIR referencing the .env file (do not embed value here)
+            format = line[:line.find('- APP_DIR=')].replace('#', '')
+            newlines.append(format + '- APP_DIR=${APP_DIR}\n')
+            continue
+        elif re.match(r'^\s+context:\s+\.$', line) and in_layersapp_build:
+            in_layersapp_build = False
+            newlines.append(line + '\n')
+            continue
+        elif ':/opt/workdir' in line:
+            newlines.append(line.replace(':/opt/workdir', ':' + app_dir + '/workdir') + '\n')
+            continue
+        elif ':/opt/layerindex-task-logs' in line:
+            newlines.append(line.replace(':/opt/layerindex-task-logs', ':' + app_dir + '/layerindex-task-logs') + '\n')
+            continue
+        elif '--workdir=/opt/layerindex' in line:
+            newlines.append(line.replace('--workdir=/opt/layerindex', '--workdir=' + app_dir + '/layerindex') + '\n')
+            continue
         elif "layersweb:" in line:
             in_layersweb = True
             newlines.append(line + "\n")
@@ -426,7 +456,7 @@  def edit_nginx_ssl_conf(hostname, https_port, certdir, certfile, keyfile):
     writefile("docker/nginx-ssl-edited.conf", ''.join(newlines))
 
 
-def edit_settings_py(emailaddr):
+def edit_settings_py(emailaddr, app_dir='/opt'):
     filedata = readfile('docker/settings.py')
     newlines = []
     lines = filedata.splitlines()
@@ -444,6 +474,9 @@  def edit_settings_py(emailaddr):
                 newlines.append("  ('Admin', '%s'),\n" % emailaddr)
             newlines.append(")\n")
             continue
+        elif line.lstrip().startswith('LAYER_FETCH_DIR'):
+            newlines.append("LAYER_FETCH_DIR = \"%s/workdir\"\n" % app_dir)
+            continue
         newlines.append(line + "\n")
     writefile("docker/settings.py", ''.join(newlines))
 
@@ -474,9 +507,9 @@  def edit_dockerfile_web(hostname, no_https):
     writefile("Dockerfile.web", ''.join(newlines))
 
 
-def setup_https(hostname, http_port, https_port, letsencrypt, letsencrypt_production, cert, cert_key, emailaddr):
+def setup_https(hostname, http_port, https_port, letsencrypt, letsencrypt_production, cert, cert_key, emailaddr, app_dir='/opt'):
     local_cert_dir = os.path.abspath('docker/certs')
-    container_cert_dir = '/opt/cert'
+    container_cert_dir = app_dir + '/cert'
     if letsencrypt:
         # Create dummy cert
         container_cert_dir = '/etc/letsencrypt'
@@ -578,8 +611,26 @@  def edit_options_file(project_name):
         f.write('project_name=%s\n' % project_name)
 
 
-def check_connectivity():
-    return_code = subprocess.call(['docker-compose', 'run', '--rm', 'layersapp', '/opt/connectivity_check.sh'], shell=False)
+def write_env_file(app_dir):
+    """Write APP_DIR to .env so docker-compose passes it as a build arg."""
+    env_vars = {}
+    try:
+        with open('.env', 'r') as f:
+            for line in f:
+                line = line.strip()
+                if '=' in line and not line.startswith('#'):
+                    k, v = line.split('=', 1)
+                    env_vars[k.strip()] = v.strip()
+    except FileNotFoundError:
+        pass
+    env_vars['APP_DIR'] = app_dir
+    with open('.env', 'w') as f:
+        for k, v in env_vars.items():
+            f.write('%s=%s\n' % (k, v))
+
+
+def check_connectivity(app_dir='/opt'):
+    return_code = subprocess.call(['docker-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)
@@ -741,24 +792,25 @@  if args.uninstall:
 if args.update:
     args.no_https = read_dockerfile_web()
     if not args.no_https:
-        container_cert_dir = '/opt/cert'
+        container_cert_dir = args.app_dir + '/cert'
         args.hostname, https_port, certdir, certfile, keyfile = read_nginx_ssl_conf(container_cert_dir)
         edit_nginx_ssl_conf(args.hostname, https_port, certdir, certfile, keyfile)
 else:
     # Always edit these in case we switch from proxy to no proxy
     edit_gitproxy(socks_proxy_host, socks_proxy_port, args.no_proxy)
     edit_dockerfile(args.http_proxy, args.https_proxy, args.no_proxy)
+    write_env_file(args.app_dir)
 
-    edit_dockercompose(args.hostname, dbpassword, dbapassword, secretkey, rmqpassword, args.portmapping, args.letsencrypt, email_host, email_port, args.email_user, args.email_password, args.email_ssl, args.email_tls)
+    edit_dockercompose(args.hostname, dbpassword, dbapassword, secretkey, rmqpassword, args.portmapping, args.letsencrypt, email_host, email_port, args.email_user, args.email_password, args.email_ssl, args.email_tls, args.app_dir)
 
     edit_dockerfile_web(args.hostname, args.no_https)
 
-    edit_settings_py(emailaddr)
+    edit_settings_py(emailaddr, args.app_dir)
 
     edit_options_file(args.project_name)
 
     if not args.no_https:
-        setup_https(args.hostname, http_port, https_port, args.letsencrypt, args.letsencrypt_production, args.cert, args.cert_key, emailaddr)
+        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)
@@ -768,7 +820,7 @@  if return_code != 0:
 
 if not (args.update or args.no_connectivity):
     ## Run connectivity check
-    check_connectivity()
+    check_connectivity(args.app_dir)
 
 # Get real project name (if only there were a reasonable way to do this... ugh)
 real_project_name = ''
@@ -830,7 +882,7 @@  if not args.no_migrate:
     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', '/opt/migrate.sh'], shell=False, env=env)
+    return_code = subprocess.call(['docker-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)
@@ -864,13 +916,13 @@  if not args.update:
                 break
     for volume in volumes:
         volname = '%s_%s' % (real_project_name, volume)
-        return_code = subprocess.call(['docker', 'run', '--rm', '-v', '%s:/opt/mount' % volname, 'debian:stretch', 'chown', '500', '/opt/mount'], shell=False)
+        return_code = subprocess.call(['docker', 'run', '--rm', '-v', '%s:%s/mount' % (volname, args.app_dir), 'debian:stretch', 'chown', '500', args.app_dir + '/mount'], shell=False)
         if return_code != 0:
             print("Setting volume permissions for volume %s failed" % volume)
             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 /opt/layerindex/manage.py collectstatic --noinput" % quote(real_project_name), shell = True)
+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)
 if return_code != 0:
     print("Collecting static files failed")
     sys.exit(1)
@@ -891,12 +943,12 @@  else:
 if not args.update:
     if not args.databasefile:
         ## Set site name
-        return_code = subprocess.call(['docker-compose', 'run', '--rm', 'layersapp', '/opt/layerindex/layerindex/tools/site_name.py', host, 'OpenEmbedded Layer Index'], shell=False)
+        return_code = subprocess.call(['docker-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', '/opt/layerindex/manage.py', 'createsuperuser', '--email', emailaddr], shell=False)
+        return_code = subprocess.call(['docker-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)