diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml
index 0684986e7..98dd00a0b 100644
--- a/.github/workflows/pipeline.yml
+++ b/.github/workflows/pipeline.yml
@@ -332,7 +332,6 @@ jobs:
           sudo GIT_TAG=$GIT_TAG release/wix/build_msi.sh ${GITHUB_WORKSPACE} amd64
           du -h binaries/msi/*.msi
 
-
       - name: Upload MSI files
         uses: actions/upload-artifact@v4
         with:
@@ -341,11 +340,16 @@ jobs:
           retention-days: 7
 
   release:
-    name: Release
-    needs: [build, msi, push-manifest]
+    name: Package/Release
+    needs: [build, msi]
     runs-on: ubuntu-latest
+    outputs:
+      package_list: ${{ steps.set-package-list.outputs.package_list }}
     steps:
       - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+          fetch-tags: true
 
       - uses: actions/download-artifact@v4
         with:
@@ -366,3 +370,56 @@ jobs:
           args: "release --clean -f release/goreleaser.yml ${{ env.RELEASE_FLAGS }}"
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Remove build artifacts
+        run: |
+          ls -l ./dist
+          rm ./dist/*.tar.gz ./dist/*.zip
+
+      - name: Upload all-packages artifact
+        uses: actions/upload-artifact@v4
+        with:
+          name: packages
+          path: dist/navidrome_v*
+
+      - id: set-package-list
+        name: Export list of generated packages
+        run: |
+          cd dist
+          set +x
+          ITEMS=$(ls navidrome_v* | sed 's/^navidrome_v[^_]*_linux_//' | jq -R -s -c 'split("\n")[:-1]')
+          echo $ITEMS
+          echo "package_list=${ITEMS}" >> $GITHUB_OUTPUT
+
+  upload-packages:
+    name: Upload Linux PKG
+    runs-on: ubuntu-latest
+    needs: [release]
+    strategy:
+      matrix:
+        item: ${{ fromJson(needs.release.outputs.package_list) }}
+    steps:
+      - name: Download all-packages artifact
+        uses: actions/download-artifact@v4
+        with:
+          name: packages
+          path: ./dist
+
+      - name: Upload all-packages artifact
+        uses: actions/upload-artifact@v4
+        with:
+          name: navidrome_linux_${{ matrix.item }}
+          path: dist/navidrome_v*_linux_${{ matrix.item }}
+
+#  delete-artifacts:
+#    name: Delete unused artifacts
+#    runs-on: ubuntu-latest
+#    needs: [upload-packages]
+#    steps:
+#      - name: Delete all-packages artifact
+#        env:
+#          GH_TOKEN: ${{ github.token }}
+#        run: |
+#          for artifact in $(gh api repos/${{ github.repository }}/actions/artifacts | jq -r '.artifacts[] | select(.name | startswith("packages")) | .id'); do
+#            gh api --method DELETE repos/${{ github.repository }}/actions/artifacts/$artifact
+#          done
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 37999c3f9..e064113bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ wiki
 TODO.md
 var
 navidrome.toml
+!release/linux/navidrome.toml
 master.zip
 testDB
 cache/*
diff --git a/Makefile b/Makefile
index 234309cdb..e581bacd6 100644
--- a/Makefile
+++ b/Makefile
@@ -144,6 +144,11 @@ docker-msi: ##@Cross_Compilation Build MSI installer for Windows
 	@du -h binaries/msi/*.msi
 .PHONY: docker-msi
 
+package: docker-build ##@Cross_Compilation Create binaries and packages for ALL supported platforms
+	@if [ -z `which goreleaser` ]; then echo "Please install goreleaser first: https://goreleaser.com/install/"; exit 1; fi
+	goreleaser release -f release/goreleaser.yml --clean --skip=publish --snapshot
+.PHONY: package
+
 get-music: ##@Development Download some free music from Navidrome's demo instance
 	mkdir -p music
 	( cd music; \
diff --git a/cmd/svc.go b/cmd/svc.go
index 219339515..e277bd459 100644
--- a/cmd/svc.go
+++ b/cmd/svc.go
@@ -20,6 +20,9 @@ var (
 		service.StatusStopped: "Stopped",
 		service.StatusRunning: "Running",
 	}
+
+	installUser      string
+	workingDirectory string
 )
 
 func init() {
@@ -70,9 +73,11 @@ func (p *svcControl) Stop(service.Service) error {
 
 var svcInstance = sync.OnceValue(func() service.Service {
 	options := make(service.KeyValue)
-	options["Restart"] = "on-success"
+	options["Restart"] = "on-failure"
 	options["SuccessExitStatus"] = "1 2 8 SIGKILL"
 	options["UserService"] = false
+	options["LogDirectory"] = conf.Server.DataFolder
+	options["SystemdScript"] = systemdScript
 	if conf.Server.LogFile != "" {
 		options["LogOutput"] = false
 	} else {
@@ -80,12 +85,13 @@ var svcInstance = sync.OnceValue(func() service.Service {
 		options["LogDirectory"] = conf.Server.DataFolder
 	}
 	svcConfig := &service.Config{
+		UserName:    installUser,
 		Name:        "navidrome",
 		DisplayName: "Navidrome",
 		Description: "Your Personal Streaming Service",
 		Dependencies: []string{
-			"Requires=",
-			"After="},
+			"After=remote-fs.target network.target",
+		},
 		WorkingDirectory: executablePath(),
 		Option:           options,
 	}
@@ -108,6 +114,10 @@ func runServiceCmd(cmd *cobra.Command, _ []string) {
 }
 
 func executablePath() string {
+	if workingDirectory != "" {
+		return workingDirectory
+	}
+
 	ex, err := os.Executable()
 	if err != nil {
 		log.Fatal(err)
@@ -141,11 +151,15 @@ func buildInstallCmd() *cobra.Command {
 		println("Service installed. Use 'navidrome svc start' to start it.")
 	}
 
-	return &cobra.Command{
+	cmd := &cobra.Command{
 		Use:   "install",
 		Short: "Install Navidrome service.",
 		Run:   runInstallCmd,
 	}
+	cmd.Flags().StringVarP(&installUser, "user", "u", "", "user to run service")
+	cmd.Flags().StringVarP(&workingDirectory, "working-directory", "w", "", "working directory of service")
+
+	return cmd
 }
 
 func buildUninstallCmd() *cobra.Command {
@@ -216,3 +230,38 @@ func buildExecuteCmd() *cobra.Command {
 		},
 	}
 }
+
+const systemdScript = `[Unit]
+Description={{.Description}}
+ConditionFileIsExecutable={{.Path|cmdEscape}}
+{{range $i, $dep := .Dependencies}} 
+{{$dep}} {{end}}
+
+[Service]
+StartLimitInterval=5
+StartLimitBurst=10
+ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}}
+{{if .WorkingDirectory}}WorkingDirectory={{.WorkingDirectory|cmdEscape}}{{end}}
+{{if .UserName}}User={{.UserName}}{{end}}
+{{if .Restart}}Restart={{.Restart}}{{end}}
+{{if .SuccessExitStatus}}SuccessExitStatus={{.SuccessExitStatus}}{{end}}
+TimeoutStopSec=20
+RestartSec=120
+EnvironmentFile=-/etc/sysconfig/{{.Name}}
+
+DevicePolicy=closed
+NoNewPrivileges=yes
+PrivateTmp=yes
+ProtectControlGroups=yes
+ProtectKernelModules=yes
+ProtectKernelTunables=yes
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+RestrictNamespaces=yes
+RestrictRealtime=yes
+SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap
+{{if .WorkingDirectory}}ReadWritePaths={{.WorkingDirectory|cmdEscape}}{{end}}
+ProtectSystem=full
+
+[Install]
+WantedBy=multi-user.target
+`
diff --git a/contrib/navidrome.service b/contrib/navidrome.service
index 817ab044c..5e6cbedce 100644
--- a/contrib/navidrome.service
+++ b/contrib/navidrome.service
@@ -11,15 +11,13 @@ WantedBy=multi-user.target
 User=navidrome
 Group=navidrome
 Type=simple
-ExecStart=/usr/bin/navidrome
+ExecStart=/usr/bin/navidrome --configfile "/etc/navidrome/navidrome.toml"
 StateDirectory=navidrome
 WorkingDirectory=/var/lib/navidrome
 TimeoutStopSec=20
 KillMode=process
 Restart=on-failure
 
-EnvironmentFile=-/etc/sysconfig/navidrome
-
 # See https://www.freedesktop.org/software/systemd/man/systemd.exec.html
 CapabilityBoundingSet=
 DevicePolicy=closed
diff --git a/release/goreleaser.yml b/release/goreleaser.yml
index e353aecfd..59f724138 100644
--- a/release/goreleaser.yml
+++ b/release/goreleaser.yml
@@ -33,6 +33,58 @@ checksum:
 snapshot:
   version_template: "{{ .Tag }}-SNAPSHOT"
 
+nfpms:
+  - id: navidrome
+    package_name: navidrome
+
+    homepage: https://navidrome.org
+    description: |-
+      🎧☁ Your Personal Streaming Service
+
+    maintainer: Deluan Quintão <deluan at navidrome.org>
+
+    license: GPL-3.0
+    formats:
+      - deb
+      - rpm
+
+    dependencies:
+      - ffmpeg
+
+    suggests:
+      - mpv
+
+    overrides:
+      rpm:
+        dependencies:
+          - "(ffmpeg or ffmpeg-free)"
+
+    contents:
+      - src: release/linux/navidrome.toml
+        dst: /etc/navidrome/navidrome.toml
+        type: "config|noreplace"
+        file_info:
+          mode: 0644
+          owner: navidrome
+          group: navidrome
+
+      - dst: /var/lib/navidrome
+        type: dir
+        file_info:
+          owner: navidrome
+          group: navidrome
+
+      - dst: /opt/navidrome/music
+        type: dir
+        file_info:
+          owner: navidrome
+          group: navidrome
+
+    scripts:
+      preinstall: "release/linux/preinstall.sh"
+      postinstall: "release/linux/postinstall.sh"
+      preremove: "release/linux/preremove.sh"
+
 release:
   draft: true
   mode: append
diff --git a/release/linux/navidrome.toml b/release/linux/navidrome.toml
new file mode 100644
index 000000000..e626c8caa
--- /dev/null
+++ b/release/linux/navidrome.toml
@@ -0,0 +1,2 @@
+DataFolder = "/var/lib/navidrome"
+MusicFolder = "/opt/navidrome/music"
diff --git a/release/linux/postinstall.sh b/release/linux/postinstall.sh
new file mode 100644
index 000000000..da43686b0
--- /dev/null
+++ b/release/linux/postinstall.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+# It is possible for a user to delete the configuration file in such a way that
+# the package manager (in particular, deb) thinks that the file exists, while it is 
+# no longer on disk. Specifically, doing a `rm /etc/navidrome/navidrome.toml` 
+# without something like `apt purge navidrome` will result in the system believing that
+# the file still exists. In this case, during isntall it will NOT extract the configuration 
+# file (as to not override it). Since `navidrome service install` depends on this file existing,
+# we will create it with the defaults anyway.
+if [ ! -f /etc/navidrome/navidrome.toml ]; then
+    printf "No navidrome.toml detected, creating in postinstall\n"
+    printf "DataFolder = \"/var/lib/navidrome\"\n" > /etc/navidrome/navidrome.toml
+    printf "MusicFolder = \"/opt/navidrome/music\"\n" >> /etc/navidrome/navidrome.toml
+fi
+
+postinstall_flag="/var/lib/navidrome/.installed"
+
+if [ ! -f "$postinstall_flag" ]; then
+    # The primary reason why this would fail is if the service was already installed AND
+    # someone manually removed the .installed flag. In this case, ignore the error
+    navidrome service install --user navidrome --working-directory /var/lib/navidrome --configfile /etc/navidrome/navidrome.toml || :
+    touch "$postinstall_flag"
+fi
+
+
diff --git a/release/linux/preinstall.sh b/release/linux/preinstall.sh
new file mode 100755
index 000000000..aa5850e6e
--- /dev/null
+++ b/release/linux/preinstall.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+if ! getent passwd navidrome > /dev/null 2>&1; then
+    printf "Creating default Navidrome user\n"
+    useradd --home-dir /var/lib/navidrome --create-home --system --user-group navidrome
+fi
diff --git a/release/linux/preremove.sh b/release/linux/preremove.sh
new file mode 100644
index 000000000..0dfcafe60
--- /dev/null
+++ b/release/linux/preremove.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+action=$1
+
+remove() {
+    postinstall_flag="/var/lib/navidrome/.installed"
+
+    if [ -f "$postinstall_flag" ]; then
+        # If this fails, ignore it
+        navidrome service uninstall || :
+        rm "$postinstall_flag"
+
+        printf "The following may still be present (especially if you have not done a purge):\n"
+        printf "1. /etc/navidrome/navidrome.toml (configuration file)\n"
+        printf "2. /var/lib/navidrome (database/cache)\n"
+        printf "3. /opt/navidrome (default location for music)\n"
+        printf "4. The Navidrome user (user name navidrome)\n"
+    fi
+}
+
+case "$action" in 
+    "1" | "upgrade")
+        # For an upgrade, do nothing
+        # Leave the service file untouched
+        # This is relevant for RPM/DEB-based installs
+        ;;
+    *)
+        remove
+        ;;
+esac