diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a0eb5d3c0ba28e99ea5737b65caf35b5b4e32995..10cafd6a6f5b0fb9f21366b850bf944b74926929 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,7 +1,7 @@
 image: registry.git.cccv.de/uffd/docker-images/buster
 
 variables:
-  DEBIAN_FRONTEND: noninteractive 
+  DEBIAN_FRONTEND: noninteractive
   GIT_SUBMODULE_STRATEGY: normal
   PYLINT_PIN: pylint~=2.10.0
 
@@ -12,6 +12,17 @@ before_script:
   - python3 -m pylint --version
   - python3 -m coverage --version
 
+build:apt:
+  script:
+  - ./debian/create_changelog.py uffd-socketmap > debian/changelog
+  - dpkg-buildpackage -us -uc
+  - mv ../*.deb ./
+  - dpkg-deb -I *.deb
+  - dpkg-deb -c *.deb
+  artifacts:
+    paths:
+    - ./*.deb
+
 linter:bullseye:
   image: registry.git.cccv.de/uffd/docker-images/bullseye
   stage: test
@@ -19,7 +30,7 @@ linter:bullseye:
   - pip3 install $PYLINT_PIN pylint-gitlab pylint-flask-sqlalchemy # this force-updates jinja2 and some other packages!
   - python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabCodeClimateReporter 'server.py' > codeclimate.json
   - python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabPagesHtmlReporter 'server.py' > pylint.html
-  - python3 -m pylint --rcfile .pylintrc --output-format=text 'server.py' 
+  - python3 -m pylint --rcfile .pylintrc --output-format=text 'server.py'
   artifacts:
     when: always
     paths:
diff --git a/README.md b/README.md
index a4083d621a996a0d820750200efcfe6bbcf0cb33..670f8e3576983b81fa31eb59cfd69459dfe77c9a 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,29 @@
-Socketmap proxy for uffd mail alias lookup
-==========================================
+Socketmap server to integrate uffd mail aliases with postfix
+============================================================
+
+[uffd](https://git.cccv.de/uffd/uffd) has features that rely on mail aliases.
+To make those mail aliases work, it provides an API to lookup alias addresses
+for a given address. uffd-socketmap uses this API to integrate alias lookup
+with MTAs that support the socketmap protocol, like sendmail and postfix.
+
+uffd-socketmap can be run manually. For production deployments, use the
+provided debian packages. Add our package mirror to `/etc/sources.list`:
 
-Run it like this:
 ```
-SERVER_API_KEY=my_secret_api_key server.py --socket-path /var/run/uffd-socketmap.sock --api-url=https://sso.example.com
+deb https://packages.cccv.de/uffd bullseye main
 ```
+Then download [cccv-archive-key.gpg](cccv-archive-key.gpg) and add it to
+the trusted repository keys in `/etc/apt/trusted.gpg.d/`. Afterwards run
+`apt update && apt install uffd-socketmap` to install the package.
+
+Set the API url and secret in `/etc/uffd-socketmap-postfix.conf`, enable
+and start `uffd-socketmap-postfix.socket`. Then configure Postfix, e.g.
+by adding the following lines to `/etc/postfix/main.cf`:
 
-Configure Postfix to use it by setting `virtual_alias_maps` in `/etc/postfix/main.cf` like this:
 ```
-virtual_alias_maps = socketmap:unix:/var/run/uffd-socketmap.sock:virtual
+# Note that postfix runs in a chroot (/var/spool/postfix) and paths are
+# relative to that!
+virtual_alias_maps = socketmap:unix:/uffd-socketmap.sock:virtual
+# Defaults to $virtual_alias_maps, which does not work here, so unset it
+virtual_alias_domains =
 ```
diff --git a/cccv-archive-key.gpg b/cccv-archive-key.gpg
new file mode 100644
index 0000000000000000000000000000000000000000..b0ac4de43a0786a52060bade75c5150ac552ade7
Binary files /dev/null and b/cccv-archive-key.gpg differ
diff --git a/debian/contrib/uffd-socketmap-postfix.conf b/debian/contrib/uffd-socketmap-postfix.conf
new file mode 100644
index 0000000000000000000000000000000000000000..c427afa4eac80512fc2c05202558e2249ce2e365
--- /dev/null
+++ b/debian/contrib/uffd-socketmap-postfix.conf
@@ -0,0 +1,7 @@
+# Both options must be set
+#SERVER_API_URL="https://localhost"
+#SERVER_API_KEY="my_secret_api_token"
+
+# The socket path is hard-coded to "/var/spool/postfix/uffd-socketmap.sock"
+# ("/uffd-socketmap.sock" in the postfix sandbox). Use systemd overwrites
+# for uffd-socketmap-postfix.socket to change it.
diff --git a/uffd-socketmap@.service b/debian/contrib/uffd-socketmap-postfix.service
similarity index 70%
rename from uffd-socketmap@.service
rename to debian/contrib/uffd-socketmap-postfix.service
index 50344e782ba52b1ed6f6f567552c36e4ec584c6b..65a8275dce66124288b40fd1d9e6cfb702766640 100644
--- a/uffd-socketmap@.service
+++ b/debian/contrib/uffd-socketmap-postfix.service
@@ -1,17 +1,19 @@
 [Unit]
-Description=Socketmap proxy for uffd mail alias lookup
+Description=Socketmap server to integrate uffd mail aliases with postfix
 After=network.target
 Before=postfix.service
+BindsTo=uffd-socketmap-postfix.socket
 
 [Service]
 ExecStart=/usr/bin/uffd-socketmap --socket-fd 3
 
 Restart=always
 RestartSec=10
-StandardOutput=syslog
-StandardError=syslog
-SyslogIdentifier=uffd-socketmap-%I
+StandardOutput=journal
+StandardError=journal
+SyslogIdentifier=uffd-socketmap-postfix
 
+DynamicUser=true
 PrivateUsers=true
 CapabilityBoundingSet=
 NoNewPrivileges=true
@@ -34,9 +36,9 @@ PrivateTmp=true
 PrivateDevices=true
 SystemCallArchitectures=native
 SystemCallFilter=@system-service
+MemoryDenyWriteExecute=true
 
-EnvironmentFile=/etc/uffd-socketmap/defaults
-EnvironmentFile=/etc/uffd-socketmap/$I.env
+EnvironmentFile=/etc/uffd-socketmap-postfix.conf
 
 [Install]
 WantedBy=default.target
diff --git a/debian/contrib/uffd-socketmap-postfix.socket b/debian/contrib/uffd-socketmap-postfix.socket
new file mode 100644
index 0000000000000000000000000000000000000000..3c2e0a422b13f0f09ba6ae8837b80f66b5b4e742
--- /dev/null
+++ b/debian/contrib/uffd-socketmap-postfix.socket
@@ -0,0 +1,11 @@
+[Unit]
+Description=Socketmap server to integrate uffd mail aliases with postfix
+
+[Socket]
+ListenStream=/var/spool/postfix/uffd-socketmap.sock
+SocketUser=postfix
+SocketGroup=postfix
+SocketMode=0640
+
+[Install]
+WantedBy=sockets.target
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000000000000000000000000000000000000..22732bed74b10029b629f12fa6f4471ad582ab3f
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,16 @@
+Source: uffd-socketmap
+Section: python
+Priority: optional
+Maintainer: CCCV <it@cccv.de>
+Build-Depends:
+ debhelper-compat (= 12),
+Standards-Version: 4.5.0
+Homepage: https://git.cccv.de/uffd/socketmap-proxy
+Vcs-Git: https://git.cccv.de/uffd/socketmap-proxy.git
+
+Package: uffd-socketmap
+Architecture: all
+Depends:
+ ${misc:Depends},
+ python3-requests,
+Description: Socketmap server to integrate uffd mail aliases with postfix
diff --git a/debian/create_changelog.py b/debian/create_changelog.py
new file mode 100755
index 0000000000000000000000000000000000000000..b62f211c24403fbfb2a1465cbf58491b5616f5b6
--- /dev/null
+++ b/debian/create_changelog.py
@@ -0,0 +1,106 @@
+#!/usr/bin/python3
+import sys
+import re
+import textwrap
+import datetime
+import email.utils
+
+import git
+
+package_name = 'UNKNOWN'
+
+alias_names = {
+	'julian': 'Julian Rother',
+	'Julian': 'Julian Rother',
+}
+
+ignore_commit_regexes = [
+	'^fixup!',
+]
+
+def print_release(tag=None, commits=tuple(), last_tag=None):
+	release_version = '0.0.0'
+	release_author = git.objects.util.Actor('None', 'undefined@example.com')
+	release_date = 0
+	release_status = 'UNRELEASED'
+	message = ''
+
+	if tag:
+		release_status = 'unstable'
+		release_version = tag.name[1:] # strip leading "v"
+		if isinstance(tag.object, git.TagObject):
+			release_author = tag.object.tagger
+			release_date = tag.object.tagged_date
+			message = tag.object.message.split('-----BEGIN PGP SIGNATURE-----')[0].strip()
+		else:
+			release_author = tag.object.committer
+			release_date = tag.object.committed_date
+	elif commits:
+		release_author = commits[0].committer
+		release_date = commits[0].committed_date
+		date = datetime.datetime.fromtimestamp(release_date).strftime('%Y%m%dT%H%M%S')
+		last_version = '0.0.0'
+		if last_tag:
+			last_version = last_tag.name[1:] # strip leading "v"
+		release_version = f'{last_version}+git{date}-{commits[0].hexsha[:8]}'
+
+	print(f'{package_name} ({release_version}) {release_status}; urgency=medium')
+	print()
+	if message:
+		print(textwrap.indent(message, '  '))
+		print()
+	commit_authors = [] # list of (key, author), sorted by first commit date
+	commit_author_emails = {} # author email -> key
+	commit_author_names = {} # author name -> key
+	commit_author_commits = {} # key -> list of commits
+	for commit in commits:
+		if any(filter(lambda pattern: re.match(pattern, commit.summary), ignore_commit_regexes)):
+			continue
+		if len(commit.parents) > 1:
+			continue # Ignore merge commits
+		author_name = alias_names.get(commit.author.name, commit.author.name)
+		key = commit_author_emails.get(commit.author.email)
+		if key is None:
+			key = commit_author_names.get(author_name)
+		if key is None:
+			key = commit.author.email
+			commit_authors.append((key, author_name))
+		commit_author_emails[commit.author.email] = key
+		commit_author_names[author_name] = key
+		commit_author_commits[key] = commit_author_commits.get(key, []) + [commit]
+	commit_authors.sort(key=lambda args: len(commit_author_commits[args[0]]))
+	for key, author_name in commit_authors:
+		print(f'  [ {author_name} ]')
+		for commit in commit_author_commits[key]:
+			lines = '\n'.join(textwrap.wrap(commit.summary, 90))
+			lines = '  * ' + textwrap.indent(lines, '    ').strip()
+			print(lines)
+		print()
+	print(f' -- {alias_names.get(release_author.name, release_author.name)} <{release_author.email}>  {email.utils.formatdate(release_date)}')
+
+if __name__ == '__main__':
+	repo = git.Repo('.')
+	package_name = sys.argv[1]
+
+	version_commits = {}
+	for tag in repo.tags:
+		if not re.fullmatch('v[0-9]+[.][0-9]+[.][0-9]+.*', tag.name):
+			continue
+		if isinstance(tag.object, git.TagObject):
+			commit_hexsha = tag.object.object.hexsha
+		else:
+			commit_hexsha = tag.object.hexsha
+		version_commits[commit_hexsha] = tag
+
+	tag = None
+	commits = []
+	for commit in repo.iter_commits('HEAD'):
+		if commit.hexsha in version_commits:
+			prev_tag = version_commits[commit.hexsha]
+			if commits:
+				print_release(tag, commits, last_tag=prev_tag)
+				print()
+			tag = prev_tag
+			commits = []
+		commits.append(commit)
+	print_release(tag, commits)
diff --git a/debian/install b/debian/install
new file mode 100644
index 0000000000000000000000000000000000000000..e886322c605606d1f23708ba9b7016abd6d60b8c
--- /dev/null
+++ b/debian/install
@@ -0,0 +1,4 @@
+server.py /usr/lib/uffd-socketmap/
+debian/contrib/uffd-socketmap-postfix.service /usr/lib/systemd/system/
+debian/contrib/uffd-socketmap-postfix.socket /usr/lib/systemd/system/
+debian/contrib/uffd-socketmap-postfix.conf /etc/
diff --git a/debian/links b/debian/links
new file mode 100644
index 0000000000000000000000000000000000000000..5c199f3ebe5c63bffb09745ceb82bf912c220605
--- /dev/null
+++ b/debian/links
@@ -0,0 +1 @@
+/usr/lib/uffd-socketmap/server.py /usr/bin/uffd-socketmap
diff --git a/debian/postinst b/debian/postinst
new file mode 100755
index 0000000000000000000000000000000000000000..bdba6dc00488e07d643734fae338985b9b62c6f1
--- /dev/null
+++ b/debian/postinst
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+set -e
+
+case "$1" in
+	configure)
+		chmod 0640 /etc/uffd-socketmap-postfix.conf
+	;;
+
+	abort-upgrade|abort-remove|abort-deconfigure)
+	;;
+
+	*)
+		echo "postinst called with unknown argument \`$1'" >&2
+		exit 1
+	;;
+esac
+
+#DEBHELPER#
+
+exit 0
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000000000000000000000000000000000000..cbe925d7587131c8ec8761915930894c60c096fd
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,3 @@
+#!/usr/bin/make -f
+%:
+	dh $@
diff --git a/server.py b/server.py
old mode 100644
new mode 100755
index 3fbfc67f01a4730ace33fb27bafeba0118729b47..dd59646006ee1c4bd0c46dc1a4495253bb05bf3f
--- a/server.py
+++ b/server.py
@@ -1,3 +1,4 @@
+#!/usr/bin/python3
 import os
 import sys
 import logging
diff --git a/uffd-socketmap@.socket b/uffd-socketmap@.socket
deleted file mode 100644
index dcff275b7590f3e976430fb3c933212572091497..0000000000000000000000000000000000000000
--- a/uffd-socketmap@.socket
+++ /dev/null
@@ -1,12 +0,0 @@
-[Unit]
-Description=Socket proxy for uffd mail alias lookup
-PartOf=uffd-socketmap@%i.service
-
-[Socket]
-ListenStream=/run/socketmap-proxy/%I.sock
-SocketUser=postfix
-SocketGroup=postfix
-SocketMode=0640
-
-[Install]
-WantedBy=sockets.target