diff --git a/changelog.d/21.added b/changelog.d/21.added
new file mode 100644
index 0000000..e69de29
diff --git a/src/cobbler_tftp/cli.py b/src/cobbler_tftp/cli.py
index e54fb2e..31871cd 100644
--- a/src/cobbler_tftp/cli.py
+++ b/src/cobbler_tftp/cli.py
@@ -2,17 +2,21 @@
 Cobbler-tftp will be managable as a command-line service.
 """
 
+import os
 from pathlib import Path
+from signal import SIGTERM
 from typing import List, Optional
 
 import click
 import yaml
+from daemon import DaemonContext
 
 try:
     import importlib.metadata as importlib_metadata
 except ImportError:  # use backport for Python versions older than 3.8
     import importlib_metadata
 
+from cobbler_tftp.server import run_server
 from cobbler_tftp.settings import SettingsFactory
 
 try:
@@ -74,20 +78,29 @@ def start(
     """
     click.echo(cli.__doc__)
     click.echo("Initializing Cobbler-tftp server...")
-    settings_factory: SettingsFactory = SettingsFactory()
-    # settings_file = SettingsFactory.load_config_file(settings_factory, config)
-    # environment_variables = SettingsFactory.load_env_variables(settings_factory)
-    # cli_arguments = SettingsFactory.load_cli_options(
-    #     settings_factory, daemon, enable_automigration, settings
-    # )
     if config is None:
         config_path = None
     else:
         config_path = Path(config)
