login as:
~/abapcraft.dev — code, crafted in SAP
florin@abapcraft:~/abap/posts/fiori-to-do-in-synology-docker/tutorial.md $ cat tutorial.md
MARKDOWN 267 lines

Building a Fiori To-Do App and Deploying it to Synology NAS

This is a full walkthrough of building a real SAP Fiori application from scratch — a To-Do app with a REST backend — and deploying it as a Docker container on a Synology NAS. No SAP backend required. No BTP account. Just Node.js, OpenUI5, and a NAS you already have.

By the end you will have:

  • A dark-themed Fiori app with routing, data binding, a detail page, and due dates
  • A json-server REST backend that persists data to a file
  • Two Docker images running on your Synology, accessible from your local network

Prerequisites

  • Node.js 22+ and npm — install via NodeSource
  • @ui5/cli and @sap/ux-ui5-tooling — installed below
  • Dockercurl -fsSL https://get.docker.com | sudo sh
  • A Synology NAS with Container Manager installed

1. Project structure

Create a folder and initialise it:

mkdir fiori-todo-app && cd fiori-todo-app

The final layout will be:

fiori-todo-app/
├── db.json                   # json-server database
├── package.json
├── ui5.yaml                  # UI5 Tooling config
├── Dockerfile                # App image (nginx)
├── Dockerfile.api            # API image (json-server)
├── nginx.conf
├── docker-compose.yml        # For building locally
├── docker-compose.synology.yml
├── start-api.sh
└── webapp/
    ├── index.html
    ├── Component.js
    ├── manifest.json
    ├── view/
    │   ├── App.view.xml
    │   ├── Main.view.xml
    │   └── Detail.view.xml
    ├── controller/
    │   ├── App.controller.js
    │   ├── Main.controller.js
    │   └── Detail.controller.js
    ├── i18n/
    │   └── i18n.properties
    └── css/
        └── style.css

2. package.json and tooling

package.json →

The scripts section wires up the dev experience: npm run dev starts both the UI5 dev server and json-server in parallel via concurrently.

Install:

npm install

3. ui5.yaml

ui5.yaml →

This file tells the UI5 toolchain which libraries to use, how to serve the app locally, and how to proxy API requests so you don't hit CORS issues in development.

The backend entry is key: any request your app makes to /todos gets forwarded by the dev server to http://localhost:3001 (json-server). In production, nginx handles the same proxy.


4. The database

db.json →

json-server turns this JSON file into a full REST API automatically. Save it in the project root. Every mutation (POST, PATCH, DELETE) is written back to this file immediately.


5. webapp/index.html

webapp/index.html →

The entry point. The data-sap-ui-bootstrap script tag loads the UI5 core and wires up the component.

The data-sap-ui-theme="sap_horizon_dark" gives the modern dark Horizon theme. sapUiSizeCompact makes controls smaller and denser — standard for desktop Fiori apps.


6. webapp/manifest.json

webapp/manifest.json →

The app descriptor. Every Fiori app has one. It declares routing, models, dependencies, and CSS. Routing is the most important part here — it maps URL hash patterns to views.

Note on controlId: The router injects views into the <App> NavContainer. The value "app" works correctly when using fiori run or ui5 serve because the tooling controls how component IDs are generated. It does not work with a plain Python or static file server — if you ever switch back to those, you will get a white screen.


7. webapp/Component.js

webapp/Component.js →

The UIComponent is the application's entry point. It creates the central JSONModel, loads todos from the API, and starts the router.

The API URL is /todos — a relative path. The dev server proxies it to json-server; nginx does the same in Docker.


8. The root view and shell controller

App.view.xml is the navigation container. The router injects Main and Detail pages into <App id="app"> at runtime.


9. i18n and CSS

All user-visible text lives in one file. Bind it in views with {i18n>key}.


10. Main view

This is where most of the Fiori concepts live — template binding, expression binding, a formatter, filter tabs, and the delete button.

