SynoSail: run Laravel on a Synology NAS with a Docker GUI manager

Deploying Laravel to a Synology NAS should be easy: build an image, paste a compose file into Dockhand or Portainer, and press start. In practice, DSM's strict folder permissions and its older kernel break most setups the moment a container starts. SynoSail is the package I made so I never have to fight that again, and this blog runs on it.

What it is#

SynoSail is a Composer package for Laravel projects. It adds a complete Docker stack to your project, built for Synology and for GUI Docker managers like Dockhand, Dockge, Arcane and Portainer. I run it on a DS723+ with DSM 7.4.

The stack has four services:

  • init runs once before the others. It copies the Nginx config, PHP config and compiled public files from your image into a data folder on the NAS.
  • db is MariaDB.
  • app is your Laravel app on PHP-FPM.
  • nginx serves the site on the port you set in APP_PORT (8082 by default).

Step 1: Install the package#

In your Laravel project, run:

composer require --dev smith89k/syno-sail

Step 2: Publish the Docker files#

For a first-time install, run:

php artisan syno-sail:install

This copies these files into your project root:

  • Dockerfile, used to build your image
  • docker-compose.yml, the stack you deploy on the NAS
  • docker/, with the Nginx and PHP configs and the backup script
  • .env.docker.example, a template for your production settings
  • .dockerignore
  • .github/, with a GitHub Actions workflow that builds and pushes your image

When you update the package later, run the same command with --force to get the newest versions of these files. It overwrites any changes you made to them.

php artisan syno-sail:install --force

Step 3: Build and push the image#

The included workflow builds your image and pushes it to GitHub Container Registry (GHCR) when you push to the main branch. Before the first run, add a repository secret named GHCR_PAT holding a personal access token with the write:packages permission.

To build on your own machine instead:

docker build -t your-registry/your-app:latest -f Dockerfile .
docker push your-registry/your-app:latest

Step 4: Create the stack, but don't start it#

Open Dockhand (or Dockge, Arcane or Portainer) and create a new stack:

  1. Paste the contents of docker-compose.yml into the compose editor.
  2. Paste the contents of .env.docker.example into the .env editor, and fill in your values: APP_KEY, the database details and your image address on the registry.
  3. Save the stack, but don't start it yet.

Step 5: Create the data folder#

Synology is strict about who can create files in Docker folders, so make the data folder by hand before the first start:

  1. Open File Station in DSM.
  2. Go to the folder where your stack was saved, for example /volume1/docker/dockhand/stacks/<user>/<project>/.
  3. Create a folder named data.
  4. Right-click it, choose Properties, and open the Permission tab.
  5. Give Everyone Read & Write, and tick Apply to this folder, sub-folders and files.

Step 6: Start the stack#

Go back to your Docker manager and click Start (or Deploy). The init container fills the data folder, then the database, app and web server start. If you skip step 5, init can't write to data and the site won't come up.

Why seccomp is unconfined#

Every service sets security_opt: seccomp=unconfined. DSM runs an older Linux kernel, and its default seccomp profile blocks newer system calls such as clone3. Modern Alpine and Debian images use them, so containers crash as soon as they start, often without a useful log. Turning the profile off for these containers lets them run normally.

Nightly backups#

The stack comes with backup.sh, which dumps the database, compresses the storage folder and keeps seven days of backups. They're saved outside the stack, in /volume1/docker/backups/<project>, so removing or recreating the stack doesn't delete them. Set BACKUP_DIR in .env to save them somewhere else.

To run it every night:

  1. Open Control Panel in DSM and go to Task Scheduler.
  2. Choose Create > Scheduled Task > User-defined script.
  3. On General, name it "Laravel Backup" and set the user to root.
  4. On Schedule, run it daily at 2:00 AM.
  5. On Task Settings, put this in Run command, using your own stack folder:
cd /volume1/docker/dockhand/stacks/<user>/<project>
BACKUP_DIR=/volume1/docker/backups/<project> DEPLOY_PATH=$(pwd) sh ./data/scripts/backup.sh

Get it#

SynoSail is free and MIT-licensed. The code and full instructions are on GitHub.