Troubleshooting an install
See also: Production install · Reverse proxy and TLS · Installer reference · Dev quickstart
Symptom first. Each entry names the cause and the fix. If your symptom is not here, start with the two things that answer most questions:
journalctl -u dedalo-ts -n 100 -o cat # the engine's own words
curl --fail --unix-socket /run/dedalo/dedalo_ts.sock http://localhost/health
| Symptom | Jump to |
|---|---|
The installer stops on a psql complaint |
Installing |
Database is not empty … restore refused |
Installing |
| The server refuses to boot | Booting |
Cannot find package … on start |
Booting |
| The socket is not where the unit says | Booting |
| The server serves the install wizard instead of the app | Booting |
| The wizard resumes on an install that is already finished | Booting |
The wizard is a 404, or refuses your address |
Booting |
502 from the proxy |
Serving |
Apache: AH01144, or AH01630 naming a path under /etc/apache2 |
Serving |
A client asset 404s at a path that is not in the repo |
Serving |
| nginx will not start | Serving |
| A big export dies after about a minute | Serving |
Uploads fail with 413 |
Serving |
| The maintenance widget says the engine is down, but the socket probe is green | Serving |
| Nobody can log in, and there is no error | Using it |
Media 404s |
Media |
| Media is served to everyone | Media |
MEDIA_HTACCESS_ADDONS rules do not appear |
Media |
| Uploads produce no thumbnail | Media |
Installing
PostgreSQL client (psql) not found
Cause. The pre-flight check could not resolve a psql binary. The installer,
the seed restore, the hierarchy import and the backup widget all shell out to it,
so this is a hard gate.
Fix. Install the client package (postgresql-client-18), or point at it:
DEDALO_PG_BIN_PATH=/usr/lib/postgresql/18/bin
The install fails part way through, with a connection error
Cause. Very often: psql is older than the server. An older client
refuses to connect to a newer server, and a machine with several major versions
installed can easily resolve the wrong one.
Fix. Check both, then pin the client with DEDALO_PG_BIN_PATH:
psql --version
sudo -u postgres psql -c 'SHOW server_version;'
Database is not empty (matrix_users already exists) — restore refused
Cause. Exactly what it says. The installer restores into a database and
never clobbers an existing install. It probes for the matrix_users table.
Fix. Point at an empty database, or drop and re-create this one — but be
certain first, because a populated matrix_users means somebody already
installed here.
DROP DATABASE dedalo_main;
CREATE DATABASE dedalo_main WITH ENCODING='UTF8' OWNER=dedalo_user;
Private config directory is not creatable/writable
Cause. The installer creates ../private/ — a sibling of the repo. The
directory one level above the repo is not writable by the service user.
Fix. chown dedalo:dedalo /opt/dedalo (the parent, not the repo). In a
container, this is what DEDALO_PRIVATE_DIR is for — see Docker.
Booting
Missing required config key 'DB_NAME' (or ENTITY, DB_HOST, DB_USER)
Cause. A partial configuration. The four keys ENTITY, DB_NAME,
DB_HOST, DB_USER are all-or-nothing: with none of them set, the server
boots into install mode; with some of them set, it is misconfigured and says
so. This is deliberate — a half-configured server must not quietly fall back to
install mode against a real database.
Fix. Set all four (they are written by the installer, under their
DEDALO_*_CONN spellings), or none.
The server serves the install wizard instead of the application
Cause. It booted with none of those four keys set, so it is in install
mode. Almost always: it is not reading the .env you think it is.
Fix. The file must be at ../private/.env, one level above the repo — or
wherever DEDALO_PRIVATE_DIR points. Confirm:
sudo -u dedalo cat /opt/dedalo/private/.env | head -5
journalctl -u dedalo-ts | grep 'INSTALL MODE'
If the four keys are set and being read, this is not install mode — see the next entry.
The wizard resumes on an install that is already finished
Cause. The install was never sealed. install_finish is what writes
install_status: 'sealed'; until it runs the status stays configured, and a
configured instance deliberately re-mounts the wizard on every reload rather
than dropping to a login form (that is what lets a mid-install reload resume
instead of stranding you on a login with no schema and no root user). Abandon the
wizard one step before the end — close the tab, restart the server — and it
resumes forever, even though the database is fully built.
Recognise it by the state file plus a configured .env:
grep install_status ../private/ts_state.json # "configured", not "sealed"
Fix. Reopen the wizard and let it run to Finish, which calls
install_finish. It refuses to seal unless the root user exists with a password
set, so it will tell you if a step really is outstanding. To seal from the shell
instead — same guard, same result — run from the repo root:
bun -e "import {installFinish} from './src/core/install/finish.ts'; console.log(await installFinish())"
The seal applies immediately (the state file is read fresh on every request; no
restart), but reload the browser page: the wizard is already mounted in the
open page's JavaScript and only goes away when the client calls start again.
Config key 'DEDALO_PREFIX_TIPOS' is RETIRED
Cause. A retired key is not an alias: it configures nothing. Left in place it would silently fall back to the replacement key's default, so the server refuses to boot instead.
Fix. Rename the line in ../private/.env:
ACTIVE_ONTOLOGY_TLDS=dd,rsc,oh,ich,lg,hierarchy
Crash loop right after the wizard's Save config
Cause. The language configuration is mandatory and the written .env is
missing it, so every boot dies on:
Config key 'DEDALO_APPLICATION_LANGS' must be a non-empty JSON object map
The wizard collects the languages on the Entity step; a hand-written .env
has to carry them itself.
Fix. Ensure all four keys are present:
DEDALO_APPLICATION_LANGS={"lg-spa":"Castellano","lg-eng":"English"}
DEDALO_PROJECTS_DEFAULT_LANGS=["lg-spa","lg-eng"]
DEDALO_APPLICATION_LANGS_DEFAULT=lg-spa
DEDALO_DATA_LANG_DEFAULT=lg-spa
FATAL: another server instance is already listening on … — refusing to steal its socket
Cause. The double-start guard. A pre-existing socket file is probed with a
connection; if something answers, a second instance would silently orphan the
first, so it exits 1 instead.
Fix. Stop the running instance, or point SERVER_UNIX_SOCKET elsewhere. A
stale socket file (nothing listening) is removed automatically — this error
only fires when something really is alive on it.
error: Cannot find package 'zod' from '…/src/core/concepts/rqo.ts'
Cause. That clone has no usable node_modules. The package name varies; the
path in the message is the authoritative part — it names the tree the engine is
running from, which is not necessarily the tree you installed into.
Almost always one of two things:
bun installwas never run in that directory, or was run in a different one.sudo -u <user> <cmd>does not move to that user's home; it inherits the directory you are standing in, so an install launched from/rootreportscould not find a package.json file to install fromand touches nothing.- It was run as
root, leaving anode_modulesthe service user cannot read.
Fix. Install as the service user, in the clone, in one command:
sudo -u dedalo bash -c 'cd /opt/dedalo/master_dedalo && /opt/dedalo/.bun/bin/bun install --frozen-lockfile --production'
ls -d /opt/dedalo/master_dedalo/node_modules/zod # proof it landed
Then clear the crash-loop trip before starting — Restart=always does not:
sudo systemctl reset-failed dedalo-ts && sudo systemctl start dedalo-ts
The engine listens on /tmp/dedalo_ts.sock instead of /run/dedalo/…
Cause. SERVER_UNIX_SOCKET is not set in ../private/.env, so the engine
falls back to its default path and ignores the unit's RuntimeDirectory
entirely. The boot line says exactly where it bound:
Dédalo TS server listening on unix socket /tmp/dedalo_ts.sock (entity: …)
Two consequences, both delayed: the proxy points at a socket that never appears
and every request 502s, and a second install on the same host refuses to
boot, because something is already answering on the default path.
Fix. Set it per install, matching the unit's RuntimeDirectory, and restart:
SERVER_UNIX_SOCKET=/run/dedalo/dedalo_ts.sock
Failed to determine group credentials: No such process
Cause. The Group= in the systemd unit — or in a drop-in overriding it —
names a group that does not exist. The message is misleading: No such
process is the errno the name lookup returns for no such group. The engine
never starts; systemd fails the unit before executing anything.
This is the normal first failure on a host upgraded from v6, where the
service user's primary group is the web server's (gid=33(www-data)) and no
group is named after the user.
Diagnose.
id <service user> # what group does it really have?
getent group <name> # empty output = that group does not exist
systemctl cat dedalo-ts # what the unit and its drop-ins actually say
Fix. Three options, best first:
-
Give the install its own group — the only one that keeps two installs on one host apart:
sudo groupadd <name> && sudo usermod -g <name> <service user> sudo chown -R <service user>:<name> <install home> <media path> sudo usermod -aG <name> www-data && sudo systemctl restart apache2On an upgraded install this is a documented migration step, with the reasoning: Phase E.
-
Point
Group=at the group that exists (systemctl edit dedalo-ts). Unblocks a test box; on a shared host it hands socket access to every member of that group. -
Omit
Group=and inherit the user's primary group. Only acceptable when that group is install-specific — inheritingusers,nogroupor the web server's group is option 2 with the trade-off hidden.
Then, in this order — Restart=always does not clear a start-limit trip:
sudo systemctl daemon-reload
sudo systemctl reset-failed dedalo-ts
sudo systemctl start dedalo-ts
The unit fails before the engine logs anything
journalctl -u dedalo-ts shows a status= code and no output of ours: systemd
could not set up the process. The code says which step:
| Code | Meaning | Usual cause |
|---|---|---|
217/USER |
the user does not resolve | User= misspelled, or the account was never created |
| group credentials | the group does not resolve | see the entry above |
200/CHDIR |
cannot enter WorkingDirectory |
wrong path, or the service user cannot traverse into it |
203/EXEC |
cannot execute ExecStart |
the pinned runtime is not at that path — it is installed per install, so it is easy to point at another install's copy |
Reproduce the same conditions by hand before editing the unit again:
sudo -u <service user> <ExecStart bun path> --version
sudo -u <service user> bash -c 'cd <WorkingDirectory> && ls src/server.ts'
The wizard is a 404
Cause. The instance is sealed. Sealing is terminal: the entire install
surface answers 404 from then on, across restarts, from any address.
Fix. None, and that is the point. A sealed instance is an installed instance — log in. (If you truly need to re-install: empty database, empty private directory, start again.)
The wizard refuses your address (403)
Cause. Your address is not on the install allowlist. Note that this is the
default state, not an unusual one: with DEDALO_INSTALL_ALLOWED_IPS unset
the wizard answers the local machine and nobody else, because until it is
sealed the installer runs without a password.
Fix. Name the machine you install from — a literal address, a CIDR range, or
any (every address; only behind a firewall, and removed once sealed). The
engine prints the list in force in its start-up log, next to the INSTALL MODE
line, so you can see what it is actually applying:
DEDALO_INSTALL_ALLOWED_IPS=203.0.113.10,10.0.0.0/24
And note that loopback will not match behind a reverse proxy: the address
is resolved from the trusted X-Forwarded-For hop, so name the real client
address.
The boot fails with core module warm-up: N module(s) failed to evaluate
Cause. A module in the engine's core graph failed to evaluate. The server evaluates the whole graph before it listens, and a failure is fatal by design — a visible crash loop beats a server that serves identical failures for the rest of its life.
Fix. This is a code defect, not a configuration one. The log names the modules. Roll back to the previous ref (upgrading) and report it.
Serving
Every request is a 502
Cause. The proxy cannot connect to the unix socket. Connecting to a unix
socket needs write permission on the socket file, and with the default umask
the engine creates it srwxr-xr-x — owner only. The proxy runs as www-data or
nginx.
Fix.
# dedalo-ts.service
UMask=0007
RuntimeDirectory=dedalo
RuntimeDirectoryMode=0750
usermod -aG dedalo www-data # nginx: usermod -aG dedalo nginx
systemctl restart dedalo-ts nginx
ls -l /run/dedalo/dedalo_ts.sock # → srwxrwx--- dedalo dedalo
Also confirm the paths agree: SERVER_UNIX_SOCKET in .env, the upstream in
the proxy configuration, and the watchdog unit's --unix-socket.
Apache: AH01144: No protocol handler was valid for the URL … (scheme 'http')
Cause. mod_proxy_http is not loaded. mod_proxy alone understands the
ProxyPass syntax but has no handler for the http scheme, so it matches the
rule and then has nothing to hand the request to.
A host that previously served an older Dédalo shows this reliably: it has
proxy_fcgi — the submodule its interpreter pool used — and never needed
proxy_http. The engine is reached over HTTP on a unix socket, so the unix:
prefix is mod_proxy's part and everything after the | is proxy_http's.
Fix.
apachectl -M | grep proxy # want proxy_module AND proxy_http_module
sudo a2enmod proxy proxy_http
sudo systemctl restart apache2 # LoadModule needs a restart, not a reload
Apache: AH01630: client denied by server configuration: /etc/apache2/…
Cause. A filesystem path in the vhost is missing its leading slash.
Apache resolves a relative path against ServerRoot (/etc/apache2 on
Debian/Ubuntu), so Alias /dedalo home/site/dedalo/client/dedalo silently becomes
/etc/apache2/home/site/…. No <Directory> block matches that invented path, and
the filesystem default outside DocumentRoot is Require all denied — hence a
denial rather than a 404. The path in the message is the fabricated one, which
is what makes it recognisable.
Fix. Find the offending line and add the slash:
grep -rnE '^\s*(Alias|AliasMatch|DocumentRoot|ScriptAlias|<Directory)\s+"?[^/"]' \
/etc/apache2/sites-enabled/ /etc/apache2/conf-enabled/
apachectl -S # the resolved DocumentRoot per vhost
apachectl configtest && sudo systemctl reload apache2
Two variants produce the same AH01630 with a correct-looking path:
- the
<Directory>block's path does not match theAliastarget exactly (a trailing slash, or a symlinked component — Apache matches the resolved path); -
the web-server user cannot traverse into the tree. A
0700home denies it beforeRequire all grantedis ever read:sudo -u www-data ls /home/<site>/<clone>/client/dedalo >/dev/null
A client asset 404s at a path that is not in the repo
Symptom. One /dedalo/lib/… or /dedalo/core/… request fails while the page
otherwise loads, and the body is the engine's own envelope:
{"ok":false,"request_id":"…","error":{"code":"resource.not_found", …}}
Cause. That envelope means the engine answered — the proxy is fine. Third-
party libraries are served through an allowlist that maps /dedalo/lib/<id>/<path>
to a registered package root, and refuses anything else rather than guessing. So a
404 here says the requested path does not exist in this install's tree.
Almost always the browser is running a cached module from an older client. On
an upgrade the hostname does not change, so every returning visitor — and the
operator testing the upgrade — carries the previous engine's module graph. Asset
paths that moved between versions are then requested at their old location. A
plain reload does not help: an ES-module import resolved inside a cached module
is not revalidated, which is also why a ?v= on the entry point cannot reach it.
Fix. Confirm it is the cache before touching the server — check whether the requested path exists in the clone:
ls <clone>/node_modules/codex-tooltip/dist/tooltip.js # the path the CURRENT client asks for
grep -rn "codex-tooltip" <clone>/client/ --include="*.js"
If the deployed client imports a different path than the browser requested, the browser is stale. Empty the cache and hard-reload (Safari: ⌥⌘E then ⌘R; Chrome and Firefox: ⇧-reload with the developer tools open, or "Empty cache and hard reload").
Tell your users once, after an upgrade
This is not only the operator's browser. Anyone who used the old install on the same address needs one hard reload; until then they may see a half-rendered interface with a console full of module errors.
If the path genuinely is missing from the tree, this is not a cache problem —
the dependencies were not installed in that clone (see
Cannot find package …),
or the library is not registered in the allowlist, which is a code change, not a
configuration one.
nginx: unknown "dedalo_auth_key" variable
Cause. You included the generated media server rules but not the
generated map file. A map cannot live inside server{}, so it ships as a
separate file that must be included at http{} scope.
Fix. Include both, or neither:
include /srv/dedalo/media/dedalo_media_protection_map.nginx.conf; # http{} scope
include /srv/dedalo/media/dedalo_media_protection.nginx.conf; # server{} scope
nginx: pcre2_compile() failed: missing closing parenthesis
Cause. A known defect in the generated publication-mode rules: the rule-B
location regex is emitted unquoted and contains {2,12}, which nginx's
configuration lexer reads as a block delimiter.
Fix. Quote that one regex. See reverse proxy → nginx.
nginx: open() … dedalo_media_protection.nginx.conf failed
Cause. The engine has not written the rule files yet. It writes them at boot — but only when a media access mode is configured.
Fix. Set DEDALO_MEDIA_ACCESS_MODE, start the engine once, then reload
nginx. Bring the proxy up with the two include lines commented out if you need
the site before then.
A large export or a long tool action dies after about a minute
Cause. The proxy's read timeout is lower than the engine's idle timeout, so the proxy hangs up first.
Fix. proxy_read_timeout 300s; (nginx) or ProxyTimeout 300 (Apache) —
at least SERVER_IDLE_TIMEOUT_S (255).
The assistant chat or a diffusion progress stream stalls
Cause. The proxy is buffering a streaming response.
Fix. proxy_buffering off; on the API location.
Uploads fail with 413 Request Entity Too Large
Cause. nginx's client_max_body_size defaults to 1 MB, and the client
uploads in ~4 MB chunks.
Fix. client_max_body_size 300m; (Apache's default is unlimited — nothing to
do there).
A script 404s with MIME type ('application/json') is not executable
Cause. Nothing to do with MIME types. /dedalo/lib/<id>/… is the
client-library route, and when it cannot resolve
a file it answers with the standard JSON error envelope. The browser refuses to
execute a 404's JSON body as a script and reports that, naming neither the
missing library nor the reason.
Two reasons it will not resolve, and a container usually has both at once:
- the library is marked
devOnly(the test harness:mocha,chai) andDEDALO_DEV_MODEis nottrue, so the route refuses it before touching disk; - its package is a
devDependency, and the image was built withbun install --frozen-lockfile --production, which drops those.
Fix. For the test harness, build the Dockerfile's dev target and set
DEDALO_DEV_MODE=true — both are required, and a dev compose overlay does the
two together. Rebuilding is not enough on a stack that keeps node_modules in an
anonymous volume: Compose reattaches the old volume when it recreates the
container, so bring it up with --force-recreate -V.
For any other library this is a real packaging bug, not a configuration one:
a library the client loads in production must be a runtime dependency. The
client_libs tripwire asserts exactly that, so a red gate is the expected way to
find out.
/health answers 503 {"db":"down"}
Cause. PostgreSQL is unreachable or the connection pool is wedged. The health check is deliberately not liveness-only — monitoring must go red when the database is down, not only when the process dies.
Fix. Check PostgreSQL, then the pool: DB_POOL_MAX is per process, and
the engine plus every diffusion runner plus the RAG drain all draw against
PostgreSQL's max_connections. Watch db_pool_waits on
GET /api/v1/counters.
/health answers 503 {"process":"poisoned"}
Cause. A module in the graph failed at request time and its failure is cached for the life of the process. The engine latches this and reports it so that the watchdog recycles the process rather than serving identical failures forever.
Fix. A watchdog recycles the process; how fast depends on the deployment.
Under systemd the health timer polls every 30 seconds and restarts the unit.
On a docker stack the healthcheck is scripts/ops/container_watchdog.sh, which
recycles the container after three consecutive red probes at 30-second
intervals — roughly 90 seconds, and only once the container has answered green
at least once (so it never recycles a box that is still on the install wizard).
Either way it is a code defect — capture the log and report it.
The maintenance widget reports the engine down while the socket probe is green
Symptom. curl --fail --unix-socket /run/dedalo/dedalo_ts.sock http://localhost/health
answers 200, but maintenance → system info shows the engine unhealthy, and
after a code update the restart poll never confirms the new version.
Cause. The vhost does not route /health. The engine serves it at the
origin root, and the browser client probes it there — so the proxy has to
carry the route. Without it Apache resolves the path against the DocumentRoot
and logs an AH01630 denial for …/health, while nginx hands the request to the
vhost's catch-all and returns 404. Nothing is wrong with the engine; only the
browser-facing checks can see it.
Fix. Add the rule for your server from Reverse proxy and TLS, reload, and re-run the probe through the domain:
curl --fail https://dedalo.example.org/health
Using it
Nobody can log in, and there is no error message
Cause. SESSION_COOKIE_SECURE defaults to true, so the browser discards
the session cookie over plain http://. The login succeeds; the cookie is
thrown away; the next request arrives with no session; you land back on the login
form.
Fix. Serve over TLS. That is the real fix, and the only fix on a server.
On a local development machine only:
SESSION_COOKIE_SECURE=false
Danger
Never set this to false anywhere a real user can reach. The media-auth
cookie inherits the same attribute, so you would also be shipping a working
media authorisation value in cleartext.
Users are logged out more often than they expect
Cause. Two clocks, and either one ends a session — see login:
| clock | key | default |
|---|---|---|
| Idle timeout | SESSION_TTL_SECONDS |
3600 (1h without a single request) |
| Absolute cap | SESSION_ABSOLUTE_TTL_SECONDS |
43200 (12h since login, unconditional) |
Users report the cap as "it logs me out in the middle of my work", because activity cannot postpone it. That is what it is for: the idle window alone never expires a browser that polls in the background.
Fix. Raise the one that is actually biting — do not raise both reflexively. An install on trusted, physically secure workstations can extend the cap; a shared or public workstation wants the idle window short above all.
SESSION_TTL_SECONDS=3600
SESSION_ABSOLUTE_TTL_SECONDS=43200
SESSION_WARNING_SECONDS=300
Users should be warned before this happens: SESSION_WARNING_SECONDS
(default 300) puts a notice on screen that many minutes ahead. If nobody is
seeing it, check it is not set to 0, and that the browser is not running a
stale cached copy of the client (a hard reload settles it).
Running jobs are not interrupted
A logout — voluntary or by timeout — never stops a background import or a publication run. They keep the requesting user on the job record and never re-read the session, and the same user reattaches to a job in progress after logging back in.
Media
A logged-in user gets 404 for every media file
The giveaway is that the application itself works perfectly — records load, searches run — and only images, video, PDFs and 3D files are missing. That is expected: the web server enforces media access, not the engine, so a media failure says nothing about the session.
Cause. Rule A failed: either the browser is not sending the
dedalo_media_auth cookie, or the marker file it names does not exist under
<media>/.publication/auth/. Rule B cannot cover for it — with no publication
markers it never matches, so Rule A is the only door for unpublished media.
Fix. Work outward from the file to the browser:
-
Confirm the marker store exists and is readable by the web server. It holds one marker per LIVE SESSION, named by that session's cookie value (the day-global
<private>/media_auth.jsonis retired — boot renames it.migrated):ls -l /srv/dedalo/media/.publication/auth/ -
Prove the rules are fine by presenting a known-good cookie by hand. If this returns
200, the gate works and the problem is the browser's cookie:curl -s -o /dev/null -w '%{http_code}\n' \ -H "Cookie: dedalo_media_auth=<today's value>" \ https://example.org/dedalo/media/image/thumb/0/<a real file>.jpg -
Check the cookie in the browser. It is
HttpOnly, so read it in DevTools → Application → Cookies, never from JavaScript. It is alsoSameSite=Lax: media embedded cross-site will not carry it, by design. - If it is
Securebut you are on plain HTTP, see the login entry above. - Reload the page. The cookie is re-issued on the next authenticated request, so a stale or missing one heals itself without a re-login. If it does not, no store exists yet — log out and back in, which creates one.
Fixed in the current engine
On engines predating the session-bound cookie, this had a much simpler
cause: the cookie was minted only at login with a fixed 24-hour
Max-Age, while the session renewed on every request. Anyone logged in for
longer than a day lost media access with no other symptom. Re-login was the
only cure. If you are seeing this on a schedule of roughly once a day, that
is what you are looking at.
An anonymous visitor gets 404 for a published record's media
Cause. Rule B did not match. Three candidates, in order of likelihood:
- The record is not published. The marker
<media>/.publication/pub/{section_tipo}_{section_id}is written by the diffusion engine when you publish. No marker, no access. - The quality folder is not public. Only the configured public qualities are
readable anonymously, and master qualities can never be made public — the
originalandmodifiedfolders are filtered out no matter what you configure. - The file name does not parse. Rule B identifies the record from the file name. A file renamed outside Dédalo's naming grammar simply never matches, and stays login-only. That is deliberate; do not loosen it.
Every media file 404s, for everybody, and nginx looks fine
Cause. The root rule. The generated nginx locations carry no root and
no alias; they inherit the server's root, which must resolve
/dedalo/<media dir>/… onto MEDIA_PATH.
Fix. See reverse proxy → the root rule.
With the canonical layout it is root /srv;.
Media is served to everyone (Apache)
Cause. The generated .htaccess is being ignored — silently, and open.
Fix. The media directory needs AllowOverride All (or at least
FileInfo Options) and mod_rewrite enabled. This one line is the entire gate
on Apache.
My MEDIA_HTACCESS_ADDONS rules are not in the generated .htaccess
Cause. The value is not valid JSON, so it was refused. The boot log says so:
[config] MEDIA_HTACCESS_ADDONS must be a JSON array of strings — ignoring the value.
Almost always this is backslash escaping. The key holds a JSON array, so every backslash in an Apache regex has to be doubled:
# wrong — natural Apache syntax, but invalid JSON
MEDIA_HTACCESS_ADDONS=["RewriteCond %{REMOTE_ADDR} ^10\.0\.","RewriteRule ^ - [L]"]
# right — backslashes doubled for JSON
MEDIA_HTACCESS_ADDONS=["RewriteCond %{REMOTE_ADDR} ^10\\.0\\.","RewriteRule ^ - [L]"]
Fix. Correct the escaping and restart. Only your addon lines were dropped — the
access gate itself is unaffected and stayed closed, which is the intended failure
direction: a malformed addon must never become half a directive inside a live
.htaccess (that would make Apache reject the whole media directory).
An uploaded image produces no derivative and no thumbnail
Cause. ImageMagick is not installed, or its binaries cannot be resolved.
Fix.
command -v magick convert identify
Either is fine — the engine probes for magick first and falls back to
convert/identify, which is what Ubuntu 24.04 ships. If they live somewhere
unusual, set DEDALO_BINARY_BASE, or the individual keys
(DEDALO_MAGICK_PATH, DEDALO_IDENTIFY_PATH).
Video uploads work, but playback does not start until the file has downloaded
Cause. qt-faststart is missing, so the MP4 index is still at the end of the
file.
Fix. Install it (it ships with ffmpeg on most distributions) or point at
it: DEDALO_AV_FASTSTART_PATH=/usr/local/bin/qt-faststart.
Unpublishing a record does not take effect
Cause. The web server is caching the stat() of the publication marker.
Fix. open_file_cache off; on the media locations (or
open_file_cache_valid ≤ 2 s). Behind a CDN, purge the record's media paths on
unpublish — the origin denies immediately, downstream caches do not.