A minimal, opinionated deployment tool for Composer based PHP projects, inspired by Deployer and Capistrano.
- Zero-downtime deployments with atomic releases
- Release management - keeps last N releases with easy rollback
- Deployment locking - prevents concurrent deployments to the same host
- Shared files/directories - persistent data between releases
- Template variables from composer.json
- Pure Go implementation - single binary, no dependencies
- Deny-by-default file selection - explicit allowlist, nothing ships unless you include it
- SSH-based deployment with key authentication
- Colored output - clear, beautiful deployment progress
- TYPO3 optimized - sensible defaults for TYPO3 projects
brew tap ochorocho/shippy https://github.com/ochorocho/shippy
brew trust ochorocho/shippy
brew install shippyTo upgrade later:
brew upgrade shippygit clone https://github.com/ochorocho/shippy.git
cd shippy
go build -o shippy
sudo mv shippy /usr/local/bin/go install github.com/ochorocho/shippy@latest- Initialize configuration in your TYPO3 project root:
shippy initThis will create a .shippy.yaml file with sensible TYPO3 defaults and read your project name from composer.json.
- Edit configuration with your server details:
vim .shippy.yamlUpdate at minimum:
hostname- your server's domain or IPremote_user- SSH usernamessh_key- path to your SSH private keyinclude- the allowlist of paths to deploy (deny-by-default; adjust to your project layout)
- Validate configuration:
shippy config validate- Deploy to production:
shippy deploy productionDeploy from a pipeline. Both examples assume a .shippy.yaml in your repository and an SSH key for the target server provided as a secret/variable (see SSH Authentication).
Install and run shippy with the setup-shippy action:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ochorocho/shippy-action@v0.0.1
with:
args: deploy productionUse the prebuilt Docker image, which ships shippy on PATH:
# .gitlab-ci.yml
deploy:
image: ochorocho/shippy:latest
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- shippy deploy productionhosts:
<hostname>:
# SSH connection
hostname: <server domain or IP>
port: <SSH port, default: 22>
remote_user: <SSH username>
ssh_key: <path to SSH private key>
ssh_options: <map of SSH options, see below>
# Deployment
deploy_path: <absolute path on server>
rsync_src: <local source directory>
keep_releases: <number of releases to keep, default: 5>
# File management
shared: <list of shared paths>
include: <allowlist - paths to deploy (deny-by-default)>
exclude: <carve-outs that win over includes>
commands:
- name: <command description>
run: <command to execute>commands (and rollback_commands) run in the new release directory, in order, before the atomic switchover. Each entry needs a name and a run:
commands:
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Database migrations
run: ./vendor/bin/typo3 upgrade:runScoping a command to specific hosts:
By default a command runs for every host in hosts:. Use only / except (GitLab-CI style) to scope a single command instead of duplicating the whole command list per host:
commands:
- name: Database migrations
run: ./vendor/bin/typo3 upgrade:run
# Only run this command for the listed host(s) (keys under `hosts:`).
# Skipped everywhere else. Omit `only`/`except` entirely to run on every host.
only:
- production
- name: Notify monitoring
run: /usr/local/bin/notify-deploy
# except is the inverse of only: runs everywhere except the listed hosts.
except:
- stagingonlyandexceptaccept a list of host names (the keys underhosts:).- If a host matches both,
exceptwins. - Commands skipped for a host are logged as skipped during
deploy/rollback. shippy config validaterejectsonly/exceptentries that reference a host name not defined underhosts:.shippy config show <host>lists only the commands that actually apply to that host;shippy config validateannotates every command with its resolved scope.
Running commands inside a container (command_context):
If your PHP sources are mounted into a container, command_context runs every command inside a subcontext instead of directly on the remote host. When set, Shippy executes each command as:
<command_context> sh -c 'cd <release-dir> && <run>'
Shippy adds sh -c itself — give only the container-entry prefix, without a trailing shell (docker exec php85, not docker exec php85 bash). The cd happens inside the context, so the release directory must resolve to the same path there (the normal bind-mount case, e.g. /var/www:/var/www).
command_context can be set globally, per host, or per command — the most specific one wins:
# Global default for every host and command (optional; empty = run directly on the host)
command_context: docker exec -u www-data php85
hosts:
production:
hostname: example.com
remote_user: deploy
command_context: docker exec php84 # Overrides the global default for this host
commands:
- name: Notify monitoring
run: /usr/local/bin/notify-deploy
# Per-command override. An empty string forces this command to run
# directly on the host even when a global/per-host context is set.
command_context: ""Precedence: per-command > per-host (hosts.<name>.command_context) > global command_context.
Shippy deploys files using an allowlist: by default nothing is deployed
unless it is listed under include:. A project root usually contains far more
that should not ship (.git/, node_modules/, dumps, IDE files, .env) than
should — an allowlist is safer and less error-prone than trying to exclude every
unwanted path.
Include = the allowlist. List exactly what should ship. A directory entry ships that directory and everything beneath it:
hosts:
production:
hostname: example.com
remote_user: deploy
deploy_path: /var/www/myproject
rsync_src: ./
include:
- "public/" # Web root (ships the whole subtree)
- "vendor/" # Composer dependencies
- "config/" # TYPO3 configuration
- "composer.json"
- "composer.lock"Exclude = carve-outs. Excludes always win over includes, so use them to punch holes in an included directory:
exclude:
- "public/typo3temp/" # Drop generated temp files under an included dir
- "*.log" # Never ship log filesCommon junk — .git/, node_modules/, var/cache/, var/log/, .DS_Store,
Thumbs.db and more (see Default Excludes) — is carved out
automatically; you don't need to list it.
Notes:
include:/exclude:are not merged across hosts — a per-host list replaces the global one entirely. Give each host its own allowlist (or define one at the global level and omit it per host)..gitignoreis not consulted for deployment. You select what ships withinclude:; you don't rely on gitignore to deselect.- Escape hatch:
include: ["*"]ships (almost) everything; the built-in junk list still protects.git/etc.
Pattern Syntax:
- Patterns use gitignore-style syntax
- A single-segment pattern (
vendor/,*.log) matches at any depth - A multi-segment pattern (
public/index.php) is anchored to the project root - Trailing
/means directory only *matches within a path segment;**matches across segments
Earlier versions shipped everything by default and used exclude: (and
.gitignore) to remove unwanted files. Deny-by-default reverses this:
- Add an
include:allowlist to every host (or globally). Without it,shippy deployscans 0 files and warns you. - Existing
exclude:entries still work, now as carve-outs on top of your includes. .gitignoreno longer affects what is deployed — anything you relied on gitignore to exclude is already excluded by default; anything gitignored that you still need (e.g.vendor/) simply goes ininclude:.- Run
shippy config validateand reviewshippy deploy's "Found N files to sync" count before your first real deploy.
SSH Key Detection:
The ssh_key field is optional. If not specified, Shippy will automatically try to find your SSH key in these locations (in order):
~/.ssh/id_ed25519~/.ssh/id_rsa~/.ssh/id_ecdsa
Explicit SSH Key:
hosts:
production:
hostname: example.com
remote_user: deploy
ssh_key: ~/.ssh/id_ed25519 # Optional: specify SSH private keyImportant: Always specify the private key (e.g., id_ed25519), not the public key (e.g., id_ed25519.pub).
SSH Agent:
If ssh-agent is running (SSH_AUTH_SOCK is set) or, on Windows, Pageant is running, Shippy also offers every key the agent holds — in addition to, not instead of, ssh_key. The server tries every offered key during authentication, so both sources are tried automatically; nothing needs to be configured to enable this.
This matters most for passphrase-protected keys: Shippy cannot decrypt a passphrase-protected private key file itself, but if the same key is already loaded in your agent (ssh-add), it's used from there instead, and ssh_key can point at that same encrypted file without issue. With an agent running, ssh_key also becomes fully optional — no default key needs to exist on disk at all.
hosts:
production:
hostname: example.com
remote_user: deploy
# No ssh_key needed - authenticates entirely via ssh-agent.
# Run `ssh-add ~/.ssh/id_ed25519` beforehand so the agent holds the key.You can configure SSH connection behavior using the ssh_options field. These options correspond to SSH configuration options (see man ssh_config):
hosts:
production:
hostname: example.com
port: 2222 # Custom SSH port (default: 22)
remote_user: deploy
# ssh_key is optional - will auto-detect from ~/.ssh/
# Advanced SSH options
ssh_options:
ConnectTimeout: "30" # Connection timeout (default: 30 seconds)
ServerAliveInterval: "60" # Send keepalive every 60 seconds
ServerAliveCountMax: "3" # Disconnect after 3 failed keepalives
Compression: "yes" # Enable SSH compression
StrictHostKeyChecking: "accept-new" # Host key verification mode
UserKnownHostsFile: "~/.ssh/known_hosts" # Known hosts file pathSpecifies the timeout for establishing an SSH connection. Supports multiple formats:
- Integer (seconds):
ConnectTimeout: "30"orConnectTimeout: 30 - Duration string:
ConnectTimeout: "30s",ConnectTimeout: "5m",ConnectTimeout: "1h"
Default: 30 seconds
Examples:
ssh_options:
ConnectTimeout: "10" # 10 seconds
ConnectTimeout: "30s" # 30 seconds
ConnectTimeout: "2m" # 2 minutesKeep SSH connections alive during long-running operations (deployments, database migrations, etc.) by sending periodic keepalive messages.
- ServerAliveInterval: Interval between keepalive messages. Supports same formats as ConnectTimeout.
- ServerAliveCountMax: Number of keepalive messages to send without response before disconnecting (default: 3)
Examples:
ssh_options:
ServerAliveInterval: "60" # Send keepalive every 60 seconds
ServerAliveCountMax: "3" # Disconnect after 3 failed attempts
# Or with duration format:
ServerAliveInterval: "1m" # Send keepalive every minuteUse case: For long-running deployments or commands, set ServerAliveInterval to prevent SSH timeouts:
ssh_options:
ServerAliveInterval: "30" # Keepalive every 30 seconds
ServerAliveCountMax: "5" # Allow up to 5 failed attempts (2.5 min grace)Enable SSH compression to reduce bandwidth usage. Particularly useful for large file transfers over slow connections.
- Values:
"yes","true","no","false"
Example:
ssh_options:
Compression: "yes" # Enable compressionNote: Go's SSH library handles compression negotiation with the server. If the server doesn't support compression, it will be automatically disabled.
Controls host key verification:
"yes"- Strict checking, reject unknown hosts (most secure)"accept-new"- Accept new hosts, verify known hosts (recommended default)"no"- Disable all verification (insecure, not recommended for production)
Example:
ssh_options:
StrictHostKeyChecking: "accept-new" # Accept first connection, verify thereafter
UserKnownHostsFile: "~/.ssh/known_hosts"Path to the known_hosts file for host key verification. Supports tilde expansion (~).
Default: ~/.ssh/known_hosts
Example:
ssh_options:
UserKnownHostsFile: "~/.ssh/my_known_hosts"hosts:
production:
hostname: example.com
port: 22
remote_user: deploy
ssh_key: ~/.ssh/id_ed25519
deploy_path: /var/www/myproject
ssh_options:
# Connection and timeout settings
ConnectTimeout: "30" # 30 second connection timeout
ServerAliveInterval: "60" # Keepalive every 60 seconds
ServerAliveCountMax: "3" # Disconnect after 3 failures
# Performance
Compression: "yes" # Enable compression
# Security
StrictHostKeyChecking: "accept-new"
UserKnownHostsFile: "~/.ssh/known_hosts"Note: The port field is a top-level configuration option for convenience. For other SSH options, use the ssh_options map.
By default, Shippy opens a fresh SSH connection for each remote operation. Set ssh_multiplexing: true to reuse a single shared connection (SSH ControlMaster) for all operations against a host, which noticeably reduces overhead on high-latency links or deployments that run many commands:
hosts:
production:
hostname: example.com
remote_user: deploy
ssh_multiplexing: true # Reuse one connection for all operations (default: false)Use {{key.path}} syntax to reference values from composer.json:
hosts:
production:
deploy_path: /var/www/{{name}} # Uses composer.json "name" fieldAccess nested values:
deploy_path: /var/www/{{extra.typo3/cms.web-dir}}Provide a fallback value with | (used when the key is not found in composer.json):
commands:
- name: Clear cache
run: ./{{config.bin-dir|vendor/bin}}/typo3 cache:flushUse ${VAR} syntax to reference environment variables:
hosts:
production:
hostname: ${DEPLOY_HOST}
remote_user: ${DEPLOY_USER}Provide a fallback value with | (used when the variable is not set):
hosts:
production:
deploy_path: ${DEPLOY_PATH|/var/www/html}If an environment variable is not set and no fallback is provided, deployment will fail with an error.
Files and directories in the shared: list are symlinked from the shared/ directory to each release:
shared:
- .env # Shared file
- var/log/ # Shared directory (note trailing slash)
- public/fileadmin/
- public/uploads/To prevent two deployments from running against the same host at once, Shippy writes a lock file to the remote deploy_path at the start of a deploy and removes it when finished. Locking is enabled by default with a 15-minute timeout, after which a stale lock (e.g. from a crashed deployment) is considered expired and automatically overridden.
# Global defaults (can be overridden per host)
lock_enabled: true # Enable deployment locking (default: true)
lock_timeout: 15 # Minutes before a stale lock expires (default: 15)
hosts:
production:
hostname: example.com
remote_user: deploy
deploy_path: /var/www/myproject
# lock_enabled: false # Per-host override to disable lockingIf a deployment fails and leaves a stale lock behind before the timeout elapses, clear it manually with shippy unlock.
Shippy creates the following structure on the server (following Deployer/Capistrano conventions):
/var/www/myproject/
├── current -> releases/20240109120000 # Symlink to latest release
├── releases/
│ ├── 20240109120000/ # Current release
│ ├── 20240109110000/ # Previous release
│ └── 20240109100000/ # Older release
└── shared/
├── .env # Shared files
├── var/
│ ├── log/
│ └── session/
└── public/
├── fileadmin/
└── uploads/
Create a new configuration file with TYPO3 defaults:
shippy initOptions:
--forceor-f- Overwrite existing configuration file
This command:
- Checks for
composer.jsonin current directory - Reads project name from composer.json
- Generates
.shippy.yamlwith sensible TYPO3 defaults - Protects against accidental overwrites (use
--forceto override)
Deploy to a target host:
shippy deploy <hostname>Options:
--dry-run- Preview which files and commands would be deployed without connecting to the host--verboseor-v- Show detailed output for each file
Example:
shippy deploy staging
shippy deploy production
shippy deploy production --dry-run # Preview files and commands, no connectionWhen run without a host argument, an interactive host selector is shown.
Rollback to a previous release:
shippy rollback <hostname>Options:
--listor-l- List available releases and exit--releaseor-r- Switch to a specific release by name--offsetor-n- Relative offset from current release (negative = older, positive = newer)
Examples:
shippy rollback production # Interactive release selection
shippy rollback production -l # List available releases
shippy rollback production -n -1 # One version back
shippy rollback production -n +1 # One version forward (e.g., after accidental rollback)
shippy rollback production -n -2 # Two versions back
shippy rollback production -r 20260109120000 # Specific release by nameWhen run without flags, shows an interactive list of available releases with deployment date/time, git commit hash, and git tag. The current release is marked and cannot be selected.
Create a ZIP archive containing a database dump and selected files from the remote shared/ directory:
shippy backup <hostname>The output file is named backup-<hostname>-<timestamp>.zip and is written to the configured output: directory (default: current working directory).
Options:
--outputor-o- Output directory for the backup ZIP (overrides the configuredoutput:)--skip-database- Skip the database dump--skip-shared- Skip the shared files--verboseor-v- Show detailed output
Configuration in .shippy.yaml:
backup:
output: ./backups # Local directory for ZIPs (default: cwd)
# Files to download from the remote shared/ directory (paths are relative to shared/)
files:
- .env
- public/fileadmin/
- public/uploads/
database:
# How database credentials are obtained:
# auto - try `typo3 configuration:show` (TYPO3 v14+), then standard .env,
# then TYPO3 .env keys, then TYPO3 settings.php (default)
# dotenv - read DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD, ... from .env
# typo3 - try `typo3 configuration:show` (TYPO3 v14+), then TYPO3-specific
# .env keys or settings.php
# manual - use the explicit driver/host/port/name/user/password fields below
credentials: auto
# Required only when credentials: manual
# driver: mysql # mysql | postgresql | sqlite
# host: 127.0.0.1
# port: 3306
# name: my_database
# user: db_user
# password: ${DB_PASSWORD} # environment-variable substitution is supported
# Exclude tables from the dump (glob patterns)
exclude_tables:
- "cache_*"
- "cf_*"
- "sys_log"
- "be_sessions"
# DBMS-specific options
options:
single_transaction: "true" # MySQL: consistent dump without table locks
# charset: "utf8mb4" # MySQL
# schema: "public" # PostgreSQLWith credentials: auto or typo3, Shippy first runs ./vendor/bin/typo3 configuration:show DB/Connections/Default (available in TYPO3 v14+) to read the authoritative active database configuration. If the command is unavailable — older TYPO3, a non-bootstrappable app, or a non-TYPO3 project — it falls back to parsing .env keys and config/system/settings.php / legacy typo3conf/LocalConfiguration.php.
Per-host overrides are supported by adding a backup: block inside a hosts.<name>: entry — useful when staging and production need different exclude tables or output directories.
Upload any file (typically a backup ZIP) to the GitLab Generic Packages registry of the project's git origin:
shippy gitlab:upload <file>The GitLab host and project path are auto-detected from the origin remote URL — no YAML config required. Authentication resolves in this order:
--token <token>flagGITLAB_TOKENenvironment variableCI_JOB_TOKENenvironment variable (set automatically inside GitLab CI jobs)
Options:
--token/-t— GitLab token--package-name— package name (default: project name from the git remote)--package-version— package version (default: timestamp, e.g.,20260501T102420)
Typical local chain — back up production, then upload the archive:
shippy backup production
shippy gitlab:upload backups/backup-production-20260501T102420.zip --token "$GITLAB_TOKEN"Run as a scheduled GitLab CI pipeline (uses CI_JOB_TOKEN automatically):
# .gitlab-ci.yml
nightly_backup:
stage: backup
image: ghcr.io/ochorocho/shippy:latest
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
script:
- shippy backup production
- shippy gitlab:upload backups/backup-production-*.zipUploaded archives appear in Project → Deploy → Package Registry, grouped by <package-name>/<package-version>.
Check if your configuration is valid:
shippy config validateThis command:
- Validates YAML syntax
- Checks required fields
- Tests composer.json template variables
- Shows processed configuration
Print the complete resolved configuration with all defaults applied and template variables replaced:
shippy config show # Complete config with resolved templates
shippy config show production # Effective config for a single host (globals + per-host overrides)
shippy config show --raw # Raw config without resolving template variablesFor a single host, the output lists only the commands that actually apply to that host (given each command's only/except filters); skipped commands are shown as comments.
Force-remove a stale deployment lock from a host (see Deployment Locking):
shippy unlock <hostname>Example:
shippy unlock # Interactive host selection
shippy unlock productionUse this only when a deployment failed and left a lock behind. Running it while a deployment is genuinely in progress may cause issues. If no active lock exists, the command reports that and exits without changes.
Print all environment variables available to Shippy. Useful for debugging configuration that uses ${ENV_VAR} substitution:
shippy env
shippy env | grep DEPLOYPrint the version, git commit, build date, and Go version:
shippy version
shippy --version # or -v--config <path> selects a different configuration file (default: .shippy.yaml). It is available on every command:
shippy --config .shippy.staging.yaml deploy stagingWhen you run shippy deploy <host>, the following steps occur:
- Scan files - Walks source directory, applies the deny-by-default allowlist (include/exclude patterns)
- Connect to server - Establishes SSH connection
- Create release - Creates new timestamped release directory (e.g.,
releases/20260109203841) - Sync files - Transfers files to the new release directory
- Create symlinks - Links shared files/directories from
shared/to the release - Execute commands - Runs commands in the new release directory (e.g., cache flush, migrations)
- Activate release - Atomically updates
currentsymlink to new release (site goes live) - Cleanup - Removes old releases, keeps last N
Important: Commands execute in the new release directory before it goes live. This ensures all preparation (cache warming, migrations, etc.) completes successfully before the atomic switchover. The site only becomes live when the current symlink is updated in step 7.
hosts:
production:
hostname: www.example.com
remote_user: deploy
deploy_path: /var/www/{{name}}
rsync_src: ./
ssh_key: ~/.ssh/id_rsa
# Deny-by-default: list exactly what should ship
include:
- public/
- vendor/
- config/
- composer.json
- composer.lock
shared:
- .env
- var/log/
- var/session/
- public/fileadmin/
- public/uploads/
commands:
- name: Clear TYPO3 cache
run: ./vendor/bin/typo3 cache:flush
- name: Run extension setup
run: ./vendor/bin/typo3 extension:setupUnder deny-by-default you must explicitly list every path a Composer-based TYPO3 installation needs to run. The block below is the complete allowlist for a standard project, with each entry annotated. Copy it and delete the optional lines that don't apply to your project.
hosts:
production:
hostname: www.example.com
remote_user: deploy
deploy_path: /var/www/{{name}}
rsync_src: ./
include:
# --- Required: a Composer TYPO3 install will not boot without these ---
- public/ # Web root: index.php, typo3/, _assets/, installed extensions' Resources/Public
- vendor/ # All Composer dependencies incl. typo3/cms-core and vendor/bin/typo3
# (the server does NOT run "composer install")
- config/ # Site config (config/sites/*/config.yaml) + system config (config/system/*.php)
- composer.json # TYPO3 reads it for package metadata / extension autoloading
- composer.lock # Pins the installed set; used by post-deploy commands
# --- Optional: uncomment the ones your project actually uses ---
# - packages/ # Local site extensions kept in the repo (monorepo layout)
# - .htaccess # Root .htaccess, if you serve from the project root
# - api/ # Additional entry points / sub-apps outside public/
# Carve-outs: excludes always win over includes. Common junk (.git/,
# node_modules/, var/cache/, var/log/, .DS_Store, ...) is already excluded
# automatically, so you only need project-specific holes here.
exclude:
- public/typo3temp/ # Generated at runtime, never ship it
# Runtime/persistent data — symlinked from shared/, never part of a release
shared:
- .env
- var/log/
- var/session/
- public/fileadmin/
- public/uploads/
commands:
- name: Run extension setup
run: ./vendor/bin/typo3 extension:setup
- name: Run upgrade wizards
run: ./vendor/bin/typo3 upgrade:run
- name: Flush caches
run: ./vendor/bin/typo3 cache:flushNote: If your project uses a non-standard web directory (configured via the
extra.typo3/cms.web-dirkey incomposer.json, e.g.web/instead ofpublic/), include that directory instead ofpublic/. Runshippy deploy <host> --dry-runto preview exactly which files the allowlist resolves to before deploying.
hosts:
staging:
hostname: staging.example.com
remote_user: deploy
deploy_path: /var/www/{{name}}/staging
rsync_src: ./
ssh_key: ~/.ssh/id_rsa
keep_releases: 3
# Allowlist: exactly what ships (deny-by-default)
include:
- public/
- vendor/
- config/
- composer.json
- composer.lock
# Carve-outs (win over includes). Common junk is already excluded.
exclude:
- public/typo3temp/
- Tests/
# Shared paths
shared:
- .env
- var/log/
- var/session/
- public/fileadmin/
- public/uploads/
production:
hostname: www.example.com
remote_user: deploy
deploy_path: /var/www/{{name}}/production
rsync_src: ./
ssh_key: ~/.ssh/id_rsa_production
keep_releases: 10
include:
- public/
- vendor/
- config/
- composer.json
- composer.lock
shared:
- .env
- var/log/
- var/session/
- public/fileadmin/
- public/uploads/
commands:
- name: Clear TYPO3 cache
run: ./{{config.bin-dir|vendor/bin}}/typo3 cache:flush
- name: Run extension setup
run: ./{{config.bin-dir|vendor/bin}}/typo3 extension:setup
- name: Database migrations
run: ./{{config.bin-dir|vendor/bin}}/typo3 upgrade:run
- name: Warmup caches
run: ./{{config.bin-dir|vendor/bin}}/typo3 cache:warmup
rollback_commands:
- name: Flush caches
run: ./{{config.bin-dir|vendor/bin}}/typo3 cache:flush
- name: Warmup caches
run: ./{{config.bin-dir|vendor/bin}}/typo3 cache:warmupThese carve-out patterns are always applied and win over your include:
allowlist, so common junk never ships even inside an included directory:
.git/.gitignore.shippy.yaml.shippy.yaml.examplenode_modules/.env.local.env.*.localvar/cache/var/log/var/transient/.DS_StoreThumbs.db
- Go 1.20 or higher (for building)
- SSH access to target server
- SSH key authentication configured
shippy/
├── cmd/
│ ├── root.go # Root CLI command
│ ├── config.go # Config validation command
│ └── deploy.go # Deploy command
├── internal/
│ ├── config/
│ │ ├── config.go # Configuration parser
│ │ └── template.go # Template variable processor
│ ├── composer/
│ │ └── parser.go # Composer.json parser
│ ├── rsync/
│ │ ├── sync.go # Allowlist file scanner (deny-by-default)
│ │ └── transfer.go # File transfer over SSH
│ ├── ssh/
│ │ ├── client.go # SSH client
│ │ └── executor.go # Command executor
│ └── deploy/
│ ├── deployer.go # Main deployment orchestrator
│ └── release.go # Release management
├── Formula/
│ └── shippy.rb # Homebrew formula (prebuilt binary, per arch)
├── scripts/
│ └── update-formula.sh # Bumps formula version + per-arch sha256 for a tag
├── main.go
├── go.mod
└── README.md
Maintainers: after tagging a release, run
make brew-formulato bumpFormula/shippy.rbto the latest tag (the Release workflow does this automatically on tagged builds).
go 1.24: go build -o shippy
Test instance
cd tests/
docker compose upcd tests/typo3/
composer installDeploy
../../shippy deploy productionTest SSH Connection
ssh -i tests/ssh_keys/shippy_key root@127.0.0.1 -p 2424Contributions are welcome! Please feel free to submit a Pull Request.