Deployment and Operations
This document is based on the following source files:
- Dockerfile -- Multi-stage Docker build definition
- scripts/docker-entrypoint.sh -- Container startup entry point and hot-replacement logic
- internal/services/upgrade_service.go -- Online upgrade service (download/test/atomic replace/rollback)
- internal/app/app.go -- Application initialization, config parsing, Start method
- internal/app/embed.go -- SPA static file serving and base href injection
- internal/config/types.go -- Startup configuration structure AppConfig
- internal/version/version.go -- Version number compile-time injection
- internal/tracelycfg/tracelycfg.go -- Tracely monitoring compile-time injection
- internal/httputil/proxy.go -- Global HTTP proxy configuration
- internal/services/whitelist.go -- SSRF protection (private network blocking)
- internal/services/auth_service.go -- JWT secret generation and authentication
- Makefile -- Build system (VERSION/BUILD_TYPE/LDFLAGS)
Table of Contents
- Introduction
- Build System
- Docker Deployment
- Standalone Binary Deployment
- Reverse Proxy and Sub-Path Deployment
- Online Upgrade System
- Tracely Monitoring
- Security Considerations
- Platform Adaptation Notes
- 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
- Dockerfile:1-99 -- Complete Docker build and runtime definition
- internal/app/app.go:459-483 -- Start method and base-path route mounting
2. Build System
Songloft's build system is controlled by two orthogonal dimensions:
| Dimension | Values | Description |
|---|---|---|
| VERSION | dev / X.Y.Z | Controls whether it is a development version. When dev, the Makefile automatically enables -tags dev, including Swagger UI and pprof |
| BUILD_TYPE | lite / 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
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:
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: fullSection sources
- Makefile:4-26 -- VERSION, BUILD_TYPE, LDFLAGS definitions
- Makefile:100-125 -- The four build targets
- internal/version/version.go:1-24 -- Version number variable definitions
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 gitbuild toolchain (with retry) - Copies
go.mod/go.sumfirst to download dependencies (leveraging Docker layer caching for acceleration) - Then copies the source code, using
--mount=type=cacheto cache Go build artifacts and the module directory - Chooses
make build-prodormake build-prod-litebased on theLITE_BUILDparameter
Stage two: runtime (alpine:latest)
- Installs
ca-certificates tzdata alsa-lib alsa-plugins alsa-utils alsa-ucm-conf(ALSA runtime libraries) - Copies
ffmpegandffprobefrom thehanxi/ffmpegimage to/bin/ - Copies the compiled
songloftbinary from go-builder to/app/songloft - Copies
docker-entrypoint.shto/app/ - Default time zone
Asia/Shanghai, exposes port58091
Diagram sources
- Dockerfile:1-58 -- Build stage
- Dockerfile:59-99 -- Runtime stage
3.2 Volume Mounts and Environment Variables
The Docker image defines two VOLUMEs:
| Mount Point | Purpose | Recommendation |
|---|---|---|
/app/music | Music file storage directory | -v /your/music/path:/app/music |
/app/data | Application data (database, cache, plugins, running binary) | -v /your/data/path:/app/data |
Built-in environment variables:
| Variable | Default | Description |
|---|---|---|
ADMIN_USERNAME | admin | Administrator username |
ADMIN_PASSWORD | admin | Administrator password |
IN_DOCKER | true | Identifies the Docker environment, enabling Docker-specific features such as upgrade checks |
LISTEN_PORT | 58091 | Listening port |
DB_PATH | data/songloft.db | Database file path |
BASE_PATH | empty | Reverse proxy sub-path prefix |
Typical docker run command:
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:latestSection sources
- Dockerfile:84-97 -- VOLUME and ENV declarations
- internal/app/app.go:547-637 -- ParseConfig environment variable parsing
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.
| Scenario | Action | Reason |
|---|---|---|
| dev ↔ release channel differs | Replace | The user switched image channels |
| BUILD_TYPE differs (full and lite swapped) | Replace | The user switched image variants |
| Both dev + same type + base Build Time > data Build Time | Replace | dev rolling builds pick the latest by build time |
| Both dev + same type + data Build Time >= base Build Time | Don't replace | data may have been upgraded online via API |
| Both release + same type + base version > data version | Replace | Production version upgrade |
| Both release + same type + data version >= base | Don't replace | data may have been upgraded online via API |
Hot-replacement flow:
- First startup (no binary in the data directory): copy directly from the base binary
- Subsequent startups: obtain both version numbers and BUILD_TYPE via
songloft -version - When replacement conditions are met: first back up the old version to
/app/songloft.backup, then copy the base binary - When replacement conditions are not met: keep the binary in the data directory (which may be a newer version after an online upgrade)
- 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
- scripts/docker-entrypoint.sh:1-173 -- Complete entrypoint script
- scripts/docker-entrypoint.sh:16-64 -- Version comparison function
- scripts/docker-entrypoint.sh:92-159 -- Hot-replacement decision logic
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]| Argument | Default | Description |
|---|---|---|
-port | 58091 | Listening port |
-db | data/songloft.db | Database file path |
-username | admin | Administrator username |
-password | admin | Administrator password |
-base-path | empty | Reverse 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
# 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 /music4.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
- internal/app/app.go:547-637 -- ParseConfig argument parsing
- internal/app/app.go:510-528 -- showHelp output
- internal/config/types.go:1-12 -- AppConfig structure definition
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:
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:
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
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
- internal/app/app.go:531-544 -- normalizeBasePath normalization
- internal/app/app.go:472-480 -- StripPrefix route mounting
- internal/app/embed.go:39-54 -- base href runtime replacement
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
| Channel | version.json Address | Description |
|---|---|---|
stable | github.com/.../releases/latest/download/version.json | Production version (latest stable Release) |
dev | github.com/.../releases/download/dev/version.json | Development 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 versionUpgrade progress state machine:
idle -> downloading -> testing -> replacing -> restarting
\ \ \
\---------\----------\--> failedAt 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
- internal/services/upgrade_service.go:22-32 -- Version file URL and binary path constants
- internal/services/upgrade_service.go:278-337 -- Complete UpgradeBinary flow
- internal/services/upgrade_service.go:399-456 -- ResetToBaseImage rollback logic
- internal/services/upgrade_service.go:150-200 -- Base binary BUILD_TYPE detection and platform suffix
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
make build-prod \
TRACELY_APP_ID=your-app-id \
TRACELY_APP_SECRET=your-secret \
TRACELY_HOST=https://tracely.example.comThe 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
versiontag - Install event: reported on first startup (
tracely_reported_versionis 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_versionconfig 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
- internal/tracelycfg/tracelycfg.go:1-11 -- Compile-time injection variables and the Enabled check
- internal/app/app.go:325-361 -- Tracely initialization and event reporting
- Makefile:24-26 -- TRACELY_* build parameters
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: adminThe 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:
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:
localhostand.localsuffix 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
- internal/app/app.go:459-465 -- Default credentials notice
- internal/app/app.go:486-509 -- initJWTSecret flow
- internal/services/auth_service.go:85-92 -- GenerateSecret implementation
- internal/services/whitelist.go:1-53 -- Complete SSRF protection implementation
- internal/httputil/proxy.go:41-52 -- Loopback addresses skip the proxy
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 --licensesto 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: falseset 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 onlibmpvunderneath - On Windows, the libmpv DLL must be distributed with the app or ensured to be findable in the system PATH
- On Linux,
libmpvmust be installed (on most distributions, installingmpvvia the package manager suffices)
9.4 Docker-Specific
- The online upgrade check (
/api/v1/upgrade/check) is available only whenIN_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
- Dockerfile:60-69 -- ALSA runtime library installation
- internal/services/upgrade_service.go:55-57 -- Docker environment detection
10. Operations Quick Reference
10.1 Common Environment Variables
| Variable | Default | Description |
|---|---|---|
ADMIN_USERNAME | admin | Administrator username |
ADMIN_PASSWORD | admin | Administrator password |
LISTEN_PORT | 58091 | Listening port |
DB_PATH | data/songloft.db | Database file path |
BASE_PATH | empty | Reverse proxy sub-path prefix |
IN_DOCKER | true (inside Docker) | Enables Docker-specific features |
TZ | Asia/Shanghai | Container time zone |
10.2 Key API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/v1/upgrade/check | GET | Check for available updates (Docker only) |
/api/v1/upgrade/execute | POST | Execute the upgrade |
/api/v1/upgrade/progress | GET | Query the upgrade progress |
/api/v1/upgrade/reset | POST | Roll back to the base binary version |
/api/v1/settings/http-proxy | GET/PUT | Global HTTP proxy configuration |
/api/v1/settings/hls-proxy | GET/PUT | HLS proxy toggle |
/api/v1/cache-manage/config | GET/PUT | Music cache configuration |
/api/v1/cache-manage/stats | GET | Cache statistics |
/api/v1/settings/log-level | GET/PUT | Runtime log level switching |
10.3 Troubleshooting
| Symptom | Possible Cause | Solution |
|---|---|---|
| Startup fails after upgrade | The new version binary is corrupted | Restart the container to trigger the entrypoint base-binary rollback, or call /api/v1/upgrade/reset |
| Upgrade check returns 403 | GitHub API rate limiting or network issues | Configure an HTTP proxy (/settings/http-proxy) or a GitHub mirror |
| Cross-device rename fails | The cache directory and /tmp are not on the same file system | The project has a built-in moveFile that automatically falls back to copy+remove; no manual handling needed |
| SQLITE_BUSY error | Concurrent write transaction conflict | Check whether multiple processes are accessing the same database file, and ensure a single instance runs |
| Sub-path deployment 404 | Inconsistent BASE_PATH configuration | Ensure the backend -base-path matches the path prefix configured in the reverse proxy |
Section sources
- internal/app/app.go:87-92 -- Dynamic log level switching
- internal/services/upgrade_service.go:35-52 -- UpgradeService structure and HTTP client
