diff --git a/platypush/platydock/__init__.py b/platypush/platydock/__init__.py
index 77e9778c9..1725bf4d5 100755
--- a/platypush/platydock/__init__.py
+++ b/platypush/platydock/__init__.py
@@ -1,462 +1,108 @@
 """
-Platydock
-
-Platydock is a helper that allows you to easily manage (create, destroy, start,
-stop and list) Platypush instances as Docker images.
+Platydock is a helper script that allows you to automatically create a
+Dockerfile for Platypush starting from a configuration file.
 """
 
 import argparse
-import enum
+import inspect
 import os
 import pathlib
-import re
-import shutil
-import subprocess
 import sys
-import textwrap
-import traceback as tb
-import yaml
+from typing import Iterable
 
 from platypush.config import Config
-from platypush.utils import manifest
+from platypush.utils.manifest import Dependencies
 
-workdir = os.path.join(
-    os.path.expanduser('~'), '.local', 'share', 'platypush', 'platydock'
-)
+ERR_PREFIX = '\n\033[6;31;47mERROR\033[0;91m '
+ERR_SUFFIX = '\033[0m'
 
 
-class Action(enum.Enum):
-    build = 'build'
-    start = 'start'
-    stop = 'stop'
-    rm = 'rm'
-    ls = 'ls'
+def generate_dockerfile(cfgfile: str) -> str:
+    """
+    Generate a Dockerfile based on a configuration file.
 
-    def __str__(self):
-        return self.value
+    :param cfgfile: Path to the configuration file.
+    :return: The content of the generated Dockerfile.
+    """
+    Config.init(cfgfile)
+    new_file_lines = []
+    ports = _get_exposed_ports()
+    deps = Dependencies.from_config(cfgfile, pkg_manager='apk')
+    is_after_expose_cmd = False
+    base_file = os.path.join(
+        str(pathlib.Path(inspect.getfile(Config)).parent), 'docker', 'base.Dockerfile'
+    )
+
+    with open(base_file, 'r') as f:
+        file_lines = [line.rstrip() for line in f.readlines()]
+
+    for line in file_lines:
+        if line.startswith('RUN cd /install '):
+            for new_line in deps.before:
+                new_file_lines.append('RUN ' + new_line)
+
+            for new_line in deps.to_pkg_install_commands(
+                pkg_manager='apk', skip_sudo=True
+            ):
+                new_file_lines.append('RUN ' + new_line)
+        elif line == 'RUN rm -rf /install':
+            for new_line in deps.to_pip_install_commands():
+                new_file_lines.append('RUN ' + new_line)
+
+            for new_line in deps.after:
+                new_file_lines.append('RUN' + new_line)
+        elif line.startswith('EXPOSE ') and ports:
+            if not is_after_expose_cmd:
+                new_file_lines.extend([f'EXPOSE {port}' for port in ports])
+                is_after_expose_cmd = True
+
+            continue
+
+        new_file_lines.append(line)
+
+    return '\n'.join(new_file_lines)
 
 
-def _parse_deps(cls):
-    deps = []
-
-    for line in cls.__doc__.split('\n'):
-        m = re.search(r'\(``pip install (.+)``\)', line)
-        if m:
-            deps.append(m.group(1))
-
-    return deps
-
-
-def generate_dockerfile(deps, ports, cfgfile, device_dir, python_version):
-    device_id = Config.get('device_id')
-    if not device_id:
-        raise RuntimeError(
-            (
-                'You need to specify a device_id in {} - Docker '
-                + 'containers cannot rely on hostname'
-            ).format(cfgfile)
+def _get_exposed_ports() -> Iterable[int]:
+    """
+    :return: The listen ports used by the backends enabled in the configuration
+        file.
+    """
+    backends_config = Config.get_backends()
+    return {
+        int(port)
+        for port in (
+            backends_config.get('http', {}).get('port'),
+            backends_config.get('tcp', {}).get('port'),
         )
-
-    os.makedirs(device_dir, exist_ok=True)
-    content = textwrap.dedent(
-        '''
-        FROM python:{python_version}-slim-bookworm
-
-        RUN mkdir -p /app
-        RUN mkdir -p /etc/platypush
-        RUN mkdir -p /usr/local/share/platypush\n
-        '''.format(
-            python_version=python_version
-        )
-    ).lstrip()
-
-    srcdir = os.path.dirname(cfgfile)
-    cfgfile_copy = os.path.join(device_dir, 'config.yaml')
-    shutil.copy(cfgfile, cfgfile_copy, follow_symlinks=True)
-    content += 'COPY config.yaml /etc/platypush/\n'
-    backend_config = Config.get_backends()
-
-    # Redis configuration for Docker
-    if 'redis' not in backend_config:
-        backend_config['redis'] = {
-            'redis_args': {
-                'host': 'redis',
-                'port': 6379,
-            }
-        }
-
-        with open(cfgfile_copy, 'a') as f:
-            f.write(
-                '\n# Automatically added by platydock, do not remove\n'
-                + yaml.dump(
-                    {
-                        'backend.redis': backend_config['redis'],
-                    }
-                )
-                + '\n'
-            )
-
-    # Main database configuration
-    has_main_db = False
-    with open(cfgfile_copy, 'r') as f:
-        for line in f.readlines():
-            if re.match(r'^(main.)?db.*', line):
-                has_main_db = True
-                break
-
-    if not has_main_db:
-        with open(cfgfile_copy, 'a') as f:
-            f.write(
-                '\n# Automatically added by platydock, do not remove\n'
-                + yaml.dump(
-                    {
-                        'main.db': {
-                            'engine': 'sqlite:////platypush.db',
-                        }
-                    }
-                )
-                + '\n'
-            )
-
-    # Copy included files
-    # noinspection PyProtectedMember
-    for include in Config._included_files:
-        incdir = os.path.relpath(os.path.dirname(include), srcdir)
-        destdir = os.path.join(device_dir, incdir)
-        pathlib.Path(destdir).mkdir(parents=True, exist_ok=True)
-        shutil.copy(include, destdir, follow_symlinks=True)
-        content += 'RUN mkdir -p /etc/platypush/' + incdir + '\n'
-        content += (
-            'COPY '
-            + os.path.relpath(include, srcdir)
-            + ' /etc/platypush/'
-            + incdir
-            + '\n'
-        )
-
-    # Copy script files
-    scripts_dir = os.path.join(os.path.dirname(cfgfile), 'scripts')
-    if os.path.isdir(scripts_dir):
-        local_scripts_dir = os.path.join(device_dir, 'scripts')
-        remote_scripts_dir = '/etc/platypush/scripts'
-        shutil.copytree(
-            scripts_dir, local_scripts_dir, symlinks=True, dirs_exist_ok=True
-        )
-        content += f'RUN mkdir -p {remote_scripts_dir}\n'
-        content += f'COPY scripts/ {remote_scripts_dir}\n'
-
-    packages = deps.pop('packages', None)
-    pip = deps.pop('pip', None)
-    exec_cmds = deps.pop('exec', None)
-    pkg_cmd = (
-        f'\n\t&& apt-get install --no-install-recommends -y {" ".join(packages)} \\'
-        if packages
-        else ''
-    )
-    pip_cmd = f'\n\t&& pip install {" ".join(pip)} \\' if pip else ''
-    content += f'''
-RUN dpkg --configure -a \\
-    && apt-get -f install \\
-    && apt-get --fix-missing install \\
-    && apt-get clean \\
-    && apt-get update \\
-    && apt-get -y upgrade \\
-    && apt-get -y dist-upgrade \\
-    && apt-get install --no-install-recommends -y apt-utils \\
-    && apt-get install --no-install-recommends -y build-essential \\
-    && apt-get install --no-install-recommends -y git \\
-    && apt-get install --no-install-recommends -y sudo \\
-    && apt-get install --no-install-recommends -y libffi-dev \\
-    && apt-get install --no-install-recommends -y libcap-dev \\
-    && apt-get install --no-install-recommends -y libjpeg-dev \\{pkg_cmd}{pip_cmd}'''
-
-    for exec_cmd in exec_cmds:
-        content += f'\n\t&& {exec_cmd} \\'
-    content += '''
-    && apt-get install --no-install-recommends -y zlib1g-dev
-
-RUN git clone --recursive https://git.platypush.tech/platypush/platypush.git /app \\
-    && cd /app \\
-    && pip install -r requirements.txt
-
-RUN apt-get remove -y git \\
-    && apt-get remove -y build-essential \\
-    && apt-get remove -y libffi-dev \\
-    && apt-get remove -y libjpeg-dev \\
-    && apt-get remove -y libcap-dev \\
-    && apt-get remove -y zlib1g-dev \\
-    && apt-get remove -y apt-utils \\
-    && apt-get clean \\
-    && apt-get autoremove -y \\
-    && rm -rf /var/lib/apt/lists/*
-'''
-
-    for port in ports:
-        content += 'EXPOSE {}\n'.format(port)
-
-    content += textwrap.dedent(
-        '''
-
-        ENV PYTHONPATH /app:$PYTHONPATH
-        CMD ["python", "-m", "platypush"]
-        '''
-    )
-
-    dockerfile = os.path.join(device_dir, 'Dockerfile')
-    print('Generating Dockerfile {}'.format(dockerfile))
-
-    with open(dockerfile, 'w') as f:
-        f.write(content)
-
-
-def build(args):
-    global workdir
-
-    ports = set()
-    parser = argparse.ArgumentParser(
-        prog='platydock build', description='Build a Platypush image from a config.yaml'
-    )
-
-    parser.add_argument(
-        '-c',
-        '--config',
-        type=str,
-        required=True,
-        help='Path to the platypush configuration file',
-    )
-    parser.add_argument(
-        '-p',
-        '--python-version',
-        type=str,
-        default='3.9',
-        help='Python version to be used',
-    )
-
-    opts, args = parser.parse_known_args(args)
-
-    cfgfile = os.path.abspath(os.path.expanduser(opts.config))
-    manifest._available_package_manager = (
-        'apt'  # Force apt for Debian-based Docker images
-    )
-    install_cmds = manifest.get_dependencies_from_conf(cfgfile)
-    python_version = opts.python_version
-    backend_config = Config.get_backends()
-
-    # Container exposed ports
-    if backend_config.get('http'):
-        from platypush.backend.http import HttpBackend
-
-        # noinspection PyProtectedMember
-        ports.add(backend_config['http'].get('port', HttpBackend._DEFAULT_HTTP_PORT))
-
-    if backend_config.get('tcp'):
-        ports.add(backend_config['tcp']['port'])
-
-    dev_dir = os.path.join(workdir, Config.get('device_id'))
-    generate_dockerfile(
-        deps=dict(install_cmds),
-        ports=ports,
-        cfgfile=cfgfile,
-        device_dir=dev_dir,
-        python_version=python_version,
-    )
-
-    subprocess.call(
-        [
-            'docker',
-            'build',
-            '-t',
-            'platypush-{}'.format(Config.get('device_id')),
-            dev_dir,
-        ]
-    )
-
-
-def start(args):
-    global workdir
-
-    parser = argparse.ArgumentParser(
-        prog='platydock start',
-        description='Start a Platypush container',
-        epilog=textwrap.dedent(
-            '''
-                                       You can append additional options that
-                                       will be passed to the docker container.
-                                       Example:
-
-                                            --add-host='myhost:192.168.1.1'
-                                       '''
-        ),
-    )
-
-    parser.add_argument('image', type=str, help='Platypush image to start')
-    parser.add_argument(
-        '-p',
-        '--publish',
-        action='append',
-        nargs='*',
-        default=[],
-        help=textwrap.dedent(
-            '''
-                                             Container's ports to expose to the host.
-                                             Note that the default exposed ports from
-                                             the container service will be exposed unless
-                                             these mappings override them (e.g. port 8008
-                                             on the container will be mapped to 8008 on
-                                             the host).
-
-                                             Example:
-
-                                             -p 18008:8008
-                                             '''
-        ),
-    )
-
-    parser.add_argument(
-        '-a',
-        '--attach',
-        action='store_true',
-        default=False,
-        help=textwrap.dedent(
-            '''
-                                             If set, then attach to the container after starting it up (default: false).
-                                             '''
-        ),
-    )
-
-    opts, args = parser.parse_known_args(args)
-    ports = {}
-    dockerfile = os.path.join(workdir, opts.image, 'Dockerfile')
-
-    with open(dockerfile) as f:
-        for line in f:
-            m = re.match(r'expose (\d+)', line.strip().lower())
-            if m:
-                ports[m.group(1)] = m.group(1)
-
-    for mapping in opts.publish:
-        host_port, container_port = mapping[0].split(':')
-        ports[container_port] = host_port
-
-    print('Preparing Redis support container')
-    subprocess.call(['docker', 'pull', 'redis'])
-    subprocess.call(
-        ['docker', 'run', '--rm', '--name', 'redis-' + opts.image, '-d', 'redis']
-    )
-
-    docker_cmd = [
-        'docker',
-        'run',
-        '--rm',
-        '--name',
-        opts.image,
-        '-it',
-        '--link',
-        'redis-' + opts.image + ':redis',
-    ]
-
-    for container_port, host_port in ports.items():
-        docker_cmd += ['-p', host_port + ':' + container_port]
-
-    docker_cmd += args
-    docker_cmd += ['-d', 'platypush-' + opts.image]
-
-    print('Starting Platypush container {}'.format(opts.image))
-    subprocess.call(docker_cmd)
-
-    if opts.attach:
-        subprocess.call(['docker', 'attach', opts.image])
-
-
-def stop(args):
-    parser = argparse.ArgumentParser(
-        prog='platydock stop', description='Stop a Platypush container'
-    )
-
-    parser.add_argument('container', type=str, help='Platypush container to stop')
-    opts, args = parser.parse_known_args(args)
-
-    print('Stopping Platypush container {}'.format(opts.container))
-    subprocess.call(['docker', 'kill', '{}'.format(opts.container)])
-
-    print('Stopping Redis support container')
-    subprocess.call(['docker', 'stop', 'redis-{}'.format(opts.container)])
-
-
-def rm(args):
-    global workdir
-
-    parser = argparse.ArgumentParser(
-        prog='platydock rm',
-        description='Remove a Platypush image. '
-        + 'NOTE: make sure that no container is '
-        + 'running nor linked to the image before '
-        + 'removing it',
-    )
-
-    parser.add_argument('image', type=str, help='Platypush image to remove')
-    opts, args = parser.parse_known_args(args)
-
-    subprocess.call(['docker', 'rmi', 'platypush-{}'.format(opts.image)])
-    shutil.rmtree(os.path.join(workdir, opts.image), ignore_errors=True)
-
-
-def ls(args):
-    parser = argparse.ArgumentParser(
-        prog='platydock ls', description='List available Platypush containers'
-    )
-    parser.add_argument('filter', type=str, nargs='?', help='Image name filter')
-
-    opts, args = parser.parse_known_args(args)
-
-    p = subprocess.Popen(['docker', 'images'], stdout=subprocess.PIPE)
-    output = p.communicate()[0].decode().split('\n')
-    header = output.pop(0)
-    images = []
-
-    for line in output:
-        if re.match(r'^platypush-(.+?)\s.*', line) and (
-            not opts.filter or (opts.filter and opts.filter in line)
-        ):
-            images.append(line)
-
-    if images:
-        print(header)
-
-        for image in images:
-            print(image)
+        if port
+    }
 
 
 def main():
+    """
+    Generates a Dockerfile based on the configuration file.
+    """
     parser = argparse.ArgumentParser(
         prog='platydock',
         add_help=False,
-        description='Manage Platypush docker containers',
-        epilog='Use platydock <action> --help to ' + 'get additional help',
+        description='Create a Platypush Dockerfile from a config.yaml.',
+        epilog='Use platydock <action> --help to get additional help.',
     )
 
-    # noinspection PyTypeChecker
-    parser.add_argument(
-        'action', nargs='?', type=Action, choices=list(Action), help='Action to execute'
-    )
     parser.add_argument('-h', '--help', action='store_true', help='Show usage')
-    opts, args = parser.parse_known_args(sys.argv[1:])
+    parser.add_argument(
+        'cfgfile', type=str, nargs=1, help='The path to the configuration file.'
+    )
 
-    if (opts.help and not opts.action) or (not opts.help and not opts.action):
-        parser.print_help()
-        return 1
-
-    globals()[str(opts.action)](sys.argv[2:])
+    opts, _ = parser.parse_known_args(sys.argv[1:])
+    cfgfile = os.path.abspath(os.path.expanduser(opts.cfgfile[0]))
+    dockerfile = generate_dockerfile(cfgfile)
+    print(dockerfile)
 
 
 if __name__ == '__main__':
-    ERR_PREFIX = '\n\033[6;31;47mERROR\033[0;91m '
-    ERR_SUFFIX = '\033[0m'
+    main()
 
-    try:
-        main()
-    except Exception as e:
-        tb.print_exc(file=sys.stdout)
-        print(ERR_PREFIX + str(e) + ERR_SUFFIX, file=sys.stderr)
 
 # vim:sw=4:ts=4:et: