A Docker setup for cross-compiling Synology packages
  • Dockerfile 67.5%
  • Shell 32.5%
Find a file
2026-09-11 13:36:39 +10:00
.gitignore Initial setup, untested 2026-09-09 15:57:42 +10:00
build.sh Initial setup, untested 2026-09-09 15:57:42 +10:00
docker-compose.yml Initial setup, untested 2026-09-09 15:57:42 +10:00
Dockerfile all-supported builds cleanly now 2026-09-10 16:31:11 +10:00
README.caddy Include instruction to build all-supported 2026-09-11 13:36:39 +10:00
README.md Improved readability of the README 2026-09-10 16:48:16 +10:00

Docker for building SynoCommunity packages

Dockerized build environment for producing Synology .spk packages with spksrc.

This wraps SynoCommunity's official prebuilt image (ghcr.io/synocommunity/spksrc) rather than reimplementing its ~50-package build environment - see their Docker setup docs. The local Dockerfile only adds a host-UID-matched user so build output isn't root-owned on the host.

Requirements for the build machine:

  • Docker Desktop or Docker Engine + the docker compose plugin, with linux/amd64 support (on Apple Silicon this runs under emulation - fine for building, just slower).

Set up to create the Docker container:

You need Docker installed on the build machine first. If that's not this machine, copy the wrapper files (Dockerfile, docker-compose.yml, build.sh, .gitignore, README.md) to the target machine, then set up spksrc there.

A plain clone of upstream is enough just to build for local use:

git clone https://github.com/SynoCommunity/spksrc.git spksrc

If you intend to submit a package upstream, fork SynoCommunity/spksrc on GitHub first and clone your fork instead, adding upstream for staying in sync:

git clone https://github.com/<you>/spksrc.git spksrc
cd spksrc
git remote add upstream https://github.com/SynoCommunity/spksrc.git
cd ..

Layout

  • spksrc/ - a clone (or fork) of spksrc (which is gitignored here; it's its own git repo).
  • Dockerfile, docker-compose.yml, build.sh - the container wrapper.

First-time setup

chmod +x build.sh   # if the executable bit didn't survive the copy
./build.sh make setup

This generates spksrc/local.mk with default toolchain configuration.

Find your TCVERSION

TCVERSION has to exactly match one of the toolchain directories spksrc ships - not just "your NAS's DSM version" loosely. Get the real value from the toolchains present for your arch, inside the container:

ls toolchain/ | grep -E '^syno-x64-'

DSM's own version string can be more precise than the toolchain name - /etc.defaults/VERSION's productversion may read e.g. 7.3.2, but the toolchain directory (and thus TCVERSION) is just syno-x64-7.3. Match against the ls output above rather than pasting the NAS's version verbatim.

Using TCVERSION=7.3.2 will fail with No such file or directory for toolchain/syno-x64-7.3.2 since that directory doesn't exist. (Older DSM releases are the exception: 6.2.4 really is the full toolchain name there - again, let the ls output be the source of truth.)

Test build

./build.sh make -C spk/transmission ARCH=x64 TCVERSION=<your version, e.g. 7.2>

If it completes, the environment works. The output .spk files are in spksrc/packages/, which is bind-mounted back from Docker to the host.

ARCH selection: x64 vs denverton

Most packages (anything that isn't a kernel module and doesn't need kernel headers) build as generic ARCH=x64 - spksrc only builds a platform-specific variant like ARCH=denverton when a package actually depends on kernel sources. Default to x64; only reach for denverton if a package's Makefile requires it (you'll see it fail asking for kernel headers otherwise).

Building a new package

This container setup doesn't decide how you do this. See spksrc's Your First Package and Package Anatomy guides for creating a new spk/<name>/Makefile. Once one exists:

./build.sh make -C spk/<name> ARCH=x64 TCVERSION=<your version>

Creating a new package

There's no scaffolding generator in spksrc - new packages are created by hand, starting from an existing one that resembles what you're building. Two directories are involved, and you may only need one of them:

  • cross/<name>/ - a source package: fetches and cross-compiles something (a C/C++ project, a library, anything built from a tarball). Only needed if you're compiling your own or upstream source code.
  • spk/<name>/ - the installable package: assembles one or more cross/ outputs (or, for a pure script/self-contained binary, nothing external at all) into the final .spk, plus the service script, icon, and install wizard.

Some real examples already in spksrc/ are worth copying from:

Example What it shows
spksrc/cross/tree/ Minimal cross/ source package (plain Makefile build, no autotools)
spksrc/spk/demoservice/ Minimal spk/ package with no upstream source at all - a hand-written src/start.sh script wired up as a background service. Best starting point if you're writing your own program from scratch rather than packaging existing software.
spksrc/spk/transmission/ Full real-world example: cross/transmission dependency, service lifecycle hooks, install wizard, settings file

