The Carino DICOM manual

Carino DICOM is a DICOM gateway and a continuity appliance: it receives studies, routes and forwards them, publishes what it holds over Query/Retrieve and DICOMweb, and keeps an imaging department working when the PACS or the RIS stops answering. This manual covers deployment, the everyday jobs and what to do when they fail, the security model, every service, and the software's real limits.

Every screenshot here is a live instance of this software, holding studies that were invented for the picture. No patient appears anywhere in this manual.

Contents

  1. What it is and what role it plays
  2. Getting started
  3. How do I…
  4. When something goes wrong
  5. The security model and the token rule
  6. The services, one by one (reference)
  7. This is not a medical device
  8. Licence and where to get help

1. What it is and what role it plays

Carino DICOM sits between the equipment that produces images and the systems that store or read them. It is not trying to replace your archive; it is trying to be the piece that connects them, and the piece still standing when one of the others falls over.

It plays two roles, and the distinction matters because it decides which services you enable:

Everything is driven by one config.json, and everything runs headless. The web dashboard is convenient, not required.

Default ports

ServicePortProtocol
Receiver (Storage SCP)11112DICOM / DIMSE
Virtual print11113DICOM Print
Modality Worklist11114DICOM C-FIND
Query/Retrieve11115DICOM C-FIND/C-MOVE/C-GET
Emergency RIS2575HL7 over MLLP
Dashboard + DICOMweb8042HTTP

Why 11112 and not 104. DICOM's registered port is 104, which is privileged on Linux and macOS and needs root. The default avoids that. Configure your equipment accordingly, or publish 104 onto 11112 at the container or the firewall.

2. Getting started

Pick a deployment shape

ShapeFitsComes back after a power cut
Desktop app (tray)A workstation with a person sitting at it: one room, one clinic, a trial.Only with "start at login"
Docker / PodmanA server, or any machine where you want it isolated and movable in one piece.Yes (restart: unless-stopped)
systemd serviceThe permanent box in the corner of the imaging department, with nobody logged in.Yes

Desktop app

Download the package for your OS from the releases page and open it. It tucks into the system tray; clicking it opens the dashboard. It is not code-signed yet, so Windows and macOS warn on first launch — the landing page has the exact steps to open it anyway.

On first run it asks, once, whether it may check GitHub for a newer version. Answering no — or closing the question — leaves it off, which is the default. If it is on, a newer release shows up as a line in the tray menu and beside the version on the Overview panel; clicking either opens the release page in your browser. Nothing is downloaded and nothing installs itself. The tray menu's Check for updates tick turns it on or off whenever you like, and only this desktop build has it at all — the container and systemd deployments do not.

Docker

git clone https://github.com/MiguelCarino/Carino-DICOM
cd Carino-DICOM
mkdir -p data
echo "PACS_UID=$(id -u)" >> .env
echo "PACS_GID=$(id -g)" >> .env
docker compose up -d --build
docker compose logs -f pacs

The first boot generates and prints an access token into the log. Open http://127.0.0.1:8042/ and paste it when the dashboard asks. Everything — config, studies, logs, index — lives in ./data, which is what you back up.

The most common first-run failure is ./data owned by root: the container never runs as root and dies with EACCES. The two echo lines above prevent it by building the image for your own account; if you skipped them, add them and run docker compose up -d --build again. On SELinux hosts (Fedora, RHEL, Rocky) the volume's :z suffix is not decoration: without it the bind mount is unwritable while the ownership looks perfectly correct.

systemd service (Linux)

git clone https://github.com/MiguelCarino/Carino-DICOM.git
cd Carino-DICOM
sudo packaging/systemd/install.sh

The installer creates the carino-pacs system user, copies the code to /opt/carino-pacs, provisions /var/lib/carino-pacs, and installs the unit… and does not start it. That is not an oversight: starting a PACS opens listeners that accept patient data, and that decision belongs after reading the config, not before. Edit /var/lib/carino-pacs/config.json, then:

sudo systemctl enable --now carino-pacs
systemctl status carino-pacs
journalctl -u carino-pacs -f

Re-running the installer upgrades the code in place and never touches an existing config.json.

Where the data lives

ShapeDirectory
Desktop / CLI~/CarinoDICOM/ (existing installs keep ~/CarinoPACS)
Docker/data in the container → ./data on the host
systemd/var/lib/carino-pacs/ (mode 0750)

config.json lives with the data, not in /etc, because relative paths in the config ("./received", "./logs", "./index.db") resolve against the directory the config file itself sits in. A config in /etc would scatter patient studies through /etc.

First run, in order

  1. Choose the services. Everything ships off. The dashboard's setup chooser asks what this machine should run; enable only that.
  2. Add your destinations (name, host, port, AE title) on the Destinations tab of Configuration, which is the tab that row opens on.
  3. Prove connectivity before moving a single image: press Echo on the destination's row. On a machine with no dashboard, ./run.sh echo --name "Hospital PACS" does the same. If the echo fails, the problem is not Carino — it is the network, the firewall or the AE title.
  4. Point one device at it using the Point the modality at line on the receiver's card, and send a test study (see How do I…).
  5. Watch the activity log, on the Logs tab of Activity. If something fails it shows up there, and on the service's card as Last problem; this software would rather shout than fail quietly.
The dashboard's setup chooser, headed “Choose the services this PC should run”: a card per service — receiver, auto-send, print receiver, emergency RIS, worklist, Query/Retrieve — each with a Use this tick box and a line saying its port is free. Only the receiver is ticked, so the footer reads “1 selected”, beside Not now and Turn these on buttons.
Step 1. Only the Receiver comes ticked — it is what makes this machine a destination at all — so the footer reads 1 selected; nothing is running yet, and no port opens until you apply. Each card names what the service is for, so the choice is made from the description rather than from the protocol name.

Headless

