Changelog
Changes are listed newest first. For history prior to 0.1.0, see git log.
0.3.1 - 2026-09-12
Section titled “0.3.1 - 2026-09-12”Changed
Section titled “Changed”-
Template
_scriptsare no longer transformed bydapsman init. The folder is copied verbatim for every template, so a new project’s scripts are byte-identical to the template’s. Scripts that need the project name read it from theDAPS_PROJECTenvironment variable instead of having it substituted into the source at init.The point is updatability. A transformed script diverges from its template the moment a project is created, so improvements to a template’s scripts never reached the projects already made from it without a hand diff. Now updating a project’s scripts is a copy, and
diff -r templates/<template>/_scripts <project>/_scriptssays whether a project is current.Exclusion is a framework convention in
TemplateManager, not atemplate.yamlentry, so it applies to future templates automatically and can’t be forgotten. Compose and Caddy files are still transformed — they carry genuinely per-project values (dev ports, prod URL) that must be written at init.Affected template scripts:
prerequisites.dev.shandprerequisites.prod.sh(wordpress and grist) andbuild-docker-images.toolkit.sh(wordpress). Each now guards with: "${DAPS_PROJECT:?DAPS_PROJECT is required}", matching the existing toolkit scripts. The WordPress image tag is derived as<project>-wordpress, which is exactly what the transform used to write into the composeimage:key, so the two still agree. -
Dapsman now sets
DAPS_PROJECTon the three script paths that previously got no environment: local build prerequisites, remote prerequisites (run over SSH duringprod deploy), andbuild-docker-images.toolkit.sh(which previously received onlyDAPS_TARGET_PLATFORM). It was already set for backup, restore, sync, and post-deploy scripts.Existing projects are unaffected. Their scripts have the project name baked in from when they were created and simply ignore the extra variable. No migration is required. To adopt the new scripts, copy them from the template — which is the whole point of the change.
0.3.0 - 2026-09-10
Section titled “0.3.0 - 2026-09-10”-
dapsman --versionto show the current version of the dapsman CLI and by extension the version of Daps itself. -
dapsman prod system [--verbose]— reports how the remote server is doing: architecture, CPU cores and load, memory, disk, and whether a reboot is pending. Read-only; it collects values over SSH from the toolkit and writes nothing to the host.--verboseadds uptime, per-container CPU and memory, per-project disk usage, and total Docker disk consumption.Architecture is reported as
uname -malongside the Docker daemon’s own platform:x86_64 (docker linux/amd64). The two are collected separately because they can legitimately disagree, and the Docker value is the authoritative one — it is what will actually execute an image.Memory “used” comes from
MemAvailablerather thanMemFree, so page cache is not counted against the user —MemFreewould show a perfectly healthy long-running server as almost out of memory.A pending reboot is reported with an explanation rather than a command, because there is no Dapsman workflow that reboots a host:
prod provisionconfigures unattended-upgrades to reboot automatically at 04:00, so the flag normally clears overnight, and the manual route is the hosting provider’s control panel.The remote half is
scripts/remote-system-status.sh, which only collects — it emits tab-separated records and does no arithmetic, unit conversion, or percentages. Dapsman interprets those in C#. It uses only coreutils and/proc, so nothing needs installing on the host, and each optional section is individually fault-tolerant: a host without Docker, or with nothing deployed yet, loses that section rather than failing the command. -
dapsman local backup [--project <name>...]— snapshots the local dev instance to_backups/from_local/, for taking a checkpoint before an upgrade or a risky change to a site still in development. Runs_scripts/backup-local.toolkit.shfrom the toolkit container; projects without that script are skipped with a message, the same conventionprod backupuses. Implemented in thewordpressandgristtemplates. Not implemented forastroorstatic, which are tracked in git and already have history.Unlike Grist’s
prod backup— an rsync mirror that only ever holds one copy — the Grist local backup writes a timestamped archive per run, and stops the Grist container while it archives so the open SQLite files can’t be caught mid-write.
Changed
Section titled “Changed”- WordPress template base image upgraded from
wordpress:7.0-php8.3-apachetowordpress:7.1-php8.3-apache dapsman local restorenow finds restore points in_backups/from_local/as well as_backups/from_prod/. Both directories are scanned, merged, and renumbered most-recent-first, so--restore-point <n>counts across both; the existing environment label on each entry says which one it came from. Restoring alocalbackup skips the prod→dev URL replacement (the URLs are already dev URLs) but still flushes the cache, which is required after any database import.DapsmanRunnerclass now lazy-loads all dependencies, e.g.IToolkitResolver,ICaddyResolver,IBashRunner,DapsConfig, etc. That way workflows that are supported before certain dependencies exist, e.g.dapsman local build, which installs the toolkit container on first run, needs to be able to run before the toolkit container exists.dapsman prod deploynow builds project images for the remote host’s architecture instead of the workstation’s, passing it tobuild-docker-images.toolkit.shasDAPS_TARGET_PLATFORM. Previously the image inherited the workstation’s architecture — harmless from Windows or an Intel Mac to an x86_64 VPS, but from an Apple Silicon Mac the deploy reported success and the container restart-looped on the remote withexec format error. Scripts that don’t read the variable keep working unchanged. See Deploying to production and workflow-remote-deploy.md.dapsman prod deploynow verifies each image against the host afterdocker loadand fails with both platforms named if they differ, catching tarballs built beforeDAPS_TARGET_PLATFORMexisted.- Refactor: removed the single-argument IHostingProviderResolver.Resolve.
- Refactor: workstation docker compose commands are built once and included as properties in LocalBuildPlan, to ensure that the dry run describes exactly what the real run will do, and to stop repeated calls to the command builder.
- Refactor:
dapsman initno longer supports_scripts/init-template.toolkit.shscripts, obviating the fixes from v0.2.2. The logic for placeholder substitution and line ending consistency is now in dapsman itself. Atemplate.yamlfile now contains template-specific configuration info, currently only supporting the list of folders to exclude from transformation. Thetemplate.yamland any_secretsfolder that might exist are excluded from being copied to the destination. - Refactor: the readme cli commands page now lists cli commands in alphabetical order
- Refactor: the toolkit’s
.bashrcno longer sources per-project*.bashrcfiles. The feature came from an early idea that projects would need shell-level environment variables, which was never needed, and its glob (/srv/projects/*/scripts/) predated the_scripts/convention — so it had silently matched nothing for some time.
.gitattributesnow pins*.bashrcto LF endings.docker/toolkit.bashrcis copied into the toolkit image at build time, and withcore.autocrlf=truea Windows checkout converted it to CRLF, so every command running through the toolkit printed carriage-return and syntax errors first. Note that fixing the file’s endings does not fix a running container — the copy inside it is baked into the image, so it takesdapsman local build --build.- The
wordpresstemplate’s.gitignoreignoredbackups/, but the folder is_backups/— database dumps and content archives were not actually being ignored. - Backup scripts now write each artifact to a
.partialfile and rename it only once the command producing it has succeeded, inbackup-local.toolkit.sh(wordpress,grist) andbackup-remote.toolkit.sh(wordpress). The shell creates a redirect’s output file beforemysqldumportarever runs, andscpwrites as it goes, so a dump that failed part-way left a truncated file sitting in_backups/looking like a usable backup — anddapsman local restorewould offer it as a restore point. An interrupted run now leaves the backup directory as it found it.
0.2.2 - 2026-08-12
Section titled “0.2.2 - 2026-08-12”-
dapsman initnow works on macOS. Every template’sinit-template.toolkit.shusedsed -i "s/…/…/g" "$file"for placeholder replacement, which is GNU-only: BSD sed (macOS) requires-ito take a backup suffix, so it consumed the expression as the suffix and then parsed the filename as the sed script, failing withcommand a expects \ followed by text. Replacement now goes through areplace_in_filehelper that works identically under GNU and BSD sed. Affects thewordpress,grist,astro, andstatictemplates.The helper writes a sibling temp file (seeded with
cp -p, so permissions carry over) andmvs it into place. The rename matters: the init script’s own source contains the placeholder, so the replacement loop rewrites the file bash is still reading. Renaming leaves the running shell’s file descriptor on the original inode; overwriting in place shifts every later byte offset and bash resumes mid-token, producing errors likeories: command not found. GNUsed -ihad been getting this right by accident — it renames too.Despite the
.toolkit.shsuffix,init-template.toolkit.shruns on the workstation (InitServiceis wired with_workstationBashRunner), so it must stay portable across Git Bash, macOS, and Linux — noted inCLAUDE.md. -
dapsman local build --build --project mywpsiteanddapsman prod deploy --project mywpsiteworkflows for Wordpress are updated to pull new images when the floating7.0-php8.3-apacheimage receives a new patch update, e.g. 7.0.2 to 7.0.3.
0.2.1 - 2026-08-09
Section titled “0.2.1 - 2026-08-09”-
dapsman prod provisionnow applies Daps host policy over SSH via the newscripts/configure-host.sh, for bothopenstackandgeneric-vpsproviders. The script enables an automatic reboot at 04:00 when an unattended security upgrade requires one, by writing a Daps-owned drop-in at/etc/apt/apt.conf.d/52daps-unattended-upgrades. Ubuntu already installs security updates unattended, but ships withUnattended-Upgrade::Automatic-Rebootunset — so new kernels were installed and never booted into, leaving hosts running a vulnerable kernel indefinitely. The script also installsunattended-upgradesif the host image lacks it, and asserts theapt-dailytimers are enabled.The script is idempotent, so running the upgraded
prod provisionagainst a host provisioned by an earlier version of Daps brings that host up to current policy. Foropenstackproviders this adds a post-create SSH step, which waits for the instance to accept connections; the step is also why re-provisioning an existing instance now reaches the host rather than stopping after the create script.Containers use
restart: unless-stoppedand return after the reboot. Anything deliberately stopped bydapsman prod offlinecorrectly stays stopped.
- Shell scripts uploaded to a remote host are now staged with LF line endings, by both
dapsman prod provision(provision-generic-vps.sh,configure-host.sh) anddapsman prod deploy(a project’s_scripts/*.sh). On a Windows checkout,core.autocrlfcan leave CRLF endings in the working tree, and a CRLF shell script fails as soon as it runs on Linux withset: pipefail: invalid option name. Git Bash tolerates CRLF, so this only ever surfaced on the remote. Normalization is done by the new, explicitly namedConfigUtils.CopyFileAndReplaceLineEndingsForLinux; other staged files keep a verbatim copy, since image tarballs and manifest uploads may be binary and compose/caddy files tolerate CRLF.
Changed
Section titled “Changed”dapsman prod provisionnow runs through aToolkitRemoteProvisionExecutorthat stages its scripts under.dapsman/provisionand runs a generatedprovision.sh, rather than passing one long&&chain tobash -lc. This matches the executor + staging pattern used byprod deploy, and is what makes the line-ending normalization above possible. The staging directory is removed after the run.
0.2.0 - 2026-08-05
Section titled “0.2.0 - 2026-08-05”dapsman local build --rebuildrecreates the selected projects from scratch, runningdocker compose down -vbeforeup. Recovers a project whose named volumes are unusable — e.g. a MySQL data directory left half-initialized by an interrupted first build, which fails withMY-012960: Cannot create redo log files because data files are corrupt. Implies--build, requires--project, and prompts for a typedyes.--yesflag to skip the--rebuildconfirmation prompt for scripted useDISALLOW_FILE_MODS trueadded to WordPress template prod compose file, blocking plugin/theme installation and the file editor from WP Admin. Paired with the existingWP_AUTO_UPDATE_CORE false, these two constants close the main in-container file-write attack surfaces.postmortem-2026-07-loccom-compromise.mddoc
- First-run chicken-and-egg:
dapsman initanddapsman local buildfailed on a workstation where Daps had never been built, because every command resolved the toolkit container at startup and errored with “No running toolkit container found. Start local Daps first (dapsman local build)”. The toolkit is now resolved only by the workflows that use it, and the caddy container name is resolved by convention when no container is running. Workflows that genuinely need the toolkit (allprodworkflows,local restore,local sync-from-prod) still report a clear error, and--dry-runno longer needs any container to exist. dapsman local buildnow creates the shareddaps_netDocker network if it is missing, in a newdocker-networkstep. Compose files declare it as external, so a fresh workstation previously failed with “network daps_net declared as external, but could not be found”.dapsman initno longer requires Docker to be running — it copies a template and runs the template’s init script, so it now checks only for bash.- The missing-
daps.yamlerror now says to copydaps.yaml.example. dapsman prod offlinenow generates its offline Caddy config from every site address in the project’s*.prod.caddyfile, not just the first. Previously a file with a comma-separated address list (example.com, www.example.com {) produced an invalid site address with a trailing comma, and any additional site blocks (e.g. awwwredirect or anapisubdomain) were dropped from the offline config entirely.dapsman prod offlineanddapsman prod onlinenow fall back to the project’s ownprovider:fromdaps.yamlwhen no--provideris given. They were resolving to the first provider in the file instead, so a project hosted elsewhere was taken offline against the wrong server — the command reported success while the real site stayed up. Every other per-project prod workflow already did this; these two were the exception.dapsman prod unprovisionnow requires--providerwhen more than one provider is configured, instead of silently defaulting to the first one indaps.yaml. It destroys a VM and its keypair and has no project to infer a provider from, so guessing was the wrong default —dapsman prod provisionalready worked this way.- fix titles of workflow docs
Changed
Section titled “Changed”dapsman local caddy restartanddapsman prod caddy restartnow pass--forcetocaddy reload. Caddy skips a reload when the incoming config is byte-identical to the running one, which made both commands a no-op in the case they are most needed: forcing Caddy to retry TLS certificate issuance after a domain’s DNS was pointed at the server. The caddy file is unchanged in that scenario, so the reload was silently skipped and the site stayed uncertified until Caddy’s own retry backoff elapsed.- misc documentation updates
0.1.0 - 2026-07-24
Section titled “0.1.0 - 2026-07-24”- Version tracking:
<Version>0.1.0</Version>added toDapsman.Cli.csproj - Upgrade instructions added to all template READMEs (WordPress, Grist, Astro, Static)
WP_AUTO_UPDATE_CORE falseset in WordPress template compose files to prevent silent background core upgrades/daps-dryrunand/daps-prClaude Code slash commands for health checking and pre-merge review
Changed
Section titled “Changed”dapsman local build --buildnow passes--renew-anon-volumestodocker compose up, ensuring that image upgrades (e.g. WordPress) take effect immediately rather than being shadowed by stale anonymous volumesdapsman prod deploynow passes--renew-anon-volumestodocker compose upon the remote server for the same reason- WordPress template base image upgraded from
wordpress:6.7-php8.3-apachetowordpress:7.0-php8.3-apache - All template images pinned to specific versions:
phpmyadmin:latest→phpmyadmin:5.2nginx:alpine→nginx:1.31-alpinegristlabs/grist:latest→gristlabs/grist:1.7