Skip to content

Deployment and Operations

This document is based on the following source files:

Table of Contents

  1. Introduction
  2. Build System
  3. Docker Deployment
  4. Standalone Binary Deployment
  5. Reverse Proxy and Sub-Path Deployment
  6. Online Upgrade System
  7. Tracely Monitoring
  8. Security Considerations
  9. Platform Adaptation Notes
  10. Operations Quick Reference

1. Introduction

Songloft supports two main deployment methods: Docker container and standalone binary. The Docker method provides a complete containerized experience, including hot-replacement upgrades and automatic restart; the standalone binary method is suitable for bare-metal or non-container environments. Both methods share the same startup parameter system and both support reverse proxy sub-path deployment.

This document covers the complete operations chain from build to production runtime, including build variant selection, deployment configuration, online upgrade, monitoring integration, and security hardening.

Section sources


2. Build System

Songloft's build system is controlled by two orthogonal dimensions:

DimensionValuesDescription
VERSIONdev / X.Y.ZControls whether it is a development version. When dev, the Makefile automatically enables -tags dev, including Swagger UI and pprof
BUILD_TYPElite / empty (i.e. full)Controls whether the Flutter Web frontend is embedded. lite runs in pure API mode

The two dimensions are strictly separate; mixed values such as BUILD_TYPE=dev are prohibited.

2.1 Build Commands

bash
make build              # Dev version (full, frontend embedded, with Swagger + pprof)
make build-lite          # Dev version (lite, without frontend embedded)
make build-prod          # Production version (full, frontend embedded)
make build-prod-lite     # Production version (lite, without frontend)

2.2 Compile-Time Injection

The Makefile injects the following variables into the binary via -ldflags -X:

makefile
LDFLAGS=-s -w \
    -X songloft/internal/version.Version=$(VERSION) \
    -X songloft/internal/version.GitCommit=$(GIT_COMMIT) \
    -X songloft/internal/version.BuildTime=$(BUILD_TIME) \
    $(if $(BUILD_TYPE),-X songloft/internal/version.BuildType=$(BUILD_TYPE)) \
    $(if $(TRACELY_APP_ID),-X songloft/internal/tracelycfg.AppID=$(TRACELY_APP_ID)) \
    $(if $(TRACELY_APP_SECRET),-X songloft/internal/tracelycfg.AppSecret=$(TRACELY_APP_SECRET)) \
    $(if $(TRACELY_HOST),-X songloft/internal/tracelycfg.Host=$(TRACELY_HOST))

View the injected results at runtime via songloft -version:

Songloft Version: 2.10.0
Git Commit: a102490
Build Time: 2026-06-12T10:00:00Z
Build Type: full

Section sources


3. Docker Deployment

3.1 Multi-Stage Build

The Dockerfile uses a two-stage build:

Stage one: go-builder (golang:1.26-alpine)

  • Installs the gcc musl-dev make upx git build toolchain (with retry)
  • Copies go.mod/go.sum first to download dependencies (leveraging Docker layer caching for acceleration)
  • Then copies the source code, using --mount=type=cache to cache Go build artifacts and the module directory
  • Chooses make build-prod or make build-prod-lite based on the LITE_BUILD parameter

Stage two: runtime (alpine:latest)

  • Installs ca-certificates tzdata alsa-lib alsa-plugins alsa-utils alsa-ucm-conf (ALSA runtime libraries)
  • Copies ffmpeg and ffprobe from the hanxi/ffmpeg image to /bin/
  • Copies the compiled songloft binary from go-builder to /app/songloft
  • Copies docker-entrypoint.sh to /app/
  • Default time zone Asia/Shanghai, exposes port 58091

Diagram sources

3.2 Volume Mounts and Environment Variables

The Docker image defines two VOLUMEs:

Mount PointPurposeRecommendation
/app/musicMusic file storage directory-v /your/music/path:/app/music
/app/dataApplication data (database, cache, plugins, running binary)-v /your/data/path:/app/data

Built-in environment variables:

VariableDefaultDescription
ADMIN_USERNAMEadminAdministrator username
ADMIN_PASSWORDadminAdministrator password
IN_DOCKERtrueIdentifies the Docker environment, enabling Docker-specific features such as upgrade checks
LISTEN_PORT58091Listening port
DB_PATHdata/songloft.dbDatabase file path
BASE_PATHemptyReverse proxy sub-path prefix

Typical docker run command:

bash
docker run -d \
  --name songloft \
  -p 58091:58091 \
  -v /data/music:/app/music \
  -v /data/songloft:/app/data \
  -e ADMIN_USERNAME=myuser \
  -e ADMIN_PASSWORD=mypassword \
  songloft/songloft:latest

Section sources

3.3 docker-entrypoint.sh Hot-Replacement Rules