webapp/view/Main.view.xml →

Key patterns to notice:

  • items="{/todos}" — template binding, creates one row per object in the array
  • class="{= ${done} ? 'todoItemDone' : 'todoItem'}" — expression binding, switches CSS class at runtime
  • {path: 'dueDate', formatter: '.formatDate'} — formatter function converts "2026-06-15""Jun 15, 2026"
  • visible="{= !!${dueDate} }" — expression binding hides the due date row when empty

11. Main controller

Every mutation calls the API immediately. Optimistic updates keep the UI responsive — the model changes first, then the request fires in the background.

webapp/controller/Main.controller.js →


12. Detail view and controller

bindElement (called in the controller) binds the entire view to one specific todo. Every binding — {title}, {done}, {dueDate} — resolves against that object without any path prefix.


13. Run it locally

Start both servers with one command:

npm run dev

Open http://localhost:8080. The app loads, fetches todos from json-server, and you can add, complete, delete, and filter tasks. Every change is written back to db.json automatically.

The fiori-tools-proxy middleware forwards /todos requests from the browser to http://localhost:3001, so there are no CORS issues.


14. Docker — the app image

The Dockerfile uses a two-stage build. Stage one uses Node.js to build the UI5 app (ui5 build -a bundles everything including the UI5 framework files). Stage two copies only the compiled output into a lightweight nginx image.

nginx does two things: serves the static UI5 app, and proxies /todos to the json-server container. The hostname api is the Docker Compose service name — Docker's internal DNS resolves it automatically.


15. Docker — the API image

json-server needs --host 0.0.0.0 to listen on all interfaces inside the container, not just localhost. The entrypoint script seeds db.json on the first run if the mounted volume is empty.


16. Docker Compose

docker-compose.yml is for building and running locally. docker-compose.synology.yml references pre-imported images by name — no build context — for use in Synology Container Manager.


17. Build the images and export

Run these on your development machine (Docker must be installed):

# Build both images
docker build -t fiori-todo-app:latest -f Dockerfile .
docker build -t fiori-todo-api:latest -f Dockerfile.api .

# Export to compressed tar archives
docker save fiori-todo-app:latest | gzip > fiori-todo-app.tar.gz
docker save fiori-todo-api:latest | gzip > fiori-todo-api.tar.gz

Each archive will be around 60 MB.


18. Deploy to Synology

  1. Copy both .tar.gz files to your Synology (via File Station or scp)
  2. Open Container Manager → Image → Add → Import from file
  3. Import fiori-todo-app.tar.gz → appears as fiori-todo-app:latest
  4. Import fiori-todo-api.tar.gz → appears as fiori-todo-api:latest
  5. Go to Container Manager → Project → Create
  6. Paste the contents of docker-compose.synology.yml
  7. Click Deploy

The app will be available at http://your-synology-ip:8080.

The todo-data Docker volume persists db.json across container restarts and image updates. Your tasks survive everything except explicitly deleting the volume.


Key concepts summary

Concept Where it appears
JSONModel + two-way binding Component.js creates the model; {done} on CheckBox syncs automatically
Template binding items="{/todos}" — one row per array entry
Expression binding class="{= ${done} ? 'todoItemDone' : 'todoItem'}"
Formatter function formatDate() converts "2026-06-15""Jun 15, 2026"
sap.ui.model.Filter _applyFilter() filters the list binding without touching model data
Routing + navTo Defined in manifest.json; Main.controller.js calls router.navTo("detail", { id })
bindElement Detail.controller.js binds the whole view to one todo object
DatePicker valueFormat / displayFormat Storage format vs display format are kept separate
fetch with optimistic updates Model updates immediately; API call fires in background
nginx reverse proxy Single port for app + API; Docker service name used as hostname
Multi-stage Docker build Node.js builds the app; nginx serves it — final image has no Node.js