-    application_settings = SettingsFactory.build_settings(
-        settings_factory, config_path, daemon, enable_automigration, settings
+    application_settings = SettingsFactory().build_settings(
+        config_path, daemon, enable_automigration, settings
     )
-    print(application_settings)
+    if application_settings.is_daemon:
+        click.echo("Starting daemon...")
+        with DaemonContext():
+            # All previously open file descriptors are invalid now.
+            # Files and connections needed for the daemon should be opened
+            # in run_server or listed in the files_preserve option
+            # of DaemonContext.
+
+            application_settings.pid_file_path.write_text(str(os.getpid()))
+            try:
+                run_server(application_settings)
+            finally:
+                application_settings.pid_file_path.unlink()
+    else:
+        click.echo("Daemon mode disabled, running in foreground.")
+        run_server(application_settings)
 
 
 @cli.command()
@@ -103,16 +116,37 @@ def print_default_config():
     """
     Print the default application parameters.
     """
-    settings_factory: SettingsFactory = SettingsFactory()
-    click.echo(settings_factory.build_settings(None))
+    click.echo(SettingsFactory().build_settings(None))
 
 
 @cli.command()
-def stop():
+@click.option(
+    "--config", "-c", type=click.Path(), help="Set location of configuration file."
+)
+@click.option("--pid-file", "-p", type=click.Path(), help="Set location of PID file.")
+def stop(config: Optional[str], pid_file: Optional[str]):
     """
     Stop the cobbler-tftp server daemon if it is running
     """
-    pass
+    if pid_file is None:
+        if config is None:
+            config_path = None
+        else:
+            config_path = Path(config)
+        application_settings = SettingsFactory().build_settings(config_path)
+        pid_file_path = application_settings.pid_file_path
+    else:
+        pid_file_path = Path(pid_file)
+    try:
+        pid = int(pid_file_path.read_text(encoding="UTF-8"))
+    except OSError:
+        click.echo("Unable to read PID file. The daemon is probably not running.")
+        return
+    try:
+        os.kill(pid, SIGTERM)
+    except ProcessLookupError:
+        click.echo("Stale PID file. The daemon is no longer running.")
+    pid_file_path.unlink()
 
 
 cli.add_command(start)
diff --git a/src/cobbler_tftp/server/__init__.py b/src/cobbler_tftp/server/__init__.py
new file mode 100644
index 0000000..bea0efa
--- /dev/null
+++ b/src/cobbler_tftp/server/__init__.py
@@ -0,0 +1,38 @@
+"""
+This package contains the actual TFTP server.
+"""
+
+import logging
+import logging.config
+
+from cobbler_tftp.server.tftp import TFTPServer
+from cobbler_tftp.settings import Settings
+
+try:
+    from importlib.resources import files
+except ImportError:
+    from importlib_resources import files
+
+
+def run_server(application_settings: Settings):
+    """Set up logging, initialize the server and run it."""
+
+    logging_conf = application_settings.logging_conf
+    if logging_conf is None or not logging_conf.exists():
+        logging_conf = files("cobbler_tftp.settings.data").joinpath("logging.conf")
+    logging.config.fileConfig(str(logging_conf))
+    logging.debug("Server starting...")
+    try:
+        address = application_settings.tftp_addr
+        port = application_settings.tftp_port
+        retries = application_settings.tftp_retries
+        timeout = application_settings.tftp_timeout
+        server = TFTPServer(address, port, retries, timeout)
+    except:  # pylint: disable=bare-except
+        logging.exception("Fatal exception while setting up server")
+        return
+    try:
+        server.run()
+    except:  # pylint: disable=bare-except
+        logging.exception("Fatal exception in server")
+        server.close()
diff --git a/src/cobbler_tftp/server/tftp.py b/src/cobbler_tftp/server/tftp.py
new file mode 100644
index 0000000..c4d9956
--- /dev/null
+++ b/src/cobbler_tftp/server/tftp.py
@@ -0,0 +1,88 @@
+"""
+This module contains the main TFTP server class.
+"""
+
+import logging
+
+from fbtftp import BaseHandler, BaseServer, ResponseData
+
+
+class CobblerResponseData(ResponseData):
+    """File-like object representing the response from the TFTP server."""
+
+    def __init__(self):
+        pass
+
+    def read(self, n):
+        return b""
+
+    def size(self):
+        return 0
+
+    def close(self):
+        pass
+
+
+def handler_stats_cb(stats):
+    duration = stats.duration() * 1000
+    logging.info(
+        "Spent %fms processing request for %r from %r",
+        duration,
+        stats.file_path,
+        stats.peer,
+    )
+    logging.info(
+        "Error: %r, sent %d bytes with %d retransmits",
+        stats.error,
+        stats.bytes_sent,
+        stats.retransmits,
+    )
+
+
+def server_stats_cb(stats):
+    """
+    Called by the fbtftp to log server stats. Currently unused.
+    """
+
+
+class CobblerRequestHandler(BaseHandler):
+    """
+    Handles TFTP requests using the Cobbler API.
+    """
+
+    def __init__(self, server_addr, peer, path, options):
+        """
+        Initialize a handler for a specific request.
+
+        :param server_addr: Tuple containing the server address and port.
+        :param peer: Tuple containing the client address and port.
+        :param path: Request file path.
+        :param options: Options requested by the client.
+        """
+        # Future arguments can be handled here
+        super().__init__(server_addr, peer, path, options, handler_stats_cb)
+
+    def get_response_data(self):
+        return CobblerResponseData()
+
+
+class TFTPServer(BaseServer):
+    """
+    Implements a TFTP server for the Cobbler API using the CobblerRequestHandler.
+    """
+
+    def __init__(self, address, port, retries, timeout):
+        """
+        Initialize the TFTP server.
+
+        :param address: IP address to listen on.
+        :param port: UDP Port to listen on.
+        :param retries: Maximum number of retries when sending a packet fails.
+        :param timeout: Timeout for sending packets.
+        """
+        # Future arguments can be handled here
+        self.handler_stats_cb = handler_stats_cb
+        super().__init__(address, port, retries, timeout, server_stats_cb)
+
+    def get_handler(self, server_addr, peer, path, options):
+        return CobblerRequestHandler(server_addr, peer, path, options)
diff --git a/src/cobbler_tftp/settings/__init__.py b/src/cobbler_tftp/settings/__init__.py
index c96cbbc..a730eb3 100644
--- a/src/cobbler_tftp/settings/__init__.py
+++ b/src/cobbler_tftp/settings/__init__.py
@@ -26,10 +26,16 @@ def __init__(
         self,
         auto_migrate_settings: bool,
         is_daemon: bool,
+        pid_file_path: Path,
         uri: str,
         username: str,
-        password: Union[str, None],
-        password_file: Union[Path, None],
+        password: Optional[str],
+        password_file: Optional[Path],
+        tftp_addr: str,
+        tftp_port: int,
+        tftp_retries: int,
+        tftp_timeout: int,
+        logging_conf: Optional[Path],
     ) -> None:
         """
         Initialize a new instance of the Settings.
@@ -45,10 +51,16 @@ def __init__(
 
         self.auto_migrate_settings: bool = auto_migrate_settings
         self.is_daemon: bool = is_daemon
+        self.pid_file_path: Path = pid_file_path
         self.uri: str = uri
         self.user: str = username
-        self.__password: Union[str, None] = password
-        self.__password_file: Union[Path, None] = password_file
+        self.tftp_addr: str = tftp_addr
+        self.tftp_port: int = tftp_port
+        self.tftp_retries: int = tftp_retries
+        self.tftp_timeout: int = tftp_timeout
+        self.logging_conf: Optional[Path] = logging_conf
+        self.__password: Optional[str] = password
+        self.__password_file: Optional[Path] = password_file
 
     def __repr__(self):
         """
@@ -126,7 +138,8 @@ def build_settings(
         # Type ignores are necessary as at this point it is not known what value comes from that key.
         auto_migrate_settings: bool = self._settings_dict.get("auto_migrate_settings", False)  # type: ignore
         is_daemon: bool = self._settings_dict.get("is_daemon", False)  # type: ignore
-        cobbler_settings = self._settings_dict.get("cobbler", None)
+        pid_file_path: Path = Path(self._settings_dict.get("pid_file_path", "/run/cobbler-tftp.pid"))  # type: ignore
+        cobbler_settings = self._settings_dict.get("cobbler", {})
         uri: str = cobbler_settings.get("uri", "")  # type: ignore
         username: str = cobbler_settings.get("username", "")  # type: ignore
         password: str = cobbler_settings.get("password", "")  # type: ignore
@@ -134,15 +147,30 @@ def build_settings(
             password_file: Optional[Path] = Path(cobbler_settings.get("password_file", None))  # type: ignore
         else:
             password_file = None
+        tftp_settings = self._settings_dict.get("tftp", None)
+        tftp_addr: str = tftp_settings.get("address", "127.0.0.1")  # type: ignore
+        tftp_port: int = tftp_settings.get("port", 69)  # type: ignore
+        tftp_retries: int = tftp_settings.get("retries", 5)  # type: ignore
+        tftp_timeout: int = tftp_settings.get("timeout", 2)  # type: ignore
+        if self._settings_dict.get("logging_conf", None) is not None:  # type: ignore
+            logging_conf: Optional[Path] = Path(self._settings_dict.get("logging_conf", None))  # type: ignore
+        else:
+            logging_conf = None
 
         # Create and return a new Settings object
         settings = Settings(
             auto_migrate_settings,
             is_daemon,
+            pid_file_path,
             uri,
             username,
             password,
             password_file,
+            tftp_addr,
+            tftp_port,
+            tftp_retries,
+            tftp_timeout,
+            logging_conf,
         )
 
         return settings
diff --git a/src/cobbler_tftp/settings/data/logging.conf b/src/cobbler_tftp/settings/data/logging.conf
new file mode 100644
index 0000000..73fa740
--- /dev/null
+++ b/src/cobbler_tftp/settings/data/logging.conf
@@ -0,0 +1,40 @@
+# based on https://github.com/cobbler/cobbler/blob/main/config/cobbler/logging_config.conf
+[loggers]
+keys=root
+
+[handlers]
+keys=FileLogger,stdout
+
+[formatters]
+keys=Logfile,stdout
+
+[logger_root]
+level=DEBUG
+handlers=FileLogger,stdout
+
+[logger_parser]
+level=DEBUG
+handlers=FileLogger
+propagate=1
+qualname=compiler.parser
+
+[handler_stdout]
+class=StreamHandler
+level=INFO
+formatter=stdout
+args=(sys.stdout,)
+
+[handler_FileLogger]
+class=FileHandler
+level=INFO
+formatter=Logfile
+args=('/var/log/cobbler-tftp.log', 'a')
+
+[formatter_Logfile]
+format=[%(threadName)s] %(asctime)s - %(levelname)s | %(message)s
+datefmt=%Y-%m-%dT%H:%M:%S
+class=logging.Formatter
+
+[formatter_stdout]
+format=%(levelname)s | %(message)s
+class=logging.Formatter
diff --git a/src/cobbler_tftp/settings/data/settings.yml b/src/cobbler_tftp/settings/data/settings.yml
index cbc7d7d..0339be9 100644
--- a/src/cobbler_tftp/settings/data/settings.yml
+++ b/src/cobbler_tftp/settings/data/settings.yml
@@ -3,9 +3,17 @@ schema: 1.0
 auto_migrate_settings: false
 # Run cobbler-tftp as a daemon in the background
 is_daemon: true
+pid_file_path: "/run/cobbler-tftp.pid"
 # Specifications of the cobbler-server
 cobbler:
   uri: "http://localhost/cobbler_api"
   username: "cobbler"
   password: "cobbler"
-  # password_file: "/etc/cobbler-tftp/cobbler_password"
\ No newline at end of file
+  # password_file: "/etc/cobbler-tftp/cobbler_password"
+# TFTP server configuration
+tftp:
+  address: "127.0.0.1"
+  port: 69
+  retries: 5
+  timeout: 2
+logging_conf: "/etc/cobbler-tftp/logging.conf"
diff --git a/src/cobbler_tftp/settings/migrations/__init__.py b/src/cobbler_tftp/settings/migrations/__init__.py
index 89bb2a5..99923ae 100644
--- a/src/cobbler_tftp/settings/migrations/__init__.py
+++ b/src/cobbler_tftp/settings/migrations/__init__.py
@@ -131,9 +131,9 @@ def __validate_module(name: ModuleType) -> bool:
     """
     # noqa for these lines because we can't use the custom types to check this.
     module_methods = {
-        "validate": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]])->bool",  # noqa
-        "normalize": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]])->Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]]",  # noqa
-        "migrate": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]])->Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]]",  # noqa
+        "validate": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[int,str,pathlib.Path]]]])->bool",  # noqa
+        "normalize": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[int,str,pathlib.Path]]]])->Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[int,str,pathlib.Path]]]]",  # noqa
+        "migrate": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[int,str,pathlib.Path]]]])->Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[int,str,pathlib.Path]]]]",  # noqa
     }
 
     for key, value in module_methods.items():
diff --git a/src/cobbler_tftp/settings/migrations/v1_0.py b/src/cobbler_tftp/settings/migrations/v1_0.py
index 9961948..e449d08 100644
--- a/src/cobbler_tftp/settings/migrations/v1_0.py
+++ b/src/cobbler_tftp/settings/migrations/v1_0.py
@@ -11,11 +11,19 @@
         Optional("schema"): float,
         Optional("auto_migrate_settings"): bool,
         Optional("is_daemon"): bool,
+        Optional("pid_file_path"): str,
         Optional("cobbler"): {
             Optional("uri"): str,
             Optional("username"): str,
             Optional(Or("password", "password_file", only_one=True)): Or(str, Path),
         },
+        Optional("tftp"): {
+            Optional("address"): str,
+            Optional("port"): int,
+            Optional("retries"): int,
+            Optional("timeout"): int,
+        },
+        Optional("logging_conf"): str,
     }
 )
 
diff --git a/src/cobbler_tftp/types/__init__.py b/src/cobbler_tftp/types/__init__.py
index fe7f153..1d1d29b 100644
--- a/src/cobbler_tftp/types/__init__.py
+++ b/src/cobbler_tftp/types/__init__.py
@@ -5,4 +5,6 @@
 
 # Dictionary type for configuration parameters
 # if this type changes: changes __valdiate_module function in migrations/__init__.py
-SettingsDict = Dict[str, Union[float, bool, str, Path, Dict[str, Union[str, Path]]]]
+SettingsDict = Dict[
+    str, Union[float, bool, str, Path, Dict[str, Union[int, str, Path]]]
+]
diff --git a/tests/test_data/valid_config.yml b/tests/test_data/valid_config.yml
index 75afccf..0947a94 100644
--- a/tests/test_data/valid_config.yml
+++ b/tests/test_data/valid_config.yml
@@ -4,4 +4,9 @@ is_daemon: false
 cobbler:
   uri: 'http://testmachine.testnetwork.com/api'
   username: 'cobbler'
-  password_file: 'tests/test_data/password_file'
\ No newline at end of file
+  password_file: 'tests/test_data/password_file'
+tftp:
+  address: '0.0.0.0'
+  port: 1969
+  retries: 10
+  timeout: 3
diff --git a/tests/unittests/application_settings/conftest.py b/tests/unittests/application_settings/conftest.py
index e7617d1..9a30562 100644
--- a/tests/unittests/application_settings/conftest.py
+++ b/tests/unittests/application_settings/conftest.py
@@ -1,6 +1,8 @@
 """
 This module implements all necessary fixtures for running the unittests using pytests. They are automaticall discovered.
 """
+from pathlib import Path
+
 import pytest
 
 from cobbler_tftp.types import SettingsDict
@@ -18,11 +20,19 @@ def fake_settings_dict() -> SettingsDict:
         "schema": 1.0,
         "auto_migrate_settings": True,
         "is_daemon": True,
+        "pid_file_path": Path("/run/cobbler-tftp.pid"),
         "cobbler": {
             "uri": "http://localhost/cobbler_api",
             "username": "cobbler",
             "password": "cobbler",
         },
+        "tftp": {
+            "addr": "127.0.0.1",
+            "port": 69,
+            "timeout": 2,
+            "retries": 5,
+        },
+        "logging_conf": "/etc/cobbler-tftp/logging.conf",
     }
     return fake_settings_dict
 
diff --git a/tests/unittests/application_settings/test_settings.py b/tests/unittests/application_settings/test_settings.py
index fafb2a4..2922421 100644
--- a/tests/unittests/application_settings/test_settings.py
+++ b/tests/unittests/application_settings/test_settings.py
@@ -17,6 +17,20 @@ def settings_factory():
     return SettingsFactory()
 
 
+def assert_default_settings(settings):
+    assert settings.auto_migrate_settings is False
+    assert settings.is_daemon is True
+    assert str(settings.pid_file_path) == "/run/cobbler-tftp.pid"
+    assert settings.uri == "http://localhost/cobbler_api"
+    assert settings.user == "cobbler"
+    assert settings.password == "cobbler"
+    assert settings.tftp_addr == "127.0.0.1"
+    assert settings.tftp_port == 69
+    assert settings.tftp_retries == 5
+    assert settings.tftp_timeout == 2
+    assert str(settings.logging_conf) == "/etc/cobbler-tftp/logging.conf"
+
+
 def test_build_settings_with_default_config_file(
     settings_factory: SettingsFactory, mocker
 ):
@@ -31,11 +45,7 @@ def test_build_settings_with_default_config_file(
 
     # Assert that the expected values are set in the Settings object
     assert isinstance(settings, Settings)
-    assert settings.auto_migrate_settings is False
-    assert settings.is_daemon is True
-    assert settings.uri == "http://localhost/cobbler_api"
-    assert settings.user == "cobbler"
-    assert settings.password == "cobbler"
+    assert_default_settings(settings)
 
 
 def test_build_settings_with_valid_config_file(
@@ -50,6 +60,10 @@ def test_build_settings_with_valid_config_file(
     assert settings.uri == "http://testmachine.testnetwork.com/api"
     assert settings.user == "cobbler"
     assert settings.password == "password"
+    assert settings.tftp_addr == "0.0.0.0"  # nosec
+    assert settings.tftp_port == 1969
+    assert settings.tftp_retries == 10
+    assert settings.tftp_timeout == 3
 
 
 def test_build_settings_with_invalid_config_file(
@@ -89,8 +103,4 @@ def test_build_settings_with_missing_config_file(
     captured_message = capsys.readouterr()
     assert captured_message.out == expected_message
     assert isinstance(settings, Settings)
-    assert settings.auto_migrate_settings is False
-    assert settings.is_daemon is True
-    assert settings.uri == "http://localhost/cobbler_api"
-    assert settings.user == "cobbler"
-    assert settings.password == "cobbler"
+    assert_default_settings(settings)