The base binary in the Docker image is located at /app/songloft, while the actually running binary is located at /app/data/songloft in the persistent data volume. Each time the container starts, the entrypoint script decides whether to overwrite the binary in the data directory with the base binary according to the following rules:

Core principle: the base binary represents the user's intent; when dev/release or full/lite differ, use the base binary to overwrite. Only when "same channel + same BUILD_TYPE" is old/new comparison performed: dev by Build Time, release by version number.

ScenarioActionReason
dev ↔ release channel differsReplaceThe user switched image channels
BUILD_TYPE differs (full and lite swapped)ReplaceThe user switched image variants
Both dev + same type + base Build Time > data Build TimeReplacedev rolling builds pick the latest by build time
Both dev + same type + data Build Time >= base Build TimeDon't replacedata may have been upgraded online via API
Both release + same type + base version > data versionReplaceProduction version upgrade
Both release + same type + data version >= baseDon't replacedata may have been upgraded online via API

Hot-replacement flow:

  1. First startup (no binary in the data directory): copy directly from the base binary
  2. Subsequent startups: obtain both version numbers and BUILD_TYPE via songloft -version
  3. When replacement conditions are met: first back up the old version to /app/songloft.backup, then copy the base binary
  4. When replacement conditions are not met: keep the binary in the data directory (which may be a newer version after an online upgrade)
  5. Finally exec /app/data/songloft "$@" to start the service

Version comparison uses awk to compare the numeric parts segment by segment (automatically stripping suffixes like -beta); dev and unknown versions do not participate in numeric comparison.

Section sources


4. Standalone Binary Deployment

When Docker is not needed, you can directly download or compile the binary and run it.

4.1 CLI Arguments

./songloft [arguments]
ArgumentDefaultDescription
-port58091Listening port
-dbdata/songloft.dbDatabase file path
-usernameadminAdministrator username
-passwordadminAdministrator password
-base-pathemptyReverse proxy sub-path prefix (e.g. /songloft)
-version--Display version information and exit
-help--Display help information and exit

Argument precedence: CLI arguments > environment variables > defaults. All CLI arguments have corresponding environment variables (LISTEN_PORT, DB_PATH, ADMIN_USERNAME, ADMIN_PASSWORD, BASE_PATH).

4.2 Startup Examples

bash
# Simplest startup (using all defaults)
./songloft

# Custom port and credentials
./songloft -port 8080 -username myuser -password mypassword

# Specify database path
./songloft -db /var/lib/songloft/songloft.db

# Sub-path deployment
./songloft -base-path /music

4.3 Data Directory Structure

After startup, the following directory structure is automatically created (relative to the directory where the database file resides):

data/
├── songloft.db         # SQLite database (WAL mode)
├── covers/             # Cover art storage
├── jsplugins/          # JS plugin installation directory
├── jsplugins_data/     # JS plugin data directory (storage API persistence)
└── music_cache/        # Remote music cache directory (path can be customized)

Section sources


5. Reverse Proxy and Sub-Path Deployment