./run.sh init            # scaffold config.json and its folders
./run.sh init --token    # and generate web.auth_token
./run.sh serve           # dashboard at http://127.0.0.1:8042
./run.sh receive         # receiver only
./run.sh send            # folder watch / forwarding only
./run.sh qr              # Query/Retrieve only
./run.sh mwl             # worklist only
./run.sh ris             # HL7 order listener only
./run.sh print           # virtual printer only
./run.sh echo --host 10.0.0.5 --port 104 --aet REMOTEPACS

Every command takes -c / --config <path>. On Windows, run.ps1.

3. How do I…

Short recipes for the jobs that come up on the floor. Each one ends with how you know it worked; if it did not, the next chapter is the list of what to check.

…point a modality at it to store images

  1. Open Services. If the Receiver card says it is stopped, press Start.
  2. Read the card's line Point the modality at: an AE title, an IP address and a port (by default CARINODICOM and 11112). The copy button beside it copies all three. If the machine has more than one address, use the one on the modality's network.
  3. On the modality, add a storage destination — vendors call it a DICOM node, an archive, a "Send to" or a "Store" target — with those three values. The modality's own AE title is the calling AE; you do not have to enter it here.
  4. If Configuration → Settings → This PACS lists AE titles under Accept only these AEs, add the modality's AE title there, or the receiver refuses it.
  5. Run the modality's own connection test (often Verify or Echo), then send one study. It appears in Studies → History.

The card's Test button checks the receiver from this machine itself. If Test passes and the modality still fails, the problem is between the two: address, port, firewall or the AE allow-list.

…point a modality at it to print

