- Rust 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| presets | ||
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| README.md | ||
dostavka
Deploy a project over ssh and rsync, and pull a database down to this machine. The binary does not know Symfony, Kirby, React, or Express. A framework is a preset: build commands, rsync rules, and what to run on the server.
Phoenix stays on vps_deploy. type = "phoenix" runs mix deploy.
Nothing here uses Docker. Hosts come from ~/.ssh/config. Changes are listed in CHANGELOG.md.
Install
cargo install --path ~/Sites/dostavka
~/.cargo/bin needs to be on PATH.
Commands
Dry-run is the default. force is the only switch that writes.
dostavka deploy # build, then show the rsync plan
dostavka deploy --target prod force # transfer and run remote commands
dostavka deploy --skip-build force
dostavka sync-db # dump only
dostavka sync-db --source prod --target local force
dostavka sync-db --file var/dumps/app-20261003-120000.sql.gz force
dostavka sync-db force --no-hooks
dostavka pull # content or uploads, prod → local
dostavka pull force
dostavka check
dostavka init --type react
dostavka preset kirby
dostavka --help
dostavka deploy --help
--force and the word force do the same thing.
A local build runs during the dry-run too, so the preview matches the files that would ship. It stops before any transfer when a build fails. --skip-build skips that step.
A project file
dostavka init --type symfony writes this. Keys you set replace the preset, except excludes and protect, which are added to the preset lists.
type = "symfony"
default_target = "prod"
[targets.prod]
ssh = "cyon_s129" # alias from ~/.ssh/config, or user@host
dir = "public_html/buchs"
port = 22 # omit this when the alias already has a port
[build]
local = [
"php bin/console tailwind:build --minify",
"php bin/console asset-map:compile --env=prod",
]
[remote]
php = "/opt/alt/php84/usr/bin/php" # substituted as {php}
run = [
"{php} bin/console cache:clear --no-warmup --env=prod",
"{php} bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration",
]
[hooks]
after_import = ["php bin/console app:reset-passwords"]
app:reset-passwords is the app's hasher. dostavka does not write UPDATE users SET password. Set that hook to the command that stores superSecret123. Until the hook is there, an import leaves the production hashes in the local database.
An empty list replaces the preset. A maintenance tree that only copies files:
[remote]
run = []
Per-target fields override the shared build, source, remote commands, and file list. One repo can ship a Vite client and an Express API:
type = "express"
[targets.web]
ssh = "vps"
dir = "/var/www/web"
build = ["npm --prefix client run build"]
source = "client/dist"
remote = []
[targets.api]
ssh = "vps"
dir = "/var/www/api"
source = "server"
remote = ["npm ci --omit=dev", "systemctl restart {service}"]
[remote]
service = "my-api"
dostavka deploy --target web and dostavka deploy --target api then do different work. Two directories with two dostavka.toml files are the other way, and --config points at a file.
Presets
| Type | What it does |
|---|---|
symfony |
Rsync src/, templates/, public/, and the usual project dirs. Exclude var/, vendor/, uploads. Remote cache clear and migrations. MariaDB pull. |
kirby |
Rsync the project. content/, accounts, sessions, cache, media, kirby/, and vendor/ stay on the server. Pulls content/ and site/accounts/. |
react |
npm run build, then rsync dist/. Create React App: set rsync.source to build. |
express |
Rsync the source without node_modules. The server runs npm ci --omit=dev. Add the restart command yourself. |
static |
Rsync only. |
phoenix |
mix deploy --dry-run, or mix deploy with force. |
dostavka preset express prints the file that is actually used.
Add a framework
Copy a preset and edit it. No rebuild.
mkdir -p ~/.config/dostavka/presets
dostavka preset express > ~/.config/dostavka/presets/fastify.toml
A file with the same name as a built-in replaces that built-in. dostavka init --type fastify writes a project file whose defaults come from that preset.
The engine understands six ideas:
build.local— shell commands on this machinersync.source— one directory, such asdist/rsync.pathsandrsync.files— several directories, files without--deletersync.rules— orderedinclude ...andexclude ...patternsremote.run— shell commands on the server, with{php},{service}, and{dir}delegate.deploy— hand the whole deploy to another command
build.local, remote.run, and delegate commands are shell. dostavka.toml is trusted the same way the old bash scripts were. Do not commit secrets in it.
Database pull
sync-db reads DATABASE_URL from remote_env on the server (.env.local for the Symfony preset) and runs mariadb-dump or pg_dump there. The production password stays in that remote script's environment. It is not an ssh argument and it is not written into dostavka.toml.
The dump is stored under dump_dir. Without force, that is the whole command.
force recreates the local database, imports, and runs hooks.after_import. The local URL comes from the environment variable local_url_env, or from local_env in the project. The host must be localhost, 127.0.0.1, or ::1. There is no target that uploads a database.
MariaDB dumps are gzipped SQL. DEFINER clauses and the MariaDB sandbox line are stripped on import. Postgres dumps are pg_dump --format=custom and come back through pg_restore --no-owner.
Set engine = "postgres" on a Symfony-shaped config when the URL is Postgres. The URL scheme is checked against engine.
Kirby has no SQL database. dostavka pull copies content/ and site/accounts/. media/ stays on the server. Kirby rebuilds it.
What the transfer refuses to do
--delete-excluded. macOS openrsync ignores protect filters, and that flag deletes the paths an exclude is keeping. An exclude is the shield..env,.env.local,.git, and the lock directory are always excluded.- A dry-run that mentions a protected path.
content/, uploads, and the Kirby account files abort the deploy before the real rsync. --max-delete. Rsync exits 25 and dostavka stops. Raise the number only after the dry-run looks right.- A second deploy.
forcecreates.dostavka.lockon the server and removes it afterwards. Delete that directory by hand if a deploy dies and leaves it. - An import into a host that is not this machine.
dostavka check opens ssh with BatchMode. A key passphrase has to be in the agent already. dostavka will not prompt.
Layout
presets/ built-in frameworks, also printed by `dostavka preset`
src/ the CLI
dostavka.toml written in each project by `dostavka init`
Search for dostavka.toml starts in the current directory and walks up. --config path skips the walk.