Option A - packaging your own script/program (no cross/ needed)

  1. mkdir -p spksrc/spk/<name>/src
  2. Put your program in spksrc/spk/<name>/src/ (e.g. start.sh, a Python script, a prebuilt static binary - whatever runs on the NAS).
  3. Copy spksrc/spk/demoservice/Makefile to spksrc/spk/<name>/Makefile and edit: SPK_NAME, SPK_VERS, SPK_REV, MAINTAINER, DESCRIPTION, DISPLAY_NAME, LICENSE, SERVICE_PORT/SERVICE_PORT_TITLE if it listens on a port. Replace the _pre_copy/_extra_install targets at the bottom with install commands that copy your files from src/ into $(STAGING_DIR).
  4. Write spksrc/spk/<name>/PLIST - one line per installed file, e.g. bin/start.sh:bin/start.sh (<install path>:<source in $(STAGING_DIR)>).
  5. Copy spksrc/spk/demoservice/src/service-setup.sh as a starting point if your program needs to run as a background service (defines SERVICE_COMMAND, and hooks like service_postinst()).
  6. Add a package icon at src/<name>.png and reference it via SPK_ICON.

Option B - packaging/compiling source code (needs cross/)

  1. mkdir -p spksrc/cross/<name>
  2. Copy spksrc/cross/tree/Makefile as a starting point and set PKG_NAME, PKG_VERS, PKG_DIST_SITE/PKG_DIST_FILE (where the source tarball comes from), DEPENDS (other cross/ packages this one needs), and the include ../../mk/spksrc.cross-*.mk line matching its build system - spksrc.cross-cc.mk (plain Makefile), spksrc.cross-configure-make.mk (autotools), spksrc.cross-cmake.mk, spksrc.cross-python3.mk, etc. (see other cross/*/Makefiles using the same upstream build system for the right one).
  3. Generate the checksum file once the Makefile can fetch the source:
    ./build.sh make -C cross/<name> digests
    
  4. Add a cross/<name>/PLIST if the build's default install layout needs remapping, and any patches/*.patch the source needs.
  5. Build it standalone to iterate: ./build.sh make -C cross/<name> ARCH=x64 TCVERSION=<your version>
  6. Then create the spk/<name>/ wrapper as in Option A, but set DEPENDS = cross/<name> in its Makefile instead of writing install commands by hand - the SPK build pulls the compiled output straight from the cross/ package's staging output.

Submitting a package to SynoCommunity

Publishing itself is automatic - SynoCommunity's CI builds and publishes a package once its PR merges, and a repo admin activates it. What's on you is getting a working, tested PR in front of them:

  1. Keep your fork current before starting:

    cd spksrc
    git checkout master && git fetch upstream && git merge upstream/master && git push origin master
    cd ..
    
  2. Branch: add-<name> for a new package, <name>-<version> for a version bump of an existing one.

  3. Test it - build for every architecture/DSM combination you can reasonably reach, install the resulting .spk on a real NAS via Package Center → ⚙️ (Manual Install), and work through their checklist:

    • Service starts and stops from Package Center
    • Log/data files land under /volume1/@appdata/<name>/ (the DSM 7 path for ${SYNOPKG_PKGVAR})
    • Any bundled commands are on PATH via /usr/local/bin and their --version/--help work
    • Uninstall removes /var/packages/<name>/ cleanly
  4. Ensure that all-supported builds cleanly

The PR rules require all-supported to run correctly. This target builds the package for every architecture/DSM-toolchain combination spksrc knows about, not just the ones your package actually supports, so "it finished" isn't the same as "it passed". See below for more detail:

./build.sh make -C spk/<name> all-supported

(Don't use ARCH=/TCVERSION= - it iterates those internally. You must make setup first, the same as any other build.)

The takes quite a while and download several toolchains the first time. all-latest (latest DSM toolchain per arch only) is a faster sanity check beforehand, but the PR rule specifically asks for all-supported.

  1. Commit messages and the PR title are both prefixed with the package's DISPLAY_NAME: <Display Name>: <what and why>. The PR description should explain why, not just restate the diff, and should include what you tested it on (NAS model, architecture, DSM version).

  2. Push the branch to your fork and open the PR against SynoCommunity/spksrc:master.

See SynoCommunity's own Contributing guide for the authoritative version of this process.

Checking all-supported was ok

Two things make the exit code of all-supported unreliable here:

  • Its target list is spksrc's global SUPPORTED_ARCHS - every combo it has a toolchain for, including any your package deliberately excludes via UNSUPPORTED_ARCHS or REQUIRED_MIN_DSM.
  • Each combo is invoked with a -@ prefix (Make's "ignore this recipe's failure and keep going"), so an excluded combo hitting your package's own precheck rejection ("is not a supported architecture" / "is lower than <version>") is silently swallowed - all-supported reports overall success regardless of how many combos failed underneath.

So check what it actually did, not just whether it finished. Logs land in the package directory itself (LOG_DIR defaults to $(CURDIR)):

  • spk/<name>/status-build.log - one BEGIN/END line pair per arch-version attempted; scan this first (grep for FAILED)
  • spk/<name>/build-<arch>-<tcversion>.log - the full output for one specific combo, for when a status line looks wrong.
  • packages/<name>_*.spk - the actual proof: list these afterward and confirm they match exactly the combos you expect to succeed.

A quick first pass:

grep -vE "is not a supported architecture|is lower than [0-9.]+" spk/<name>/status-build.log

Anything left over that isn't a clean BEGIN/END pair is a real failure to chase down via its build-<arch>-<tcversion>.log.

Reference

Get an interactive shell inside Docker:

./build.sh