When Songloft is deployed behind a reverse proxy (Nginx, Caddy, Traefik, etc.) and needs to be mounted on a sub-path (e.g. https://example.com/songloft/), you need to configure BASE_PATH.

5.1 Backend Handling

Configured at startup via -base-path /songloft or the environment variable BASE_PATH=/songloft. The normalizeBasePath function performs the following normalization:

  • Ensures it starts with /
  • Strips the trailing /
  • Rejects paths containing ?, #, or ..

Route mounting in the Start method:

go
if a.config.BasePath != "" {
    mux := http.NewServeMux()
    mux.Handle(a.config.BasePath+"/", http.StripPrefix(a.config.BasePath, a.router))
    mux.HandleFunc(a.config.BasePath, func(w http.ResponseWriter, r *http.Request) {
        http.Redirect(w, r, a.config.BasePath+"/", http.StatusMovedPermanently)
    })
    handler = mux
}

http.StripPrefix strips the sub-path prefix at the outermost layer, and the internal routing (Chi router) always works starting from /, with no need to be aware of the sub-path. Accessing /songloft (no trailing slash) automatically 301-redirects to /songloft/.

5.2 Frontend base href Injection

embed.go replaces <base href="/"> in the embedded index.html with <base href="/songloft/"> at runtime:

go
if a.config.BasePath != "" {
    indexBytes = bytes.Replace(
        indexBytes,
        []byte(`<base href="/">`),
        []byte(`<base href="`+a.config.BasePath+`/">`),
        1,
    )
}

The replaced index.html regenerates the pre-compressed versions (Brotli/Gzip), ensuring the compression cache is consistent with the content. The frontend embedded mode automatically detects the sub-path via Uri.base.path, with no additional configuration required.

5.3 Nginx Configuration Example

nginx
location /songloft/ {
    proxy_pass http://127.0.0.1:58091/songloft/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    
    # WebSocket support (if needed)
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

Note: the reverse proxy should pass the complete sub-path (including the prefix) to the backend, which will StripPrefix on its own.

Section sources


6. Online Upgrade System

Online upgrade is available only in the Docker environment (determined by the IN_DOCKER=true environment variable).

6.1 Upgrade Channels

Channelversion.json AddressDescription
stablegithub.com/.../releases/latest/download/version.jsonProduction version (latest stable Release)
devgithub.com/.../releases/download/dev/version.jsonDevelopment version (rolling builds from the main branch)

CheckForUpdates only checks the currently running channel: dev only checks dev, release only checks stable. dev determines whether there is an update by build_time; release determines whether there is an update by version number. Online upgrade does not allow switching between dev/release, nor between full/lite.

6.2 Upgrade Flow

UpgradeBinary executes a 6-step flow, updating the progress status at each step for frontend polling:

1. FetchVersionInfo   -- Fetch target version info (version.json)
2. DownloadBinary     -- Download the new version to /app/data/songloft.new (with progress callback)
3. TestBinary         -- After chmod +x, run -help to test usability
4. backupCurrentBinary -- Back up the current version to /app/data/songloft.backup
5. os.Rename          -- Atomically replace songloft.new -> songloft
6. os.Exit(0)         -- Exit after a 5-second delay; the Docker restart policy automatically brings up the new version

Upgrade progress state machine:

idle -> downloading -> testing -> replacing -> restarting
                \         \          \
                 \---------\----------\--> failed

At download time, the platform suffix (such as -linux-amd64 or -linux-amd64-lite) is automatically appended based on the base binary's BUILD_TYPE, ensuring the build type after the upgrade matches the base binary.

6.3 Rollback to the Base Binary

ResetToBaseImage provides one-click rollback: it copies /app/songloft (the original base binary in the Docker image) back to /app/data/songloft, then restarts the service. The flow is similar to the upgrade (back up -> write temp file -> atomic replace -> exit).

6.4 GitHub Proxy

The upgrade service supports the proxyPrefix parameter for GitHub mirror acceleration. The proxy prefix works by URL concatenation (e.g. https://ghproxy.com/ + the original URL). In addition, a global HTTP proxy (HTTP/HTTPS/SOCKS5) can be configured via /api/v1/settings/http-proxy; the two can coexist: first concatenate the mirror prefix, then forward through the HTTP Proxy.

Section sources


7. Tracely Monitoring

Songloft integrates the Tracely monitoring client to collect anonymous install/upgrade events and heartbeat data. This feature uses an opt-in model: it is enabled only when the three values AppID, AppSecret, and Host are injected at compile time.

7.1 Compile-Time Injection

bash
make build-prod \
  TRACELY_APP_ID=your-app-id \
  TRACELY_APP_SECRET=your-secret \
  TRACELY_HOST=https://tracely.example.com

The injection writes into the three variables of the internal/tracelycfg package via -ldflags -X. The Enabled() function returns true when all three are non-empty.

7.2 Runtime Behavior

Once enabled, the Tracely client is initialized in the Init method of app.go:

  • Heartbeat: reported once every 60 seconds, carrying the version tag
  • Install event: reported on first startup (tracely_reported_version is empty), including the version number, platform (linux-amd64), and hostname
  • Upgrade event: reported when the version number changes, including the old version and the new version
  • After reporting, the current version is written into the tracely_reported_version config for persistence, avoiding duplicate reporting

Open-source builds do not inject these three values by default, so the Tracely client is not initialized and produces no network requests.

Section sources


8. Security Considerations

8.1 Default Credentials

Songloft's default administrator account and password are both admin/admin. When started with the default credentials, the log outputs a clear notice:

Starting with default administrator account and password
Default administrator account: admin, default password: admin

The AppConfig.UsingDefaultCredentials boolean marks whether the default credentials are currently in use. Production environments should set a strong password via the -username/-password arguments or the ADMIN_USERNAME/ADMIN_PASSWORD environment variables.

8.2 Automatic JWT Secret Generation

On first startup, initJWTSecret checks whether jwt_secret already exists in the database configs table. If not, it calls GenerateSecret() to generate a 32-byte (256-bit) random secret and persists it to the database:

go
func GenerateSecret() (string, error) {
    bytes := make([]byte, 32)
    if _, err := rand.Read(bytes); err != nil {
        return "", err
    }
    return hex.EncodeToString(bytes), nil
}

The JWT secret is stored in the SQLite database (rather than a config file) and persisted together with the data volume. Subsequent startups directly read the existing secret, ensuring tokens remain valid across restarts.

8.3 SSRF Protection

Features that involve the server initiating HTTP requests, such as the HLS proxy, use two lines of SSRF defense:

First line: same-origin validation -- the HLS proxy endpoint checks at entry whether the request URL's scheme+host+port strictly equals the song's original URL, keeping non-same-origin URLs unrewritten to avoid becoming an open proxy.

Second line: private network blocking -- services.IsHostnameAllowed uses a blacklist strategy, blocking access to the following addresses:

  • localhost and .local suffix domains
  • Loopback addresses (127.0.0.0/8, ::1)
  • Private addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
  • Link-local addresses (169.254.0.0/16, fe80::/10)
  • Unspecified addresses (0.0.0.0, ::)

Domains that fail to resolve are allowed through, letting the subsequent HTTP request report the error on its own.

8.4 Plugin Sandbox Isolation

JS plugins run in a QuickJS sandbox, with limited host bridge capabilities (http.fetch, storage, logger) provided through internal/jsruntime. A plugin's permissions are declared by the permissions field in the manifest and validated at runtime by internal/jsplugin. Calls to undeclared permissions are rejected.

8.5 Global HTTP Proxy

internal/httputil/proxy.go maintains the global proxy configuration. Loopback addresses (localhost, 127.0.0.1, ::1) automatically skip the proxy, preventing internal requests from being forwarded to an external proxy server. When the proxy configuration changes, sharedTransport.CloseIdleConnections() is called to clean up old connections.

Section sources


9. Platform Adaptation Notes

Songloft's Flutter frontend covers 6 platforms; the following are the known adaptation points for each platform.

9.1 macOS

  • secure_storage (Flutter Secure Storage) cannot use the Keychain in an unsigned sandbox environment
  • Automatically degrades to SharedPreferences for token storage, which is less secure but functional
  • Official distribution should be signed with an Apple Developer certificate to enable the Keychain

9.2 Android

  • Before building, run sdkmanager --licenses to accept all license agreements
  • Android 13 (API 33) and above require requesting notification permission (POST_NOTIFICATIONS) at runtime, otherwise the foreground service notification will not display
  • ROMs with aggressive background management such as HyperOS3 need androidStopForegroundOnPause: false set to prevent the foreground service from being reclaimed on pause, which would interrupt playback

9.3 Windows / Linux

  • The audio playback backend uses just_audio_media_kit, which relies on libmpv underneath
  • On Windows, the libmpv DLL must be distributed with the app or ensured to be findable in the system PATH
  • On Linux, libmpv must be installed (on most distributions, installing mpv via the package manager suffices)

9.4 Docker-Specific

  • The online upgrade check (/api/v1/upgrade/check) is available only when IN_DOCKER=true
  • The ALSA user-space libraries (alsa-lib, alsa-plugins, etc.) are installed inside the container, resolving the "No such file or directory" error when MPD opens the ALSA device
  • The default time zone is set to Asia/Shanghai, which can be overridden via -e TZ=xxx

Section sources


10. Operations Quick Reference

10.1 Common Environment Variables

VariableDefaultDescription
ADMIN_USERNAMEadminAdministrator username
ADMIN_PASSWORDadminAdministrator password
LISTEN_PORT58091Listening port
DB_PATHdata/songloft.dbDatabase file path
BASE_PATHemptyReverse proxy sub-path prefix
IN_DOCKERtrue (inside Docker)Enables Docker-specific features
TZAsia/ShanghaiContainer time zone

10.2 Key API Endpoints

EndpointMethodDescription
/api/v1/upgrade/checkGETCheck for available updates (Docker only)
/api/v1/upgrade/executePOSTExecute the upgrade
/api/v1/upgrade/progressGETQuery the upgrade progress
/api/v1/upgrade/resetPOSTRoll back to the base binary version
/api/v1/settings/http-proxyGET/PUTGlobal HTTP proxy configuration
/api/v1/settings/hls-proxyGET/PUTHLS proxy toggle
/api/v1/cache-manage/configGET/PUTMusic cache configuration
/api/v1/cache-manage/statsGETCache statistics
/api/v1/settings/log-levelGET/PUTRuntime log level switching

10.3 Troubleshooting

SymptomPossible CauseSolution
Startup fails after upgradeThe new version binary is corruptedRestart the container to trigger the entrypoint base-binary rollback, or call /api/v1/upgrade/reset
Upgrade check returns 403GitHub API rate limiting or network issuesConfigure an HTTP proxy (/settings/http-proxy) or a GitHub mirror
Cross-device rename failsThe cache directory and /tmp are not on the same file systemThe project has a built-in moveFile that automatically falls back to copy+remove; no manual handling needed
SQLITE_BUSY errorConcurrent write transaction conflictCheck whether multiple processes are accessing the same database file, and ensure a single instance runs
Sub-path deployment 404Inconsistent BASE_PATH configurationEnsure the backend -base-path matches the path prefix configured in the reverse proxy

Section sources