English | Deutsch
An admin page for a dcm4chee-arc-light PACS that runs in Docker Compose. It is one static page, served by the nginx that already serves the OHIF viewer, behind oauth2-proxy. A small helper service on the host does what a browser cannot: add disks to the archive, manage iptables rules for the Docker ports and keep an encrypted audit log.
The user interface is in German.
- Disks of the archive with fill level and free space, and which disk gets new images now and which ones follow.
The order is the write list of the archive AE (
dcmObjectStorageIDanddcmMetadataStorageID). BelowSTORAGE_WARN_GB(default 300 GB) left up to the threshold, the page asks for a new disk. - Write list:
- "In Schreibliste aufnehmen" adds a disk at the end of the list.
- "Aus Schreibliste nehmen" removes a disk from the list. Its images stay readable. The page hides the button on
the last disk that takes new images, and
pacs-storage.shrefuses to remove the last storage that is not marked full. - "Markierung entfernen" clears the full mark (
dcmStorageThresholdExceeded) that dcm4chee sets at the threshold. - Adding and clearing need at least
STORAGE_FREE_GBfree on the disk (default 50 GB). The page shows the two buttons only on disks with at least that much free space (frei_gbinstatus.json), andpacs-storage.shchecks it again.
- New disk: give the server (or VM) a blank disk. It shows up within 5 minutes, or at once with "Jetzt suchen". After
you confirm,
pacs-storage.shformats it as ext4, mounts it underSTORAGE_BASE, adds it to/etc/fstab, creates the storage in LDAP as a copy of the current one and appends it to the write list. The page shows each step and the script output. - Containers of the dcm4chee Compose project, running or stopped.
- Firewall tile: on or off, default mode, number of newly detected sources.
- The last 15 changes made through the page, with account.
- Search by patient ID, name, accession number and date range. Newest first, up to 200 results.
- Edit description, accession number, study date and referring physician. dcm4chee keeps the old values in the Original Attributes Sequence.
- Move a study to another patient. Name and birth date of the target patient stay as they are.
- Reject a study (
113001^DCM). In the view of rejected studies: restore, or delete permanently. Deleting asks you to type the accession number (or the patient ID) first.
- Search by patient ID and name, up to 100 results.
- Edit name, birth date and sex. The page sends the whole record back, so address and other attributes stay.
- Merge two patient entries.
- iptables rules for every port Docker publishes. Per port one mode: offen (everyone), Probelauf (dry run: everyone gets through, sources not on the list are logged) or gesperrt (only listed IPv4 addresses and networks).
- A default mode for ports without a rule, which also covers ports published later.
- Ports that Docker also publishes on IPv6 get the badge "IPv6 nicht gefiltert", see Limitations.
- DICOM, HL7 and viewer ports are settings (
FW_DICOM_PORTS,FW_HL7_PORTS,FW_WEB_PORTS), for sites that publish DICOM on 104 or the viewer on another port. - "Neu erkannt" lists sources that were seen but are not allowed for their port, with "Erlauben" and "Ausblenden".
Sources come from the dcm4chee
server.log(DICOM and HL7 senders), from the kernel log (connections logged by the rules, last 7 days) and from client PC discovery. - Client PC discovery looks up DNS names built from patterns such as
WS{0001..0400}(FW_CLIENT_NAMES) and suggests one /24 per network for DICOM. Empty pattern: no lookups, and the card is hidden. - Counters of new connections per port and source.
- Changes apply on "Speichern". The service refuses a rule set that would lock the saving PC out of the https port
(443), keeps the previous version in
PACS_DIR/firewall/and goes back to it if applying fails.
- Who searched for studies or patients, who opened which study or report (in the viewer, on this page or in the dcm4chee UI section), and every change made through the page, with account, PC and time.
- Filters: date range, patient ID, patient name, account, PC, type of entry, free text. The same account with the same study or search within 30 minutes counts as one entry.
- CSV export for Excel (semicolon, UTF-8 with BOM).
- Stored encrypted with SQLCipher on the server. Every single request is stored too (source
zugriffe) and can be read withserver/pacsdb.py.
The header links to the dcm4chee UI and to the viewer. Title, AE titles, links, an optional logo and the locale for
numbers and timestamps come from web/admin/config.js.
Browser --https--> nginx in the OHIF container --auth_request--> oauth2-proxy --> Entra ID or other OIDC
|
+-- /admin/ static page (web/admin, read-only mount)
+-- /admin/daten/ status.json, written by dashboard-status.py (cron, every 5 minutes)
+-- /admin/dienst/ unix socket --> verwaltung-dienst.py (root, on the host)
| +-- pacs-storage.sh disks, LDAP
| +-- firewall.py iptables
| +-- pacsdb.py audit log (SQLCipher)
+-- /dcm4chee-arc/ allow-listed REST calls --> dcm4chee-arc
+-- access_log syslog --> unix socket --> verwaltung-dienst.py --> audit log
- The page is plain HTML, JavaScript and CSS. No build step, no CDN, no external fonts. nginx serves it straight from the repository directory.
- Login: every request goes through
auth_requestto oauth2-proxy. The viewer needs one of the allowed groups./admin/and every changing dcm4chee call need the admin group. - dcm4chee REST: nginx passes only the calls the viewer and the page use. Everything else under
/dcm4chee-arc/returns 404. Changes need the admin group and theX-PACS-Adminheader, which the page sets with a readable description for the log. nginx adds these locks:- study update only with a
StudyInstanceUID(without it dcm4chee updates all studies) - move to another patient only with
updatePolicy=PRESERVE(otherwise dcm4chee overwrites the target patient) - patient update never with
merge - permanent delete only through the AE
IOCM_QUALITY, where dcm4chee deletes only rejected studies
- study update only with a
- Helper service:
server/verwaltung-dienst.pyruns as root on the host and listens only on a unix socket. nginx passes the account inX-Auth-Userand the client IP inX-Real-IP. The service runspacs-storage.shfor disks andfirewall.pyfor iptables, and reads the audit log. - Audit log: nginx sends one JSON line per request by syslog to a second unix socket. The service collects the lines
and writes them to the SQLCipher database every 2 seconds. Sources:
zugriffe(every request with account),ereignisse(study and patient searches, opened studies),admin(changes, with description and new values). server/dashboard-status.pyruns from cron every 5 minutes and writesstatus.json: disks, blank disks, containers. No patient data.server/firewall.pykeeps its rules in the chainPACS-SPERRE, jumped to fromDOCKER-USERfor the outside interface.pacs-firewall.serviceapplies them after every Docker start.- Optional: a section in the nginx example serves the dcm4chee UI through the same login, without Keycloak, for the admin group only. Changes made there go into the audit log.
- dcm4chee-arc-light in Docker Compose, running without Keycloak (image
dcm4chee-arc-psql, not the-securevariants). nginx and oauth2-proxy do the access control. - Linux host with systemd, Python 3.7 or newer (standard library only), Docker with the compose plugin, iptables, cron.
libsqlcipher0(Debian/Ubuntu package) for the audit log.- For the disk assistant: util-linux, e2fsprogs, curl. The LDAP container needs its root password in its own
environment (
LDAP_ROOTPASSorLDAP_ROOTPASS_FILE). The arc container needsSTORAGE_BASEmounted with:rslave. - The OHIF image (nginx 1.25.1 or newer) and oauth2-proxy. The compose example sets up both.
- An OIDC provider with a groups claim. Tested with Microsoft Entra ID.
docs/install.md goes through every step. In short:
- Clone the repository to
/opt/pacs-adminand copydeploy/pacs-admin.env.exampleto/etc/default/pacs-admin. - Install
libsqlcipher0and create the key for the audit log. - Install
pacs-verwaltung.serviceand the cron job. - Register the app in Entra ID, fill in the compose and nginx examples, start the OHIF container.
- Start the firewall in Probelauf, with the dcm4chee REST and management ports blocked, and tighten it later in the Firewall tab.
python demo/serve.py
Open http://localhost:8000/admin/. The demo serves web/ and loads demo/mock.js, which answers every request of
the page with made-up data inside the browser. No dcm4chee, nginx or helper service needed. Changes last until you
reload the page.
python demo/serve.py 8080uses another port.http://localhost:8000/admin/?demo=platteshows a blank disk ready to be set up.http://localhost:8000/admin/?demo=offen#firewallopens all cards in the Firewall tab.
- The UI is German only. The docs are English and German.
- Tested with dcm4chee-arc-light 5.35.1, Debian 10, Docker 26 and iptables-nft.
- The firewall covers only ports that Docker publishes, for IPv4 traffic coming in over one interface. It does not cover SSH, other host services, or traffic from the host itself and from Docker networks.
- With Docker's defaults a published port is also bound to
[::]. IPv6 connections then reach the container throughdocker-proxy, past the firewall, whatever the Firewall tab says for that port. The tab marks such ports with "IPv6 nicht gefiltert". Publish ports on IPv4 only or switch IPv6 off on the host (install guide, step 10). - Client PC discovery works only by DNS name pattern. PCs without a predictable name show up only through their connections.
- The DICOM and HL7 sender lists parse the dcm4chee
server.logfor the ports inFW_DICOM_PORTSandFW_HL7_PORTS(default 11112, 2762, 2575 and 12575), translated to the container port throughdocker ps. - The disk assistant formats whole disks as ext4, never partitions. The page offers only the blank disks that
pacs-storage.sh kandidatenlists, and the helper service accepts only the device names/dev/sd*,/dev/vd*,/dev/xvd*and/dev/nvme*n*. Storage ID, filesystem label and mount directory share one name (STORAGE_PREFIXplus number). Storages named differently do not appear in the overview. - One archive AE per installation.
- The audit log is never pruned. Plan disk space and retention yourself.
- The helper service runs as root and trusts
X-Auth-UserandX-Real-IP. Only nginx may reach its socket. The service creates both sockets with ownerPACS_SOCKET_UIDand mode 0600. Mount the socket directory only into the nginx container. The uid is the only protection of the sockets and the TLS key, so no account on the host may have it:getent passwd 101must print nothing (install guide, step 6). - nginx replaces
X-Auth-UserandX-Real-IPsent by the browser before it passes a request to the helper service, and stripsX-PACS-Adminbefore it passes a change on. A change through/admin/dienst/or through the allow-listed dcm4chee calls withoutX-PACS-Admingets 403. Pages from other origins cannot set this header, because nginx answers no CORS preflight on these paths. - The optional dcm4chee UI section sends every request under
/dcm4chee-arc/whoseRefererstarts withhttps://<host of the request>/dcm4chee-arc/ui2/(no port, that is 443) to the full dcm4chee API. There the locks listed under How it works and theX-PACS-Admincheck do not apply, the admin group check does. The map compares the host in theRefererwith$host. ARefererwith another host, with a port, over http, or noRefererat all leaves the request with the locked locations. Browsers let a page send only aRefererof its own origin (scheme, host and port). Another service on the same host name, for example Portainer on 9443, is another origin, and itsReferercarries the port. A logged-in admin can set anyRefererwith curl and gets the same access as through the dcm4chee UI. Without the section (theif ($pacs_dcm4chee_ui)block removed) only the allow-listed calls reach dcm4chee. - dcm4chee runs without its own authentication. Anyone who reaches arc on 8080 or 8443 directly can read and change
everything. Publish these ports on 127.0.0.1 only (nginx reaches arc through the Docker network), keep them blocked
from outside (the built-in firewall rule set does) and keep them off other networks. A port that Docker also binds
to
[::]is open over IPv6, see Limitations. - The audit database is encrypted with the raw 256-bit key in
PROTOKOLL_KEY. Keep the key file root-only (0600) and keep an offline copy. Without the key the log cannot be read. The log contains patient IDs and names from search URLs. Treat it like patient data. - nginx writes no access log to
docker logs, because URLs contain patient data. - Narrow
--trusted-proxy-ipof oauth2-proxy to the subnet of the Docker network. Sessions end after 12 hours (--cookie-expire) and are not refreshed. - The lock-out check on firewall saves covers only the saving PC and the https port (the first of
FW_WEB_PORTS). It skips addresses in the Docker networks: over IPv6 nginx sees the Docker gateway, not the PC.
MIT, see LICENSE.



