@studiometa/trafic-cli
v0.1.41
Published
CLI to deploy projects to DDEV servers — preview environments from CI
Readme
@studiometa/trafic-cli
CLI to deploy projects to DDEV servers — preview environments from CI.
Part of Trafic, a tool for managing DDEV preview environments on Linux servers.
Installation
npm install -g @studiometa/trafic-cli
# or use directly with npx
npx @studiometa/trafic-cli deploy ...Commands
trafic setup
Setup a new server over SSH. Run it from any machine that can reach the server with SSH — the command bootstraps Node.js, installs @studiometa/trafic-agent on the server, then runs trafic-agent setup there.
trafic setup \
--host server.example.com \
--tld previews.example.com \
--email [email protected]Options:
| Option | Description | Default |
|--------|-------------|---------|
| --host | SSH host (required) | - |
| --tld | TLD for DDEV projects (required) | - |
| --email | Email for Let's Encrypt certificates | - |
| --user | SSH user | root |
| --port | SSH port | 22 |
| --agent-version | Agent version to install | latest |
| --ssh-users | SSH users to allow after hardening, comma-separated. The --user value is always added | ddev |
| --trusted-proxy-hops | Proxies in front of the agent — 2 behind a CDN. Fresh installs only | 1 |
| --no-hardening | Skip server hardening | false |
| --no-root-ssh | Disable root SSH login. Refused when connecting as root | false |
| --no-docker | Skip Docker installation | false |
| --no-ddev | Skip DDEV installation | false |
| --dry-run | Print the remote commands without running them | false |
| --ssh-options | Extra SSH options. Use the = form when the value starts with a dash: --ssh-options="-i key -o IdentitiesOnly=yes" | - |
Requirements:
- Ubuntu 24.04 LTS on the target server (26.04 also verified)
- SSH access as
root, or as a user with passwordless sudo — SSH runs in batch mode, so a password prompt cannot be answered - With
--no-root-ssh, connect as a sudo user: root is dropped fromAllowUsersandPermitRootLoginbecomesno, leaving the provider's rescue mode as the only recovery path - Wildcard DNS (
*.previews.example.com→ server IP)
Recommended: a firewall in front of the server, as an extra layer. setup closes DDEV's tool ports itself with DOCKER-USER rules — UFW cannot, because Docker's rules run before its chains — but a network-level firewall drops the packets before they reach the host at all. On OVH dedicated servers that is the Network Firewall in the control panel; it is stateless, so permit tcp established is required or outbound return traffic breaks, and it is IPv4-only. Note it does not filter traffic originating inside the same provider: measured on OVH, a host inside OVH still reached a denied port. setup does not configure it, since it can neither create nor verify it. See Network exposure.
The command is safe to run again: each step is skipped when the server is already in the target state.
trafic deploy
Deploy a project to a DDEV server.
trafic deploy \
--host server.example.com \
--name my-project \
--sync "dist/" \
--script "composer install --no-dev"Options:
| Option | Description | Default |
|--------|-------------|---------|
| --host | SSH host (required) | - |
| --name | Project name (required) | - |
| --user | SSH user | ddev |
| --port | SSH port | 22 |
| --sync | Comma-separated local paths to sync. Directories and files both work | . |
| --script | Post-sync script to run in DDEV | - |
| --env | Environment for --script, repeatable. KEY=VALUE, or bare KEY to take the runner's value | - |
| --before-script | Script to run before deploy (on server, outside the container) | - |
| --after-script | Script to run after deploy (on server, outside the container) | - |
| --create-script | Script to run only on the deploy that creates the project (on server) | - |
| --timeout | Per-command timeout, e.g. 10m, 90s, 1h | 10m |
| --branch | Git branch name | auto-detected from CI |
| --preview | Preview environment ID (MR/PR number) | - |
| --repo | Repository URL | auto-detected from CI |
| --ssh-options | Extra SSH options. Use the = form when the value starts with a dash: --ssh-options="-i key -o IdentitiesOnly=yes" | - |
trafic destroy
Remove a DDEV project from the server.
trafic destroy \
--host server.example.com \
--name my-project \
--preview 123CI Examples
GitLab CI
SSH_PRIVATE_KEY is a file-type CI variable, so ssh can read the key directly:
deploy_preview:
stage: deploy
image: node:24
before_script:
- chmod 600 "$SSH_PRIVATE_KEY"
script:
- npx @studiometa/trafic-cli deploy
--host $SSH_HOST
--name $CI_PROJECT_PATH_SLUG
--preview $CI_MERGE_REQUEST_IID
--ssh-options="-i $SSH_PRIVATE_KEY -o IdentitiesOnly=yes"
--sync "dist/"
rules:
- if: $CI_MERGE_REQUEST_IDTwo things worth copying exactly:
No ssh-agent. GitLab runs after_script in a separate shell from before_script, so SSH_AGENT_PID is not set there and ssh-agent -k kills nothing. The surviving agent holds the job's output file descriptors, and the runner waits on them — jobs sat running for up to an hour after their script had finished. Pointing ssh at the key avoids the daemon entirely. IdentitiesOnly=yes stops any other key on the runner from being offered first.
--ssh-options= with an equals sign. The value starts with a dash, and --ssh-options "-i …" makes the parser read it as another option: Option '--ssh-options' argument is ambiguous.
GitHub Actions
deploy_preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: webfactory/[email protected]
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- run: npx @studiometa/trafic-cli deploy
--host ${{ vars.SSH_HOST }}
--name ${{ github.event.repository.name }}
--preview ${{ github.event.pull_request.number }}
--sync "dist/"How it works
The setup command executes 5 steps over SSH:
- Check the server — Read the OS, resolve root or sudo privileges
- Install Node.js — Install Node.js 24 from the NodeSource apt repository (skipped when already present)
- Install the agent —
npm install -g @studiometa/trafic-agent - Setup the server — Run
trafic-agent setup(Docker, DDEV, Traefik, systemd, hardening) - Verify — Check that the
trafic-agentservice is active
The deploy command executes 8 steps over SSH:
- Update source code — Clone the repository, or fetch and check out the branch when it is already there. A clone also writes
.ddev/config.local.yamlwith the project name and the server's router ports, read fromddev config global - Start DDEV — Run
ddev startunless the project is already running - Before-script — Optional, on the server, outside the container
- Rsync files — Sync local paths to the server. A directory is mirrored with
--delete, so a file the build stops producing is removed from the server too; a single file is copied as itself. A path that does not exist stops the deploy - Create-script — Optional, on the server, and only on the deploy that created the project. For one-time seeding such as
ddev pull, which overwrites the database and so must not repeat - Run script — Optional, inside the container, with
--envvalues available to it - After-script — Optional, on the server, outside the container
- Verify — Run
ddev describeand return the project URL
Seeding a database once
A preview environment starts with an empty database. --create-script runs on the deploy that created the project and never again, which is what a ddev pull provider needs — it overwrites the database it imports into, so repeating it on every deploy would discard the environment's content:
trafic deploy \
--host server.example.com \
--name my-app \
--preview 42 \
--sync "vendor,dist" \
--create-script "ddev pull prod-db -y" \
--timeout 30mRaise --timeout when the pull is slow: it applies per remote command, and a timeout partway through an import leaves the environment half-built.
Zero dependencies
This package has no runtime dependencies. It uses native Node.js APIs (node:child_process) for SSH and rsync operations.
License
MIT — see LICENSE