For equipment whose only output is film. Carino pretends to be the film printer and captures each sheet for you to identify; nothing is printed on paper.

  1. Open Services and press Start on the Print receiver card.
  2. Read its line Point the modality at (by default CARINOPRINT, this machine's IP and port 11113).
  3. On the modality, add a DICOM printer — sometimes called a laser imager or film printer — with those three values. Grayscale is always offered; if the modality only prints in colour, tick Also accept color print jobs under Configuration → Settings → Printing.
  4. Press Send a test print on the card. A sheet marked TEST PRINT — safe to discard appears in Studies → Pending within a few seconds. That proves the receiver works; discard the sheet.
  5. Print one film from the modality. The card shows Last print with the number of films, the modality's AE title and its address, the Captured counter goes up, and the film waits in Pending for you to approve (see below).

Store each captured film as (under Printing) decides what the film becomes when you approve it. Many archives and viewers store a PDF but do not display it; choose Image (Secondary Capture) if the film has to be read on the other side.

The printer AE title typed on the modality is not checked: a mistyped one still prints. Only the modality's own AE title is filtered, and only if you fill in Accept only these AEs. If a print fails, the card shows Last problem; the print troubleshooting table lists every message and its fix.

…point a modality at it to pull its worklist

  1. Make sure there are orders. Either the RIS sends HL7 orders to the Emergency RIS service (port 2575), or someone types them in Orders → New order.
  2. Under Configuration → Modalities, register the room with its exact station AE title. An order's target is then picked from that list instead of typed.
  3. Open Services, press Start on the Worklist card and read its line Point the modality at (by default CARINOMWL and port 11114).
  4. On the modality, set the worklist provider (MWL, "HIS/RIS" or "worklist server") to those values and query it.

An order with no target appears on every station; an order with a target appears only on the station whose AE title matches it exactly. When the study comes back it is matched to its order by accession number and the order closes itself.

…approve a pending film

Captured films, and PDFs or images found beside a study, wait in Studies → Pending. The 📎 count next to Studies says how many.

  1. Open Preview and look at the sheet.
  2. If the patient has an open order, press Match to order… and pick it. Name, patient ID, accession and study come from the order, and the order is closed as captured.
  3. Otherwise type the identity yourself. Patient ID and Patient name are required; accession, date and description are strongly recommended.
  4. If the item arrived beside a study and you changed the patient, choose what it belongs to: Attach to the original study only if that study really is this patient's, otherwise New study. A wrong choice files one patient's sheet inside another patient's study.
  5. Press Approve & send. It is converted to DICOM and forwarded like any other study.

There is no undo: once approved, the original file is gone from Pending and the DICOM copy is on its way. A mistake is fixed on the archive. Discard removes an item without sending it.

…fix a stuck forward

Open Studies → Stuck (the ⚠ count next to Studies). Rows are grouped by study, with the patient, accession and description, and sorted into three kinds:

…find out why something failed

  1. Look at the service's card. Every listener shows Last problem when something went wrong; click it to open the log already filtered to that service.
  2. On Activity → Logs, use the service chips (store, send, print, worklist, Q/R, RIS…), Errors and warnings only, and the text filter — an AE title, an IP address or an accession number narrows it fast.
  3. To hand the problem to someone else, copy the visible lines, or download a whole day's log.
  4. Press Test on the card. If the test passes from this machine but the modality still fails, look at the network between them.
  5. If a setting changed and nobody knows who changed it, Activity → Audit names the person and the sections they changed.

4. When something goes wrong

Symptom first, then where to look, then the fix. Almost every row starts in the same two places: the service's card on Services (its Last problem) and Activity → Logs.

Sending, receiving and the worklist

SymptomWhere to lookFix
The modality cannot send to us at allReceiver card; log lines starting Receiver: refusedits AE title is not in the allowed list: add the modality's AE under Accept only these AEs, or clear the list. Nothing in the log: the connection never arrived — check the IP and port on the modality, the firewall, and that the card is running. Test on the card separates "this machine" from "the network".
The modality sent, but the study is not in HistoryLog, store chip; Overview Free spaceRefused C-STORE — low disk space: free disk space or lower scp.min_free_gb. The modality can resend once there is room.
A card is amber: "Enabled but not running"Log at the time it startedIts port is taken by another program, or by another service here. Change the port or stop the other program, then Start.
Studies do not reach the main PACSStudies → StuckThe row's error says what the PACS did: rejected permanently: calling AE title not recognised → add our sending AE (default CARINOSCU) on the PACS; connection refused → wrong port or the PACS is stopped; no reply → host off or a firewall; aborted → often a TLS mismatch.
Nothing is sent to one destination, and nothing retriesStuck, "Held" sectionA de-identification hold — see De-identify on forward.
The modality does not see its worklistWorklist card; log, worklist chipMWL: refused: add its AE to the worklist's allow-list. No query in the log: wrong IP/port/AE on the modality. Query arrives but the list is empty: the order's target AE does not match the station exactly — fix it under Modalities.
The hospital's own RIS does not give the modality its listConfiguration → Modalities → Test worklistSee Worklist probe; the answers land on Activity → Worklist probes.
HL7 orders from the RIS do not appearEmergency RIS card; log, RIS chipCheck the RIS is sending to this IP on port 2575, and that its address is in ris.allowed_hosts if that list is set.
Everyone was signed out—Normal after a restart: sessions live in memory by design. Sign in again.
The dashboard stops responding or its numbers stop changingReload the pageIf it does not come back, the engine is not answering: check that the service or container is running (systemctl status carino-pacs, docker compose ps) and read its log.
The container dies with EACCESdocker compose logs pacs./data is not writable by the container's user: set PACS_UID/PACS_GID in .env and rebuild (see Docker above).
Failover e-mail is on, but nobody got oneConfiguration → PeopleMail goes to profiles with an e-mail address, not to a list. See Notifications.

Printing

Every refusal and failure of the print receiver is a line in the log (the print chip), counts in the card's Errors, and shows as its Last problem. A successful job shows as Last print.

Log line or symptomWhat it meansFix
Print: refused X @ ip — its AE title is not in the allowed list (…)The modality's AE title is not on the print receiver's list.Add it under Printing → Accept only these AEs, or clear the list.
Print: refused X @ ip — too many connections at onceThe modality, or several, opened more connections than the receiver accepts at once.Let the jobs finish and print again; if it repeats, print one job at a time.
Print: X @ ip asked for Basic Color Print Management…, which this printer does not offerThe modality wants colour and colour is off.Tick Also accept color print jobs. Not counted as an error: a modality that offers both prints in grayscale.
Print from X: nothing to print — no film was created first, … N film(s) were created but no image was sent into them, … the film it named (…) was never created hereThe modality skipped a step of the print sequence. It was told "empty page" (a warning, 0xB603).A modality-side setting or fault: check its printer configuration (film format, number of images) and print again.
Print: X sent an image for a box this printer never handed out (…); refusedThe modality referred to an image box from another session.Restart the print from the modality; if it repeats, report it with the log line.
Print: could not read image N from X (…); it is left blank on the filmOne image used a pixel format the receiver does not read; the rest of the sheet was kept.Change the modality's print settings (bit depth, colour) if it offers that; report the line otherwise.
Failed to render or queue the print job from X: …The film arrived but could not be written (status 0x0110).Check free disk space and that the pending folder (scu.pending_dir) is writable.
Other lines starting Print: …Passed through from the DICOM library's own messages for that print connection.Read with the line before it; report if unclear.
Nothing in the log at allThe connection never arrived.Is the card running (not amber)? Is the modality using this machine's IP and port 11113? Is a firewall in the way? Send a test print on the card proves this side.

Two things that are not the cause: the printer AE title typed on the modality (it is not checked), and the modality leaving the film session and film box numbers to the printer (they are created here when missing). A Presentation LUT is accepted and ignored, and a modality that first asks for the printer's configuration gets an answer.

The Logs tab of Activity: above the stream, a chip per service, an errors-and-warnings-only box and a text filter; below, the day's timestamped lines, newest at the bottom, each tagged with the service that wrote it; under the stream, a line count, Copy visible, a day picker, Download day's log and Clear view.
Where every row above starts. The log opens on its newest lines, at the bottom; errors are drawn in red and warnings in amber, and each line that matters says what it wants done about it. When the stream is busy, narrow it with the service chips (Receiver, Auto-send, Printing, …), Errors and warnings only and the text filter. Copy visible copies what the filters leave, Download day's log saves the whole file for the day picked beside it, and Clear view empties the screen, not the file.

5. The security model and the token rule

The sign-in gate: a card headed “This PACS needs its access token”, one password field, a Sign in button, and a note saying the token is web.auth_token in the config file and that the dashboard never shows a stored one.
This is served instead of the dashboard, and again on any later 401 — a restart drops every session by design, so signing in again is normal rather than a fault.

Start with the part that matters: the dashboard is the key to the archive. Anyone who opens it can read every stored study, download the DICOM files, change every setting, start and stop services, delete studies and shut the server down. Out of the box there is one shared secret and whoever holds it can do everything — profiles are optional and off until you turn them on. Once you do, each person signs in as themselves with their own permissions, their own view of patient identifiers, and their own name in the audit trail; the shared token keeps working as an administrator so nothing that already uses it breaks.

The People tab of Configuration, under a line reading “4 profiles.” The Administrator's card is short — its “Administrator (everything, including future permissions)” box is ticked, so it carries no permission grid at all, only its password field, Save and Delete. The IT card below it shows its Can do grid of tick boxes, and the screenshot cuts off partway through that grid, before its Can see row. An Add someone button is pinned at the bottom right.
Profiles, once they are on. Permissions are per person and enforced at every endpoint, not hidden in the browser — and Can see is separate from Can do: a profile can be allowed to re-route a study by its accession number without ever reading a patient's name. Anything unticked is shown as *** wherever it would have appeared.

Permissions also decide what the dashboard draws. A profile sees only the sidebar rows, and only the tabs inside them, that its capabilities pay for: a Radiologist who holds routing.read but not config.read gets Configuration with Destinations and Routing and no Settings tab, and Reception, holding none of the three, has no Configuration row at all. The service chips in the top header are the deliberate exception — see Web dashboard below.

The same gate on an appliance that uses profiles: four buttons — Administrator, IT, Radiologist, Reception — each with a padlock, and a link reading “Sign in with the access token instead”.
The gate an appliance with profiles shows. The picker is only drawn while users.list_profiles is on — publishing the staff list to anyone who can reach the port is a real disclosure, and it is your call. The token still works, folded away underneath, because it is the way back in when somebody locks themselves out.

The rule

An empty web.auth_token is allowed only while web.host is loopback. If web.host is anything else — 0.0.0.0, a LAN address, a hostname — and the token is empty, the server refuses to start. That refusal is a feature, not an error to route around.

The reason is concrete. Unauthenticated on 127.0.0.1, the operating system is the access control: only a process on this machine can reach the API. That is defensible. But web.host is operator-configurable, and the moment somebody changes it to 0.0.0.0 so they can "get in from the other PC" — in a hurry, on a Tuesday, not thinking about security — that same API hands any LAN neighbour the study list, the storage paths, the DICOM bytes and /api/shutdown. The rule exists because that change takes ten seconds and its consequences last years.

Every container is always in that situation: a container publishes on 0.0.0.0 by construction. So the image generates a 256-bit token on first boot when you do not supply one — and prints it. What it never does is generate one quietly: a secret nobody sees is a secret nobody rotates.

Generating and presenting the token

./run.sh init --token                                  # writes it into config.json
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
openssl rand -base64 32

The server accepts the credential three ways:

The cookie exists so the dashboard asks for the token once instead of holding it in JavaScript, where every XSS and every browser extension can read it. It never carries the token: it carries an HMAC over a secret generated at startup and kept in memory only. That is why a restart logs everyone out — the right trade for a single-operator appliance: no session store on disk, nothing to leak, and at worst the token is retyped once a shift.

The dashboard speaks plain HTTP. There is no built-in TLS for the web layer. If you expose it beyond loopback, put it behind a reverse proxy that terminates HTTPS. A token sent over cleartext HTTP on a shared network is a token you have given away.

The DICOM side

What the model does not protect

Backups. The sqlite index is a cache and rebuilds itself; the images do not. Back up the storage directories — and test a restore at least once.

No telemetry, with one exception you switch on yourself. The engine sends nothing anywhere: no analytics, no crash reporting, no usage counters, no third-party scripts fetched at runtime. Its only outbound connections are the DICOM associations and HL7 acknowledgements you configured, to the peers you named — and a Docker, Podman or systemd deployment is that and nothing more unless an administrator turns on the version check below, which is the sentence that matters, because that is the shape sitting in the imaging department with the patient data on it. The desktop app can additionally check whether a newer version has been published, and only if you said yes to the question it asks once on first run; it is off by default and the tray menu turns it off again. That check is one HTTPS request a day to GitHub's list of releases, carrying a User-Agent that names the app and nothing else — no identifier, no counter, no configuration, no patient data — so what GitHub learns is this machine's IP address and roughly when the app was started, and that is the whole of it. Nothing is downloaded and nothing installs itself. The dashboard offers the same check from the server itself, for deployments without the desktop app: it is off until an administrator turns it on, from the button beside the version on Overview or from Settings → Integrations; it sends exactly that request, from the server rather than from your browser; and Overview then shows whether this version is current, with an Update button that opens this website. A change that sent more than that, or that sent anything nobody had agreed to, would be treated as a vulnerability.

The Audit tab of Activity: a line giving the file count, the size on disk and the chain head digest; a filter row — everyone, every action, a Failed or refused only box — with Check integrity and Export buttons at its right; and timestamped rows, newest first, each Done and each with its details folded: the engine signing in with the access token, a row per sign-in, and below them the access token’s Tested a worklist for US_ROOM_2.
The audit trail, with the chain head printed above it — that digest is the value worth copying somewhere this machine cannot reach. The tab has its own filter row — whose actions (everyone, or one person), which action, and Failed or refused only — with Check integrity and Export at its right; Clear view belongs to Logs, not here. Refusals are recorded as faithfully as successes: a request that was turned down is a row here like any other.

6. The services, one by one

Everything that opens a port ships off. The right question is not "what can it do?" but "what does this machine need to do?" Every enabled service is one more open port. The index below is the one exception — it starts on its own, because it is a local sqlite cache that binds nothing.

Each service has one switch. Start on its card starts it and keeps it on after a restart; Stop asks first, then stops it and keeps it off. In Configuration → Settings the same switch is the box Run this service (also at startup), so the card and the setting can never disagree. Saving settings restarts only the services whose own settings changed, and the dashboard says which.

The Services panel: a card per service — receiver, auto-send, print receiver, emergency RIS, worklist, Query/Retrieve — each with a status dot, a Stop button, a “Point the modality at” line giving AE title, IP address and port with a copy button, a Test button and its counters; the print receiver's card also shows its Last print line and a Send a test print button.
Each service starts and stops on its own, and each card carries what a technologist needs: the address to type into the modality, a Test that checks the service from this machine, and the two numbers worth watching — what it has handled and what it has failed. When something fails, Last problem says what and links to the log. The dashboard is not the engine — stopping one here stops that listener, not the appliance.
The Overview panel: counting tiles for services on, received, sent, stuck, pending, open orders and free space, over cards naming this machine's address, its receiver, its config and storage paths, the reachability of each destination, and the last study received.
Overview answers "is this machine working" in one screen. Every tile that counts something with a home is a button onto the panel that owns it — Stuck and Pending open their own tab of Studies — so a number you did not expect is one click from its detail. Free space is the exception: it leads nowhere, so it stays a plain readout rather than a button that would do nothing.

Receiver — Storage SCP

Port 11112 · C-STORE, C-ECHO

What it does
Accepts studies pushed by modalities and files them to disk, optionally organised by Patient / Study / Series. It accepts all transfer syntaxes and stores compressed objects as-is — no transcoding, so nothing is altered on the way in.
Turn it on when
Anything needs to send images here: modalities, another PACS, a workstation. This is the central service.
Leave it off when
This machine only forwards what something else drops in a folder.
The History tab of Studies, on its Received strip: a filter box and modality chips above a row per study with patient, ID, date, accession number, description, modality and image count, its series listed underneath, and on each row a Send button and a ⋯ button that opens the rest of the row's actions.
What arrived, newest first, with the series each study is made of; the filter box and the modality chips narrow the list. Send is the one action on the row — the ⋯ menu holds the rest: Add PDF/image, Edit tags (once an editor address is set), Copy path, Open folder (offered only when the dashboard runs on the machine itself) and Delete. The path is real: the filing is a folder tree, not a database somebody has to export from. The strip above the list has three positions, in the order a study lives: Received; Outgoing, the outgoing folder, with each destination's send state for every study; and Sent / Archived, which fills only when Auto-send is set to move a study after sending it. Under the default, keep, a forwarded study stays in Outgoing.

Auto-send — Storage SCU and routing rules

Outbound client · C-STORE

What it does
Watches a folder and forwards every new file to whichever destinations apply. A file is only sent once it is stable (size unchanged between two scans), so a half-written file is never forwarded. Progress is tracked per destination: a file counts as done only when every enabled destination has accepted it, and failed hosts are retried on the next scan. On success the original can be kept, moved or deleted.
Rules
With routing enabled, a rule picks destinations by modality, calling AE title, station, patient ID or study description (case-insensitive * and ? globs). Example: CT from ER_* to the teaching archive, de-identified.
The guarantee
A study can never end up going nowhere. Routing off, no rule matched, an unreadable header, a rule naming a destination that no longer exists — every one of those falls back to every enabled destination. Over-sending annoys an operator; under-sending loses an image.
The one exception
A rule that asks to de-identify for a destination, where the scrub cannot actually be performed, holds that destination: it is not sent to at all, rather than sent identified. Two things stop the scrub — the profile is off, or the profile is on and no de-identifier can be built from the settings — and they need different fixes. Nothing is lost either way; see De-identify on forward below.
Turn it on when
This machine has to deliver images onward: to the central PACS, to a reading workstation, or as copies to a teaching archive.
The Destinations tab of Configuration: three rows — Main archive, Reading room and Teaching archive — each with host, port, AE title, tick boxes for TLS, no RIS and failover, and an Echo button.
Destinations are named here and referred to by name everywhere else, so renaming a node does not orphan the rules that point at it. Echo proves the association before any image depends on it.
The Routing tab of Configuration: a Route by rule tick box, then numbered rules with match fields for modality, calling AE, station, patient ID and study description, and a Send to row of destination tick boxes plus De-identify and Stop at this rule.
Rules are read top to bottom, and the blank fields say any — a rule is a filter, not a form to fill in. The order matters, which is why each rule carries arrows to move it.
The Stuck tab of Studies, under a banner reading “8 files need attention” and split in two: “Retrying automatically”, listing the teaching archive with two instances of the CR study waiting, its last error in red, a Retry now button and a countdown to the next attempt; and “Held — nothing is being sent”, listing the teaching archive tagged profile off, with six instances of the CT study waiting under its patient and accession number, the edit that releases them, the buttons De-identification settings and The rule that asks for it, and Send to… and Remove from queue.
The two halves here are different problems and the tab keeps them apart. The top is a node that is down and will clear itself; the bottom is a de-identification hold, which no timer ever releases — so each row spells out the edit that does, and carries the two buttons that go and make it. Rows are grouped by study, so you see whose images are waiting, not just file names. A third section, No destination left to retry, is drawn only when it has something to hold: files routed to a name that is no longer an enabled destination. Nothing retries those, because there is no node left to dial — restore the destination, send them to another one with Send to…, or accept the loss with Remove from queue; until then they sit in the outgoing folder rather than being archived or deleted.

De-identify on forward

PS3.15 Annex E profile · applied only to the copy that leaves

What it does
Applies the Basic Application Level Confidentiality Profile to the object being sent, declaring in (0012,0064) exactly which retain options were used so the recipient can see what was kept. The archived original is never rewritten — that asymmetry is the whole idea.
Profiles
basic retains dates (full or shifted), patient characteristics, device identity and institution identity. strict drops device and institution identity and removes private attributes even if you asked to keep them.
Turn it on when
Images leave the clinical environment: teaching, research, a vendor, an outside second opinion.
Turning it off
The profile decides whether scrubbing happens at all; a rule's de-identify tick decides which destinations get a scrubbed copy. Set the profile to off while a rule still asks for it and Carino holds that destination rather than sending an identified copy to a node whose owner was told it receives none.

Held, not sent — and this is the answer when studies stop moving. A destination a rule marks de-identify receives nothing whenever the scrub cannot actually be performed. Delivery is deferred, identity is not disclosed: a study waiting on disk is released by an edit, a name that has arrived at an outside node is not recoverable by any edit.

There are two reasons the scrub cannot happen, and they are not fixed the same way. Every hold records which one it is, and the Stuck tab of Studies prints the remedy for the cause it recorded rather than guessing:

  • The profile is off while a rule still asks for a scrub. Set the profile to basic or strict and the next auto-send pass releases them.
  • The profile is on, but no de-identifier could be built from the current settings, so there is still nothing to scrub with. The failure that stopped it is on the Logs tab of Activity, on the send channel — fix that. Turning the profile off does not release this one: it releases nothing and only moves which half is stopping the scrub.

Unticking de-identify on the rule releases either hold, and it releases the studies identified — which is the exact outcome the hold exists to prevent. Make that edit only if that destination is genuinely no longer meant to receive scrubbed data.

Whichever of these edits is the right one, it is a click from the row that reports the hold. Each held row carries a De-identification settings button that opens the Settings tab of Configuration, and a The rule that asks for it button that opens the Routing tab. Both open the tab and nothing more: neither scrolls down to the rule that asks for the scrub, and neither highlights it. Even so, the row that names the problem is the way to both places it is fixed, so this repair is shorter than it was when each of them was a sidebar row to find.

Nothing is lost and nothing is quiet about it. The studies stay in the outgoing folder — never archived, never deleted — every other destination on the same study still receives them, the Logs tab of Activity raises an error naming the study, the withheld destination and the cause, the de-identification fieldset on the Settings tab names it too, and the ⚠ badge beside Studies counts those files — and that badge is itself a button, which opens the Stuck tab. Nothing releases a hold on its own: no timer runs it down and nothing retries it.

It does not clean pixels. Patient demographics burned into the image — the banner an ultrasound or a secondary capture prints inside the frame — survive every profile, and they are the most common way "anonymised" data walks out of a hospital with a name on it. The Clean Pixel Data option (113101) is deliberately not claimed, because it is not done. Nothing inside the program can detect that leak for you: a human has to look at the images. Narrative text inside Structured Reports is likewise not read.

Index

sqlite · index.db

What it does
Keeps one row per stored file and derives patient, study and series answers from it, so a study summary can never drift out of step with the instances it summarises. It is the query layer behind Query/Retrieve and DICOMweb.
Turn it on when
You use Query/Retrieve or DICOMweb. It is the only dependency of both.
What it guarantees
The index is a cache, never the source of truth. Losing it costs a rescan, never an image.

Query/Retrieve — C-FIND, C-MOVE, C-GET

Port 11115 · Patient Root and Study Root

What it does
Lets another system ask "what studies do you have for this patient?" and then "send them to me / to that workstation". This is the half of a PACS that old equipment can actually talk to: a 2009 ultrasound or a CR reader will never speak DICOMweb, but it does speak DIMSE.
Turn it on when
Workstations or modalities need to pull studies from here, or you are acting as the temporary archive during a primary-PACS outage.
What it guarantees
A C-MOVE never invents its instance list — it resolves through the index. Anything the index knows about but cannot be read off disk is counted as a failed sub-operation and named in the Failed SOP Instance UID List. It is never quietly dropped: a C-MOVE that reports success while sending fewer images than it matched is the worst failure this software can have.

DICOMweb — QIDO-RS, WADO-RS, STOW-RS

HTTP, under /dicom-web on the dashboard port

What it does
Lets modern viewers (OHIF, Weasis and friends) query, retrieve and store over HTTP without negotiating a DICOM association. A STOW-RS upload goes through the same filing a C-STORE uses, so a study posted by a viewer is indistinguishable from one pushed by a modality.
Turn it on when
You want a web viewer against the archive. Remember the token protects these routes too.
CORS
cors_origins is matched exactly (scheme + host + port) and is empty by default, so nothing is reflected back to an origin that is not on the list. There is no pattern syntax — but a literal * is honoured, and only because you typed it: from then on every origin is reflected and any page the operator visits could read the archive off this machine. Name the viewer instead.
What it does not do
/rendered, /thumbnail, bulkdata URIs and transcoding between transfer syntaxes are not implemented and answer 406. A half-working viewer is worse than an absent feature: anything that cannot be produced is never faked.

Modality Worklist (MWL)

Port 11114 · worklist C-FIND

What it does
Serves orders to modalities so the technologist does not hand-key patient details. Each order carries a pre-generated Study Instance UID that is burned into the exam, so the study the modality sends back reconciles to its order exactly. An order's target modality AE steers it to one station; blank means every station sees it.
Turn it on when
The RIS is not reaching the modalities: because it is down, because the destination simply has no RIS, or to test a RIS→PACS flow without a live RIS.

Registered modalities

No port · Configuration → Modalities

What it does
The list of rooms this appliance serves — a name a person recognises, the station AE title the worklist matches exactly, the modality code, and an optional station name. It is not a permission list: a scanner does not have to be registered here to send images, and being here does not let it. What it buys is that an order's target becomes a pick from a list rather than a field somebody types, and it is the list the worklist probe below borrows an AE title from.
Fill it in when
You are serving the worklist or hand-keying orders. Until one station is registered, an order's target is typed by hand and a typo cannot be caught.
The Modalities tab of Configuration: three registered rooms — ER CT, Ultrasound 2 and Portable CR — each with its name, station AE title, modality code and station name, an enabled tick box and a Test worklist button, over a Save modalities button.
The rooms this appliance serves. An order's target is picked from this list, and Test worklist on a row asks the hospital's other RIS what it would give that room.

Worklist probe — asking the other RIS

Outbound client · worklist C-FIND · Configuration → Modalities

What it does
Answers “why is this scanner not seeing its list?” without touching the scanner. Give the hospital's real RIS or its broker an address under Settings, then press Test worklist on a modality's row: Carino asks that RIS the question that modality would ask, using that modality's own AE title, and files every answer under Activity → Worklist probes. One run asks the same question five times, widened a key at a time — station and date and modality, then without the modality, without the date, without the station — because the difference between the answers is what locates the fault. Nothing comes back to any of them and the other RIS has nothing scheduled; only the narrowest comes back empty and it is the station AE title that is wrong.
Before you press it
Take the modality off the network first. The probe calls as that modality, and two things answering to one AE title on one RIS is a confusion somebody then has to go and undo.
What it is not
Not a service, not a schedule and not a queue. What Worklist probes holds is a record of what somebody else answered: those orders are never served on this appliance's own worklist, and it claims none of them. Clear empties that record and does nothing else.
The Worklist probes tab of Activity: one probe round asked as US_ROOM_2 of the other RIS at pacs-peer, listing the five questions — station, date and modality, then each key dropped in turn — and what each one returned, which here is nothing every time, because the demo's other RIS holds no orders; Refresh and Clear buttons sit at the right.
One round of Test worklist: the heading says whom it asked and as which AE title, and each row is one question with the keys it kept, followed by what came back. Read it from the narrowest question to the widest — if a wider question brings orders back where a narrower one did not, the key it dropped is the one that was wrong. Here every question answers nothing, which means the other RIS has nothing scheduled for that room, not that a key is wrong: the demo's peer holds no orders.

Emergency RIS — HL7 orders

Port 2575 · HL7 ORM^O01 over MLLP

What it does
Receives HL7 orders over MLLP, and also lets you hand-key them into the dashboard when nothing upstream is alive. When the study comes back by C-STORE it is matched to its order by accession number (patient ID as a fallback), and the order is closed and archived for the trail — never erased.
Turn it on when
The RIS is down, or you are testing an integration.
What it guarantees
Image delivery is never gated on an order match. A study with no matching order is still stored and forwarded; the order simply stays open for manual reconciliation.
The RIS orders panel: a New order (manual entry) disclosure, Open and Closed tabs, a search box, an Arm sound toggle, and three open orders from the RIS, each showing accession number, patient, modality, requested procedure, the HL7 sender and the time queued, with an Add PDF/image button.
Orders that arrived over HL7, and the disclosure at the top for typing one in when nothing upstream is alive. Each closes itself when the matching study lands — and is archived rather than erased. Orders from the RIS can only be cancelled by the RIS, so they carry no Cancel. A new order flashes the Orders row, and this workstation can also beep for it: the toggle is per machine and remembered. An eye-photo order (modality OP) also offers Add eye photos: the editor opens on that order, and the photos it makes come back as the order's study, which closes it.

No TLS, no credential. See the security section: anyone who can open a socket to that port can inject orders. Trusted clinical network and a firewall, or not at all.

Emergency failover

C-ECHO monitoring of the destination marked primary

What it does
Mark a destination as primary and arm the monitor: Carino C-ECHOes it periodically and watches forward failures. If it stays unreachable past the threshold you get the prompt to activate the emergency RIS. Activating starts the local worklist so technologists keep scanning, holds every study received during the outage, and forwards it once the primary is back. You decide when to stand down.
Turn it on when
This machine is the gateway to a PACS you depend on. It is why the word "continuity" is in the first line of this manual.

Notifications — webhook and e-mail

Outbound only · HTTP webhook, SMTP

What it does
Reaches the people who do not have the dashboard open. The banner on screen is enough for whoever is watching a transfer and useless for the case this exists for: the primary goes down at 03:00 and the radiologist on call is not looking at a screen. Two channels, independent of each other, both dead until the master switch is on.
What it sends
Failover events, and nothing else — triggered, activated, resolved. No other part of the appliance produces a notification. This is not a general alerting system and does not become one by being switched on.
Who receives the mail
Profiles, not addresses. A message goes to every enabled profile that failover is set to notify and that has an e-mail address on it. SMTP configured with no address on any profile sends nothing at all, quietly — that is the first thing to check when “e-mail is on and nobody was told”. The wording differs by role, and the failing destination's address appears only for a reader already allowed to see the destination table: an e-mail leaves the building and cannot be recalled.
What it will never do
Delay or break the thing it is reporting. Every send is handed to a worker thread, the queue is bounded — the oldest is dropped and the drop is counted, because a stale “primary is down” delivered after recovery is worse than silence — and a failure becomes a counter and a log line rather than an exception. Sent, failed, dropped, queued and the last error are all readable, so “enabled and has never actually delivered anything” is visible before the outage that depends on it. Test it on a quiet afternoon, not during one.
Where to set it
Not in the dashboard yet: in the notify section of config.json (the webhook URL, the SMTP server, and the master switch notify.enabled). The webhook secret and the SMTP password go through POST /api/notify/secret, never into the file by hand. Every key is described in CONFIGURATION.md.

Virtual print

Port 11113 · DICOM Print, PDF or Secondary Capture output

What it does
Presents itself as a DICOM printer (AE title CARINOPRINT by default) and captures every film sheet sent to it. For equipment whose only output is film, this is how you keep something filable. A film carries no reliable patient identity, so each one waits in Pending for a person to identify it; nothing printed is forwarded on its own.
Turn it on when
You have a print-only modality whose output you want to rescue. The steps are under How do I… print.
Settings
Under Configuration → Settings → Printing: the AE title and port, Accept only these AEs (the modalities allowed to print; empty means any), Also accept color print jobs, and Store each captured film as PDF or Image. Pick Image when the receiving PACS or viewer does not display PDFs — many do not.
How you know it works
Send a test print on the card runs a whole print through the receiver and drops a sheet marked "TEST PRINT — safe to discard" into Pending. After a real job the card shows Last print (how many films, from which AE and address) and the Captured counter. A refusal or failure shows as Last problem; see the print troubleshooting table.
What it tolerates
Modalities that leave the film session and film box numbers to the printer, a Presentation LUT (accepted and ignored), and a printer AE title typed differently on the modality — the called AE is not checked.
The Pending tab of Studies, its tab reading 3: the JPEG and, below it, the PDF that arrived beside the MR study, each with patient name, patient ID, accession number, study date and study description already filled in from that study, a Preview link, a Match to order… button and Approve & send / Discard buttons. The third item, the film captured from the print receiver, is further down the list, below the bottom of the screenshot.
A JPEG and a PDF found beside the MR study, each with that study's details already filled in. Anything that is not DICOM waits here rather than being converted on a guess — film captured by the print receiver lands on this tab too, below these two in this list. Pick the open order it belongs to with Match to order…, or type the identity (patient ID and name are required), then approve. Change the patient on an item that arrived beside a study and you are asked whether it still belongs to that study or starts a new one.

Web dashboard

Port 8042 · HTTP, 127.0.0.1 by default

What it does
Per-service status, destinations, orders, a live activity log, settings and a bundled DICOM editor. Everything the dashboard does also exists in the CLI.
Getting around
The sidebar has six rows: Overview, Services, Studies, Orders, Configuration and Activity — and a seventh, Dev peer, only in a process started with --dev-peer. The three questions asked of the same pile of files are the tabs of Studies — History, Pending and Stuck; what you set when commissioning the machine are the tabs of Configuration — Destinations, Routing, Settings, Modalities and People, and it opens on Destinations, not on Settings; and the records are the tabs of Activity — Logs, Connections, Audit and Worklist probes. Connections lists recent DICOM connections both ways and says why one failed and what to change, on which machine; the What it accepts button on a destination row asks that node about every kind of image stored here. Worklist probes is what another RIS answered when this appliance asked it as one of your modalities. Orders keeps its own strip of open and closed. The Studies row carries two counts that are buttons in their own right, 📎 pending and ⚠ stuck, and each opens its own tab: so reaching Stuck is one click while something is stuck, and two — the row, then the tab — when nothing is, because a count of zero hides itself. A counter that permanently reads 0 is an alarm that is always on, and nobody looks at those. Orders carries its own count on the same terms. Every panel and tab has its own address — #studies/stuck, #configuration/routing, #activity/logs — so a place in the dashboard can be bookmarked, read out over a phone during an outage, and left with the browser's Back button; older #dlgStuck-style links still resolve. Overview is the one exception: it answers to #overview if you type it, but it is never written to the address bar and leaves no history entry, because it prints a patient's name and an unattended screen must not come back to it after a reload.
The chips in the header
The service chips along the top are unchanged for anyone who may start and stop services. A profile without services.control still sees them, as indicators, but they are disabled: service state is not privileged — the front desk has to be able to see that the receiver is dead — and a switch the server would refuse is not worth offering.
Move it off loopback when
Only with a concrete reason — and then with a token (mandatory) and HTTPS in front. See the token rule.
The Settings tab of Configuration, opened on its first group, This PACS, marked Receiver: running: the receiver's Run this service (also at startup) box, AE title, bind address, port, storage folder, an Organize by Patient / Study / Series box, the free-space floor, the AE allow-list and a Serve DICOM over TLS box. The other groups are further down, and Re-run service setup and Save configuration buttons sit at the bottom right.
Everything in config.json, in a form, grouped by job: open the group you need and leave the rest folded. The file stays the source of truth — an edit here writes it, and a config the dashboard would refuse to save is refused with the reason and the field it is about, not silently rewritten. The groups are collapsible sections — This PACS open here, Sending, Worklist and orders, Printing and the rest folded below it — and Shut down service sits alone in the last one, Danger zone. Re-run service setup and Save configuration are the footer buttons, always in view.
The bundled DICOM editor with a sample study open: the image on the left with window centre and width sliders, a Featured clinical panel on the right listing patient, modality and study description with the patient's name masked behind a Show button, and a Technical panel below it giving SOP Instance UID, transfer syntax, photometric interpretation and the file's SHA-256.
The bundled editor, opened from a study on the History tab or on its own. It runs entirely in the browser — nothing is uploaded anywhere — and the patient's name starts masked, because the most common reason to open a header is to read a UID.
The editor's Edit tab: a toolbar with Anonymize (PS3.15), Options, Fake patient (test data), undo and redo, + Tag, JSON, CSV, Print, This file and Download all; a strip of the five loaded series; filter chips for patient, study, series, image, equipment, UIDs and private tags; the tag table itself, one row per attribute with its group and element numbers, description, VR and an editable value; and down the right-hand third an Image edits panel — rotate, flip, invert and redact burned-in text, written into the stored pixels — above the frame preview, a folded Window/Level reading and the drop zone.
The tag table, where a header is corrected before a study is re-sent — a wrong accession number or a mistyped patient ID, fixed at the gateway rather than at the modality that has already moved on.

The dev peer — a disposable second archive

Loopback only · pacs serve --dev-peer · never in a shipped build

What it does
Creates a whole second archive inside this same process, on 127.0.0.1 only, with its own AE title, its own receiver and Query/Retrieve ports and its own storage in a temporary folder — and a destination on this appliance already aimed at it. It is somewhere real for a study to go while you prove that forwarding, a routing rule, de-identify-on-forward or C-MOVE does what you believe it does. A black-hole destination is created beside it, switched off: enable that one and sends fail on purpose, which is how you fill the Stuck tab on demand and find out what being stuck looks like before it happens for real.
How to open one
Start the engine with pacs serve --dev-peer — it says so on the line under the dashboard URL — then pick 🧪 Dev peer in the sidebar and press Create peer. The panel then reports its AE title, both ports, its folder and what it has received. Without that flag the row is not there, the API answers 404, and no setting in config.json will bring it back. That is deliberate: a setting can be changed by anybody holding the dashboard's token, and a launch argument cannot be reached over HTTP at all.
Who may
A profile allowed to create and discard the disposable test archive. Among the built-in profiles that means IT, and an administrator — proving where a forward is dying is bench work. Reception and the radiologist do not get it: a second archive is a bench tool, not a clinical one. Creating one and discarding one are both written to the audit trail.
What becomes of what it stored
All of it goes — the folder, and both destination rows on this appliance — when you press Discard, when the process stops, and on the next start after a crash or a power cut. Nothing outlives it, which is the entire reason to use this rather than hand-writing a second config and forgetting the folder. One thing to expect: discard a peer with sends still queued against it and those sends have nowhere left to retry, so clear them in Studies → Stuck.

A bench tool, and the flag is what keeps it one. The peer is loopback-only and unconditionally so — a test archive a modality on the network could find would be a second, unaudited store of somebody's images, and nobody would notice it was there. Do not pass the flag on an appliance holding real studies. The desktop and container builds never pass it for you.

7. This is not a medical device

Carino DICOM is not a medical device. It is not certified, cleared or registered with any regulator, in any country. It carries no CE marking as a medical device, no FDA clearance, and no registration with ANVISA, COFEPRIS or any equivalent body. Nobody has run a clinical-use validation against it.

It is not for primary diagnosis. There is no diagnostic viewer here: no windowing, no measurements, no calibrated rendering chain, no control over the display it appears on. Read studies on the validated workstation you already have.

Stated plainly, because that is more useful than a footnote:

None of this is pessimism. The project takes seriously that an image that silently never arrives is worse than a crash — which is why it would rather refuse to start, count failed sub-operations explicitly, and over-send than under-send. It withholds a delivery in exactly one case, loudly and reversibly: a destination a rule asks to de-identify for, while the scrub that was promised cannot be performed — because the profile is off, or because the profile is on and no de-identifier could be built. It says which of the two, because the two are fixed differently. But software can only answer for what it does; the rest is yours.

8. Licence and where to get help

Carino DICOM is released under AGPL-3.0-or-later. Because it is a network server, §13 applies: if you run a modified version as a service, you must offer its source to the people who use it. Keep the LICENSE file and a pointer to the source with any modified deployment.

Found an error in this manual — or a translation a radiologist would never say? Open an issue. The Spanish, Portuguese, Japanese and Russian manuals are part of the project, not an extra.

Carino DICOM · landing page · part of the carino.systems workshop · AGPL-3.0-or-later.