<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="https://automate-deployer.com/xsl/atom.xsl" media="all"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <id>https://automate-deployer.com/docs/</id>
  <title>Automate - Documentation</title>
  <subtitle><![CDATA[Automate is a deployment tool for Linux servers: describe your platforms in a YAML file, deploy with one command from your workstation or your CI.]]></subtitle>
  <link href="https://automate-deployer.com/docs/atom.xml" rel="self" type="application/atom+xml" />
  <link href="https://automate-deployer.com/docs/" rel="alternate" type="text/html" />
  <updated>2026-10-30T11:10:35+00:00</updated>
  <author>
    <name>Julien Jacottet</name>
    <uri>https://github.com/automatephp/automate</uri>
  </author>
  <entry xml:lang="en">
    <id>https://automate-deployer.com/docs/ci-integration/</id>
    <title>CI integration</title>
    <content type="html">
      <![CDATA[<p>Automate runs anywhere a shell runs. In a pipeline, three things change compared to your workstation:</p>
<ol>
<li><strong>Install a pinned version</strong> of the binary at the start of the job, so that a new release never changes your pipeline unexpectedly.</li>
<li><strong>Provide secrets through the environment.</strong> There is no terminal to answer a prompt: map your CI secrets to <code>AUTOMATE__&lt;name&gt;</code> variables, or pass them with <code>--var</code>. See <a href="/docs/variables/">Variables</a>.</li>
<li><strong>Use <code>--no-interaction</code></strong> so that any missing value fails the job instead of hanging on a prompt.</li>
</ol>
<p>Private keys stored as secrets must be written to a file, since <code>ssh_key</code> expects a path.</p>
<h2 id="github-actions">GitHub Actions</h2>
<p>This workflow deploys <code>main</code> to <code>staging</code> on every push, and tags to <code>production</code>.</p>
<pre><code class="language-yaml"># .github/workflows/deploy.yml
name: Deploy

on:
    push:
        branches: [main]
        tags: ['*']

concurrency: deploy-${{ github.ref_name }}

jobs:
    deploy:
        runs-on: ubuntu-latest
        env:
            AUTOMATE_VERSION: 4.2.2
        steps:
            - uses: actions/checkout@v4

            - name: Install Automate
              run: |
                  curl -LSs -o /usr/local/bin/automate \
                      "https://github.com/automatephp/automate/releases/download/${AUTOMATE_VERSION}/automate-linux-amd64"
                  chmod +x /usr/local/bin/automate
                  automate --version

            - name: Write the SSH key
              run: |
                  install -m 600 /dev/null /tmp/deploy_key
                  echo "${{ secrets.DEPLOY_SSH_KEY }}" &gt; /tmp/deploy_key

            - name: Deploy to staging
              if: github.ref == 'refs/heads/main'
              run: automate deploy staging --no-interaction

            - name: Deploy to production
              if: startsWith(github.ref, 'refs/tags/')
              run: automate deploy production "${{ github.ref_name }}" --no-interaction
              env:
                  AUTOMATE__prod_password: ${{ secrets.PROD_PASSWORD }}</code></pre>
<p>With the matching configuration:</p>
<pre><code class="language-yaml">platforms:
    staging:
        default_branch: main
        servers:
            staging-01:
                host: staging.acme.io
                user: deploy
                ssh_key: /tmp/deploy_key
                path: /var/www/shop
    production:
        default_branch: main
        servers:
            web-01:
                host: web-01.acme.io
                user: deploy
                password: "%prod_password%"
                path: /var/www/shop</code></pre>
<blockquote>
<p>The <code>concurrency</code> key makes GitHub queue the deployments of the same reference. Automate also protects the servers with its own lock file, so two overlapping jobs cannot corrupt a release.</p>
</blockquote>
<h3 id="building-in-ci-and-uploading">Building in CI and uploading</h3>
<p>When the servers should not build the application, build it in the job and upload the result. Leave <code>repository</code> unset, or keep it and upload only the generated files (see <a href="/docs/deployment-strategies/">Deployment strategies</a>).</p>
<pre><code class="language-yaml">            - uses: actions/setup-node@v4
              with:
                  node-version: 22
                  cache: npm
            - run: npm ci &amp;&amp; npm run build

            - name: Deploy
              run: automate deploy production --no-interaction</code></pre>
<pre><code class="language-yaml"># .automate.yml
repository: git@github.com:acme/shop.git

pre_deploy:
    - upload: public/build</code></pre>
<h2 id="gitlab-ci">GitLab CI</h2>
<pre><code class="language-yaml"># .gitlab-ci.yml
stages: [build, deploy]

variables:
    AUTOMATE_VERSION: "4.2.2"

.automate:
    image: ubuntu:24.04
    before_script:
        - apt-get update -qq &amp;&amp; apt-get install -y -qq curl ca-certificates
        - curl -LSs -o /usr/local/bin/automate "https://github.com/automatephp/automate/releases/download/${AUTOMATE_VERSION}/automate-linux-amd64"
        - chmod +x /usr/local/bin/automate
        - install -m 600 /dev/null /tmp/deploy_key
        - echo "$DEPLOY_SSH_KEY" &gt; /tmp/deploy_key

deploy:staging:
    extends: .automate
    stage: deploy
    script:
        - automate deploy staging --no-interaction
    rules:
        - if: $CI_COMMIT_BRANCH == "main"
    environment:
        name: staging
        url: https://staging.acme.io

deploy:production:
    extends: .automate
    stage: deploy
    script:
        - automate deploy production "$CI_COMMIT_TAG" --no-interaction
    rules:
        - if: $CI_COMMIT_TAG
    environment:
        name: production
        url: https://www.acme.io
    when: manual</code></pre>
<p>Declare <code>DEPLOY_SSH_KEY</code> and the <code>AUTOMATE__*</code> passwords as <strong>masked</strong> CI/CD variables in the project settings. A variable named <code>AUTOMATE__prod_password</code> is picked up directly, with no extra mapping.</p>
<h3 id="review-environments">Review environments</h3>
<p>Combine <a href="/docs/variables/">variables</a> with <code>automate remove</code> to create one environment per branch and tear it down when the branch is closed:</p>
<pre><code class="language-yaml"># .automate.yml
platforms:
    review:
        default_branch: main
        servers:
            vm:
                host: review.acme.io
                user: deploy
                ssh_key: /tmp/deploy_key
                path: /srv/review/%slug%</code></pre>
<pre><code class="language-yaml">deploy:review:
    extends: .automate
    stage: deploy
    script:
        - automate deploy review "$CI_COMMIT_REF_NAME" --var slug="$CI_COMMIT_REF_SLUG" --no-interaction
    environment:
        name: review/$CI_COMMIT_REF_SLUG
        url: https://$CI_COMMIT_REF_SLUG.review.acme.io
        on_stop: stop:review
    rules:
        - if: $CI_MERGE_REQUEST_ID

stop:review:
    extends: .automate
    stage: deploy
    script:
        - automate remove review --var slug="$CI_COMMIT_REF_SLUG" --yes --no-interaction
    environment:
        name: review/$CI_COMMIT_REF_SLUG
        action: stop
    rules:
        - if: $CI_MERGE_REQUEST_ID
          when: manual</code></pre>
<h2 id="other-runners">Other runners</h2>
<p>Any runner works the same way: download the binary (or the PHAR with PHP 8.4+), export the <code>AUTOMATE__*</code> variables, run <code>automate deploy</code>. On macOS runners, use the <code>automate-darwin-arm64</code> binary.</p>]]>
    </content>
    <link href="https://automate-deployer.com/docs/ci-integration/" rel="alternate" type="text/html" />
    <published>2026-10-30T11:10:35+00:00</published>
    <updated>2026-10-30T11:10:35+00:00</updated>
  </entry>
  <entry xml:lang="en">
    <id>https://automate-deployer.com/docs/commands/</id>
    <title>Commands</title>
    <content type="html">
      <![CDATA[<pre><code class="language-text">automate &lt;command&gt; [arguments] [options]</code></pre>
<table>
<thead>
<tr>
<th>Command</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="#deploy"><code>deploy</code></a></td>
<td>Deploy a platform</td>
</tr>
<tr>
<td><a href="#rollback"><code>rollback</code></a></td>
<td>Point <code>current</code> back to a previous release</td>
</tr>
<tr>
<td><a href="#check"><code>check</code></a></td>
<td>Verify the connection to the servers and the access to the repository</td>
</tr>
<tr>
<td><a href="#exec"><code>exec</code></a></td>
<td>Run a command on one server</td>
</tr>
<tr>
<td><a href="#remove"><code>remove</code></a></td>
<td>Remove the project from the servers of a platform</td>
</tr>
</tbody>
</table>
<h2 id="common-options">Common options</h2>
<p>These options are accepted by every command:</p>
<table>
<thead>
<tr>
<th>Option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>-c, --config &lt;file&gt;</code></td>
<td>Path of the configuration file. Default: <code>.automate.yml</code> in the current directory</td>
</tr>
<tr>
<td><code>--var &lt;name&gt;=&lt;value&gt;</code></td>
<td>Value of a <a href="/docs/variables/">variable</a>. Repeat the option for several variables</td>
</tr>
<tr>
<td><code>-n, --no-interaction</code></td>
<td>Never ask a question. Use it in CI; a password that cannot be resolved then fails instead of prompting</td>
</tr>
<tr>
<td><code>-v, -vv, -vvv</code></td>
<td>Increase the verbosity of the output</td>
</tr>
<tr>
<td><code>-h, --help</code></td>
<td>Help of the command</td>
</tr>
<tr>
<td><code>-V, --version</code></td>
<td>Version of Automate</td>
</tr>
</tbody>
</table>
<h2 id="deploy">deploy</h2>
<pre><code class="language-bash">automate deploy &lt;platform&gt; [&lt;gitRef&gt;] [--force] [-c &lt;file&gt;] [--var name=value]</code></pre>
<p>Deploys the platform on all its servers. See the <a href="/docs/deployment-lifecycle/">Deployment lifecycle</a>.</p>
<table>
<thead>
<tr>
<th>Argument or option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>platform</code></td>
<td>Name of the platform, as declared in <code>platforms</code></td>
</tr>
<tr>
<td><code>gitRef</code></td>
<td>Branch, tag or commit to deploy. Default: the <code>default_branch</code> of the platform. Ignored when <code>repository</code> is not set</td>
</tr>
<tr>
<td><code>-f, --force</code></td>
<td>Deploy even if an <code>automate.lock</code> file exists on a server</td>
</tr>
</tbody>
</table>
<pre><code class="language-bash">automate deploy production
automate deploy production v1.4.0
automate deploy staging feature/checkout -c deploy/.automate.yml
automate deploy review my-branch --var slug=my-branch</code></pre>
<p>The command prints a summary (repository, platform, servers, version, variable names), then each step with the output of the commands prefixed by the server name. It exits with a non-zero status when the deployment fails.</p>
<h2 id="rollback">rollback</h2>
<pre><code class="language-bash">automate rollback &lt;platform&gt; [&lt;release&gt;] [--yes] [--force] [-c &lt;file&gt;] [--var name=value]</code></pre>
<p>Points <code>current</code> back to a previous release on every server of the platform, without cloning or building anything. The command shows the servers and the targeted release, then asks for confirmation. See <a href="/docs/deployment-lifecycle/#rolling-back">Rolling back</a>.</p>
<table>
<thead>
<tr>
<th>Argument or option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>platform</code></td>
<td>Name of the platform</td>
</tr>
<tr>
<td><code>release</code></td>
<td>Id of the release to go back to, for example <code>2026.10.22-1805.341</code>. Default: the release that precedes the <code>current</code> one on each server</td>
</tr>
<tr>
<td><code>-y, --yes</code></td>
<td>Do not ask for confirmation. Required when the command runs without a terminal</td>
</tr>
<tr>
<td><code>-f, --force</code></td>
<td>Ignore an existing <code>automate.lock</code> file</td>
</tr>
</tbody>
</table>
<pre><code class="language-bash">automate rollback production
automate rollback production 2026.10.22-1805.341
automate rollback production --yes</code></pre>
<p>On each server, Automate switches <code>current</code> to the target release, runs the <a href="/docs/hooks/#rollback-hook"><code>post_rollback</code> hooks</a> in it, then moves the release that was left to <code>releases/failed</code>, where it stays for inspection until the next deployment.</p>
<ul>
<li>Nothing is changed unless every server has a valid target: <code>current</code> must point to one of the listed releases, the target must exist on the server, and without an explicit <code>release</code> all the servers must agree on the previous one. Otherwise the command stops and asks for an explicit release.</li>
<li>If a <code>post_rollback</code> command fails, <code>current</code> has already been switched, the release that was left stays in <code>releases/</code>, and the command exits with a non-zero status.</li>
<li>Since the release that was left is no longer listed, a second <code>rollback</code> goes one release further back.</li>
</ul>
<h2 id="check">check</h2>
<pre><code class="language-bash">automate check &lt;platform&gt; [-c &lt;file&gt;] [--var name=value]</code></pre>
<p>Connects to every server of the platform and runs <code>git ls-remote &lt;repository&gt;</code> from there. Use it after writing a configuration, or to diagnose a failing deployment: a wrong password, an unreachable host or a missing deploy key are reported without touching the server.</p>
<pre><code class="language-bash">automate check production</code></pre>
<h2 id="數散">exec</h2>
<pre><code class="language-bash">automate exec &lt;platform&gt; &lt;server&gt; "&lt;command&gt;" [--current] [-c &lt;file&gt;] [--var name=value]</code></pre>
<p>Runs a single command on one server of the platform and prints its output.</p>
<table>
<thead>
<tr>
<th>Argument or option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>platform</code></td>
<td>Name of the platform</td>
</tr>
<tr>
<td><code>server</code></td>
<td>Name of the server, as declared under <code>servers</code></td>
</tr>
<tr>
<td><code>command</code></td>
<td>The shell command, quoted</td>
</tr>
<tr>
<td><code>--current</code></td>
<td>Run the command in the <code>current</code> release, with the same <a href="/docs/hooks/#hook-environment">environment variables</a> as the hooks</td>
</tr>
</tbody>
</table>
<p>Without <code>--current</code>, the command runs as is, from the home directory of the SSH user:</p>
<pre><code class="language-bash">automate exec production web-01 "df -h /var/www"
automate exec production web-01 "ls -la /var/www/shop/releases"</code></pre>
<p>With <code>--current</code>, it runs in the published release, which is convenient to replay a hook or to run a console command:</p>
<pre><code class="language-bash">automate exec production web-01 "php bin/console cache:clear" --current
automate exec production web-01 'echo $AUTOMATE_RELEASE_ID' --current</code></pre>
<h2 id="remove">remove</h2>
<pre><code class="language-bash">automate remove &lt;platform&gt; [--yes] [--force] [--keep-shared] [-c &lt;file&gt;] [--var name=value]</code></pre>
<p>Removes the project from every server of the platform, for example to clean up a review environment. The command shows what will be removed and asks for confirmation.</p>
<table>
<thead>
<tr>
<th>Option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>-y, --yes</code></td>
<td>Do not ask for confirmation. Required when the command runs without a terminal</td>
</tr>
<tr>
<td><code>-f, --force</code></td>
<td>Ignore an existing <code>automate.lock</code> file</td>
</tr>
<tr>
<td><code>--keep-shared</code></td>
<td>Keep the <code>shared</code> folder. The project directory is then kept too</td>
</tr>
</tbody>
</table>
<p>On each server, Automate runs the <a href="/docs/hooks/#removal-hook"><code>on_remove</code> hooks</a> in the <code>current</code> release, then deletes the <code>releases</code> folder, the <code>current</code> link and the <code>shared</code> folder, and finally the project <code>path</code> itself when nothing else is left in it.</p>
<pre><code class="language-bash">automate remove review --var slug=my-branch --yes</code></pre>
<ul>
<li>A <code>shared_path</code> located outside of the project path is never deleted, since other projects may use it.</li>
<li>If an <code>on_remove</code> command fails, nothing is deleted on any server.</li>
<li>The command can be run again safely: when the project path does not exist, there is nothing to remove.</li>
</ul>]]>
    </content>
    <link href="https://automate-deployer.com/docs/commands/" rel="alternate" type="text/html" />
    <published>2026-10-30T11:10:35+00:00</published>
    <updated>2026-10-30T11:10:35+00:00</updated>
  </entry>
  <entry xml:lang="en">
    <id>https://automate-deployer.com/docs/configuration/</id>
    <title>Configuration</title>
    <content type="html">
      <![CDATA[<p>Automate reads its configuration from <code>.automate.yml</code> in the current directory. Use the <code>-c</code> option of any command to point to another file.</p>
<h2 id="complete-example">Complete example</h2>
<pre><code class="language-yaml">repository: git@github.com:acme/shop.git

platforms:
    staging:
        default_branch: develop
        max_releases: 3
        servers:
            staging-01:
                host: staging.acme.io
                user: deploy
                password: "%staging_password%"
                path: /var/www/shop
                port: 2222
    production:
        default_branch: main
        max_releases: 5
        servers:
            web-01:
                host: web-01.acme.io
                user: deploy
                ssh_key: /home/me/.ssh/id_ed25519
                password: "%key_passphrase%"
                path: /var/www/shop
            web-02:
                host: web-02.acme.io
                user: deploy
                ssh_key: /home/me/.ssh/id_ed25519
                path: /var/www/shop
                shared_path: /mnt/nfs/shop-shared

shared_files:
    - .env.local

shared_folders:
    - var/uploads
    - var/log

pre_deploy:
    - upload: public/build

on_first_deploy:
    - 'cp .env.dist "$AUTOMATE_SHARED_PATH/.env.local"'

on_deploy:
    - composer install --no-dev --optimize-autoloader
    - php bin/console cache:warmup

post_deploy:
    - cmd: php bin/console doctrine:migrations:migrate -n
      only: [web-01]
    - sudo systemctl restart shop-worker

on_remove:
    - sudo systemctl stop shop-worker

post_rollback:
    - sudo systemctl restart shop-worker</code></pre>
<h2 id="root-options">Root options</h2>
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>repository</code></td>
<td>string</td>
<td>none</td>
<td>Git URL cloned on the servers. Omit it to fill the release with <code>upload</code> actions instead. See <a href="/docs/deployment-strategies/">Deployment strategies</a></td>
</tr>
<tr>
<td><code>platforms</code></td>
<td>map</td>
<td><strong>required</strong></td>
<td>The platforms, keyed by name. At least one is required</td>
</tr>
<tr>
<td><code>shared_files</code></td>
<td>list</td>
<td><code>[]</code></td>
<td>Files kept across releases, relative to the release root</td>
</tr>
<tr>
<td><code>shared_folders</code></td>
<td>list</td>
<td><code>[]</code></td>
<td>Folders kept across releases, relative to the release root</td>
</tr>
<tr>
<td><code>pre_deploy</code></td>
<td>list</td>
<td><code>[]</code></td>
<td>Actions run after the clone, before the shared items are set up</td>
</tr>
<tr>
<td><code>on_first_deploy</code></td>
<td>list</td>
<td><code>[]</code></td>
<td>Actions run only on servers that have no release yet</td>
</tr>
<tr>
<td><code>on_deploy</code></td>
<td>list</td>
<td><code>[]</code></td>
<td>Actions run before the release is published</td>
</tr>
<tr>
<td><code>post_deploy</code></td>
<td>list</td>
<td><code>[]</code></td>
<td>Actions run after the release is published</td>
</tr>
<tr>
<td><code>on_remove</code></td>
<td>list</td>
<td><code>[]</code></td>
<td>Commands run by <code>automate remove</code> before anything is deleted</td>
</tr>
<tr>
<td><code>post_rollback</code></td>
<td>list</td>
<td><code>[]</code></td>
<td>Commands run by <code>automate rollback</code> after <code>current</code> is switched back</td>
</tr>
</tbody>
</table>
<p>Hooks are described in detail on the <a href="/docs/hooks/">Hooks</a> page.</p>
<h3 id="repository">repository</h3>
<pre><code class="language-yaml">repository: git@github.com:acme/shop.git      # SSH
repository: https://github.com/acme/shop.git  # HTTPS, for a public repository</code></pre>
<p>The clone runs <strong>on the server</strong>, so the server needs access to the repository, for example with a deploy key. The clone is recursive: submodules are included.</p>
<h3 id="shared-files-and-shared-folders">shared_files and shared_folders</h3>
<pre><code class="language-yaml">shared_files:
    - .env.local
    - config/jwt/private.pem

shared_folders:
    - var/uploads
    - var/sessions</code></pre>
<p>Paths are relative to the release root. On every deployment, each entry is replaced in the release by a symbolic link to the same path under <code>shared/</code>. The first deployment moves the file or folder from the release to <code>shared/</code> when it exists there; otherwise an empty file or folder is created. Intermediate directories are created as needed.</p>
<h2 id="platforms">Platforms</h2>
<p>A platform is a named group of servers deployed together, such as <code>staging</code> or <code>production</code>.</p>
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>default_branch</code></td>
<td>string</td>
<td><strong>required</strong></td>
<td>The branch deployed when <code>deploy</code> is called without a reference</td>
</tr>
<tr>
<td><code>max_releases</code></td>
<td>integer</td>
<td><code>5</code></td>
<td>Number of releases kept on each server. Older ones are removed at the end of a deployment</td>
</tr>
<tr>
<td><code>servers</code></td>
<td>map</td>
<td><strong>required</strong></td>
<td>The servers, keyed by name. At least one is required</td>
</tr>
</tbody>
</table>
<pre><code class="language-yaml">platforms:
    production:
        default_branch: main
        max_releases: 5
        servers:
            web-01: { host: web-01.acme.io, user: deploy, ssh_key: /home/me/.ssh/id_ed25519, path: /var/www/shop }
            web-02: { host: web-02.acme.io, user: deploy, ssh_key: /home/me/.ssh/id_ed25519, path: /var/www/shop }</code></pre>
<p>Server names are used by the <code>only</code> option of actions, by the <code>exec</code> command and in the console output. Keep them short and unique within the platform.</p>
<h2 id="servers">Servers</h2>
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>transport</code></td>
<td><code>ssh</code> or <code>local</code></td>
<td><code>ssh</code></td>
<td>How the server is reached. See <a href="#local-transport">Local transport</a></td>
</tr>
<tr>
<td><code>host</code></td>
<td>string</td>
<td>required with <code>ssh</code></td>
<td>Host name or IP address</td>
</tr>
<tr>
<td><code>user</code></td>
<td>string</td>
<td>required with <code>ssh</code></td>
<td>SSH user</td>
</tr>
<tr>
<td><code>port</code></td>
<td>integer</td>
<td><code>22</code></td>
<td>SSH port</td>
</tr>
<tr>
<td><code>password</code></td>
<td>string</td>
<td>none</td>
<td>SSH password, or the passphrase of <code>ssh_key</code> when it is set. Usually a <a href="/docs/variables/">variable</a></td>
</tr>
<tr>
<td><code>ssh_key</code></td>
<td>string</td>
<td>none</td>
<td>Path to a private key file on the machine that runs Automate</td>
</tr>
<tr>
<td><code>path</code></td>
<td>string</td>
<td><strong>required</strong></td>
<td>Directory of the project on the server. It must exist and be writable by the user</td>
</tr>
<tr>
<td><code>shared_path</code></td>
<td>string</td>
<td><code>&lt;path&gt;/shared</code></td>
<td>Location of the shared files and folders, for example on a network mount</td>
</tr>
</tbody>
</table>
<h3 id="authentication">Authentication</h3>
<p>Automate authenticates with a <strong>password</strong> or with a <strong>private key</strong>:</p>
<pre><code class="language-yaml">servers:
    with-password:
        host: web-01.acme.io
        user: deploy
        password: "%prod_password%"
        path: /var/www/shop
    with-key:
        host: web-02.acme.io
        user: deploy
        ssh_key: /home/me/.ssh/id_ed25519
        password: "%key_passphrase%"   # only if the key is encrypted
        path: /var/www/shop</code></pre>
<p>Automate reads the key file itself; the SSH agent and <code>~/.ssh/config</code> are not used. Give the absolute path of the key. In CI, write the key from a secret to a file before running Automate (see <a href="/docs/ci-integration/">CI integration</a>).</p>
<p>A <code>password</code> written as <code>%name%</code> is a variable: Automate reads it from <code>--var name=...</code>, then from the <code>AUTOMATE__name</code> environment variable, and finally prompts for it in the terminal. See <a href="/docs/variables/">Variables</a>.</p>
<h3 id="local-transport">Local transport</h3>
<p>With <code>transport: local</code>, the server is the machine that runs Automate. No SSH connection is made: commands run directly through <code>/bin/sh</code>, and uploads are copied on the file system. Use it to deploy on the host itself, for example from a CI runner that sits on the target machine, or to try Automate without a remote server.</p>
<pre><code class="language-yaml">platforms:
    production:
        default_branch: main
        servers:
            self:
                transport: local
                path: /var/www/shop</code></pre>
<p>Commands run as the user who launches Automate and inherit its environment. The <code>host</code>, <code>user</code>, <code>password</code> and <code>ssh_key</code> options are rejected with the <code>local</code> transport, since Automate cannot switch user: run Automate as the user who must own the releases.</p>
<p>All the commands work with a local server: <code>deploy</code>, <code>rollback</code>, <code>check</code>, <code>remove</code> and <code>exec --current</code>. The local transport needs a POSIX system (Linux, macOS); it does not work when Automate itself runs on Windows.</p>
<h2 id="validation">Validation</h2>
<p>The configuration is validated when a command starts. Unknown options, a missing <code>path</code> or a platform without servers stop the command with a message naming the faulty key, for example:</p>
<pre><code class="language-text">Unrecognized option "shared_file" under "automate". Available options are "on_deploy", "on_first_deploy", ...</code></pre>]]>
    </content>
    <link href="https://automate-deployer.com/docs/configuration/" rel="alternate" type="text/html" />
    <published>2026-10-30T11:10:35+00:00</published>
    <updated>2026-10-30T11:10:35+00:00</updated>
  </entry>
  <entry xml:lang="en">
    <id>https://automate-deployer.com/docs/deployment-lifecycle/</id>
    <title>Deployment lifecycle</title>
    <content type="html">
      <![CDATA[<p>This page describes what <code>automate deploy</code> does on each server, in order. Knowing the sequence helps you choose the right <a href="/docs/hooks/">hook</a> for each task.</p>
<h2 id="directory-layout">Directory layout</h2>
<p>Under the <code>path</code> configured for a server, Automate maintains three entries:</p>
<pre><code class="language-text">/var/www/shop
├── automate.lock                      # only while a deployment runs
├── current -&gt; releases/2026.10.23-1412.587
├── releases
│   ├── 2026.10.21-0930.112
│   ├── 2026.10.22-1805.341
│   ├── 2026.10.23-1412.587
│   └── failed                         # the last failed or rolled back release, if any
└── shared
    ├── .env.local
    └── var/uploads</code></pre>
<table>
<thead>
<tr>
<th>Entry</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>releases/</code></td>
<td>One folder per deployment, named after its release id: <code>YYYY.MM.DD-HHMM.NNN</code>, where <code>NNN</code> orders the releases deployed within the same minute</td>
</tr>
<tr>
<td><code>current</code></td>
<td>A symbolic link to the published release. Point your web server, workers or cron jobs at it</td>
</tr>
<tr>
<td><code>shared/</code></td>
<td>Files and folders kept across releases (see <code>shared_files</code> and <code>shared_folders</code>). Can be moved elsewhere with <code>shared_path</code></td>
</tr>
<tr>
<td><code>releases/failed</code></td>
<td>The release of the last failed deployment, or the one left by the last rollback, kept for inspection and removed at the next deployment</td>
</tr>
<tr>
<td><code>automate.lock</code></td>
<td>Created at the start of a deployment, a rollback or a removal, and removed at its end, successful or not</td>
</tr>
</tbody>
</table>
<h2 id="steps-of-a-deployment">Steps of a deployment</h2>
<p>Automate opens a session with every server of the platform, then runs the following steps. Each step completes on every server before the next one starts, and a failure at any step stops the deployment on all servers.</p>
<ol>
<li><strong>Connect and detect first deployments.</strong> A server with no release in its <code>releases</code> folder is in its first deployment, which enables the <code>on_first_deploy</code> hooks for that server.</li>
<li><strong>Lock.</strong> <code>automate.lock</code> is created in <code>path</code>. If it already exists, the deployment stops with <code>A deployment, a removal or a rollback is already in progress</code>, unless <code>--force</code> is given. Any previous <code>releases/failed</code> folder is removed.</li>
<li><strong>Create the release folder</strong> <code>releases/&lt;release id&gt;</code>.</li>
<li><strong>Clone the repository</strong>, when <code>repository</code> is set: <code>git clone &lt;repository&gt; -q --recursive -b &lt;default_branch&gt; .</code>, followed by a <code>git checkout</code> of the branch, tag or commit given to <code>deploy</code>. A reference that matches a tag is checked out as <code>tags/&lt;ref&gt;</code>. Without <code>repository</code>, the release folder stays empty: your <code>pre_deploy</code> hooks are expected to <a href="/docs/deployment-strategies/">upload</a> the application.</li>
<li><strong><code>pre_deploy</code> hooks</strong> run in the release folder.</li>
<li><strong>Shared items.</strong> Each entry of <code>shared_files</code> and <code>shared_folders</code> is replaced in the release by a symbolic link to <code>shared/</code>. On the first deployment, a file or folder present in the release is moved to <code>shared/</code> first; otherwise an empty file or folder is created there.</li>
<li><strong><code>on_first_deploy</code> hooks</strong> run, only on the servers in their first deployment.</li>
<li><strong><code>on_deploy</code> hooks</strong> run in the release folder. This is where you build the application: install dependencies, compile assets, warm up caches.</li>
<li><strong>Publish.</strong> <code>current</code> is pointed to the new release with <code>ln -sfn</code>. From this moment, the new release is live.</li>
<li><strong><code>post_deploy</code> hooks</strong> run in the new release: migrations, cache clearing, restarting workers.</li>
<li><strong>Clean up.</strong> Releases beyond <code>max_releases</code> are removed, oldest first. The release that <code>current</code> points to is never removed; <code>releases/failed</code> and any folder that does not look like a release id are ignored.</li>
<li><strong>Unlock.</strong> <code>automate.lock</code> is removed.</li>
</ol>
<blockquote>
<p><strong>Which hook should I use?</strong> Anything that must be ready before the release goes live belongs in <code>on_deploy</code>. Anything that acts on the live application, such as database migrations or process restarts, belongs in <code>post_deploy</code>. See <a href="/docs/hooks/">Hooks</a> for details and the environment variables available to your commands.</p>
</blockquote>
<h2 id="multiple-servers">Multiple servers</h2>
<p>Commands run <strong>in parallel</strong> on all the servers of the platform, each one in its own SSH session. The output is prefixed with the server name. If a command fails on one server, the deployment stops once the command completed on the others; the release is never published anywhere, because publishing comes after all the build hooks.</p>
<p>Use the <code>only</code> option of an action to run it on a subset of servers, typically to run a database migration once:</p>
<pre><code class="language-yaml">post_deploy:
    - cmd: php bin/console doctrine:migrations:migrate -n
      only: [web-01]</code></pre>
<h2 id="when-a-deployment-fails">When a deployment fails</h2>
<ul>
<li><strong>Before publishing</strong> (steps 2 to 8): the release is moved to <code>releases/failed</code>, <code>current</code> still points to the previous release, and the lock is released. Nothing changed for your users. Inspect <code>releases/failed</code>, fix the problem and deploy again.</li>
<li><strong>After publishing</strong> (<code>post_deploy</code>): the new release stays live, since it was already switched. The deployment is reported as failed, the lock is released, and the release is kept. Run the failing command again with <a href="/docs/commands/#exec"><code>automate exec --current</code></a>, or deploy a fix.</li>
</ul>
<h2 id="rolling-back">Rolling back</h2>
<p>Publishing is a symbolic link, so rolling back is pointing <code>current</code> to a previous release. The releases kept by <code>max_releases</code> are intact, including their shared links, and <a href="/docs/commands/#rollback"><code>automate rollback</code></a> switches to one of them on every server:</p>
<pre><code class="language-bash">automate rollback production                        # the release before the current one
automate rollback production 2026.10.22-1805.341    # a given release</code></pre>
<p>On each server, the command:</p>
<ol>
<li><strong>Locks</strong> the project with <code>automate.lock</code>, as a deployment does.</li>
<li><strong>Checks the target.</strong> <code>current</code> must point to one of the listed releases and the target must exist. Without an explicit release, every server must agree on the previous one; otherwise nothing is changed and the command asks for an explicit release.</li>
<li><strong>Publishes</strong> the target release with <code>ln -sfn</code>.</li>
<li><strong>Runs the <code>post_rollback</code> hooks</strong> in the restored release, with the same <a href="/docs/hooks/#hook-environment">environment</a> as the deployment hooks and <code>AUTOMATE_PHASE=rollback</code>.</li>
<li><strong>Sets aside the release that was left</strong> by moving it to <code>releases/failed</code>, where it can be inspected until the next deployment removes it. Since it is no longer listed, it will not be picked by <code>max_releases</code> over an older, working release, and a second <code>rollback</code> goes one release further back.</li>
<li><strong>Unlocks.</strong></li>
</ol>
<p>If a <code>post_rollback</code> command fails, <code>current</code> already points to the restored release and the release that was left stays in <code>releases/</code>.</p>
<p>Remember to revert any <code>post_deploy</code> effect that is not backward compatible, such as a database migration, for example in a <code>post_rollback</code> hook.</p>
<h2 id="locking">Locking</h2>
<p>The lock is per server, in the <code>path</code> directory. A lock left behind by a crashed process or a lost connection makes every later deployment fail with <code>A deployment, a removal or a rollback is already in progress</code>. When you are sure no deployment is running, deploy with <code>--force</code>, which ignores the existing lock, or remove the file:</p>
<pre><code class="language-bash">automate exec production web-01 "rm /var/www/shop/automate.lock"</code></pre>]]>
    </content>
    <link href="https://automate-deployer.com/docs/deployment-lifecycle/" rel="alternate" type="text/html" />
    <published>2026-10-30T11:10:35+00:00</published>
    <updated>2026-10-30T11:10:35+00:00</updated>
  </entry>
  <entry xml:lang="en">
    <id>https://automate-deployer.com/docs/deployment-strategies/</id>
    <title>Deployment strategies</title>
    <content type="html">
      <![CDATA[<p>Automate can fill a release in three ways. Choose the one that matches where your application is built.</p>
<table>
<thead>
<tr>
<th>Strategy</th>
<th>The server needs</th>
<th>Best for</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="#git-deployment">Git</a></td>
<td><code>git</code>, access to the repository, the build tools</td>
<td>Simple projects, servers that can build</td>
</tr>
<tr>
<td><a href="#ci-build-and-upload">CI build and upload</a></td>
<td>Nothing but a shell</td>
<td>Heavy builds, servers without build tools, no repository access from the server</td>
</tr>
<tr>
<td><a href="#mixed-deployment">Mixed</a></td>
<td><code>git</code>, access to the repository</td>
<td>Build the assets in CI, keep the source from Git</td>
</tr>
</tbody>
</table>
<h2 id="git-deployment">Git deployment</h2>
<p>Set <code>repository</code>. On each server, Automate clones the repository into the new release, checks out the requested reference and runs your hooks there. This is the simplest setup: the server does everything.</p>
<pre><code class="language-yaml">repository: git@github.com:acme/shop.git

on_deploy:
    - composer install --no-dev --optimize-autoloader
    - yarn install --frozen-lockfile
    - yarn build

post_deploy:
    - cmd: php bin/console doctrine:migrations:migrate -n
      only: [web-01]</code></pre>
<p>The server must be able to reach the repository, usually with a deploy key, and must have the build tools installed: <code>composer</code>, <code>node</code>, and so on. <code>automate check &lt;platform&gt;</code> verifies the repository access from each server.</p>
<blockquote>
<p>The clone uses <code>--recursive</code>, so Git submodules are checked out too.</p>
</blockquote>
<h2 id="ci-build-and-upload">CI build and upload</h2>
<p>Leave <code>repository</code> unset. Build the application in your CI pipeline, then upload the result with an <code>upload</code> action in <code>pre_deploy</code>. The server only receives files and runs the commands that must happen there, such as a database migration.</p>
<pre><code class="language-yaml"># no "repository" key

pre_deploy:
    - upload: .
      exclude: [node_modules, .git, tests]

post_deploy:
    - cmd: php bin/console doctrine:migrations:migrate -n
      only: [web-01]</code></pre>
<p>Each <code>upload</code> packs the local path in a <code>tar.gz</code> archive, sends it to the release folder of every server (or of the servers listed in <code>only</code>), and extracts it there. See <a href="/docs/hooks/#upload">Upload actions</a> for the <code>exclude</code> syntax.</p>
<p>This strategy keeps the servers minimal and makes the deployed artifact identical on every server, since it is built once.</p>
<h2 id="mixed-deployment">Mixed deployment</h2>
<p>Set <code>repository</code> <strong>and</strong> upload what the CI built. Automate clones the source on the server, then your <code>pre_deploy</code> upload adds the generated files, for example the compiled assets, on top of it.</p>
<pre><code class="language-yaml">repository: git@github.com:acme/shop.git

pre_deploy:
    - upload: public/build

on_deploy:
    - composer install --no-dev --optimize-autoloader

post_deploy:
    - cmd: php bin/console doctrine:migrations:migrate -n
      only: [web-01]</code></pre>
<p>This keeps the Git history and the PHP build on the server, while sparing it from installing Node and compiling assets.</p>
<h2 id="deploying-on-the-local-machine">Deploying on the local machine</h2>
<p>With <code>transport: local</code>, a server is the machine that runs Automate itself: no SSH is involved, commands run directly and uploads are plain file copies. The three strategies above work the same way. See <a href="/docs/configuration/#local-transport">Local transport</a>.</p>]]>
    </content>
    <link href="https://automate-deployer.com/docs/deployment-strategies/" rel="alternate" type="text/html" />
    <published>2026-10-30T11:10:35+00:00</published>
    <updated>2026-10-30T11:10:35+00:00</updated>
  </entry>
  <entry xml:lang="en">
    <id>https://automate-deployer.com/docs/hooks/</id>
    <title>Hooks</title>
    <content type="html">
      <![CDATA[<p>Hooks are lists of <strong>actions</strong> run on the servers at a given stage of a deployment. Every hook is optional.</p>
<table>
<thead>
<tr>
<th>Hook</th>
<th>When</th>
<th>Working directory</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>pre_deploy</code></td>
<td>After the repository is cloned, before the shared items are linked</td>
<td>The new release</td>
</tr>
<tr>
<td><code>on_first_deploy</code></td>
<td>After the shared items, only on servers with no release yet</td>
<td>The new release</td>
</tr>
<tr>
<td><code>on_deploy</code></td>
<td>After the shared items, <strong>before</strong> the release is published</td>
<td>The new release</td>
</tr>
<tr>
<td><code>post_deploy</code></td>
<td><strong>After</strong> the release is published</td>
<td>The new release, now <code>current</code></td>
</tr>
<tr>
<td><code>on_remove</code></td>
<td>By <code>automate remove</code>, before anything is deleted</td>
<td>The <code>current</code> release</td>
</tr>
<tr>
<td><code>post_rollback</code></td>
<td>By <code>automate rollback</code>, <strong>after</strong> <code>current</code> is switched back</td>
<td>The restored release, now <code>current</code></td>
</tr>
</tbody>
</table>
<p>The full sequence is described in the <a href="/docs/deployment-lifecycle/">Deployment lifecycle</a>.</p>
<h2 id="actions">Actions</h2>
<p>An action is a <strong>command</strong> or an <strong>upload</strong>. A plain string is a command:</p>
<pre><code class="language-yaml">on_deploy:
    - composer install --no-dev
    - cmd: yarn build
    - upload: public/build</code></pre>
<h3 id="commands">Commands</h3>
<table>
<thead>
<tr>
<th>Option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>cmd</code></td>
<td>The shell command. It runs through <code>sh</code> with <code>cd &lt;release&gt; &amp;&amp; &lt;cmd&gt;</code>, so <code>&amp;&amp;</code>, pipes, redirections and environment variables work as in a shell script</td>
</tr>
<tr>
<td><code>only</code></td>
<td>A server name or a list of server names the command runs on. Default: all the servers of the platform</td>
</tr>
</tbody>
</table>
<pre><code class="language-yaml">post_deploy:
    - cmd: php bin/console doctrine:migrations:migrate -n
      only: web-01
    - cmd: php bin/console messenger:stop-workers
      only: [web-01, web-02]</code></pre>
<p>The command fails, and so does the deployment, when it exits with a non-zero status. Its output is printed under the server name.</p>
<blockquote>
<p><strong>Tip:</strong> long scripts are easier to maintain in a file committed with the project: <code>- bash deploy/after-publish.sh</code>. The script receives the <a href="#hook-environment">hook environment</a>.</p>
</blockquote>
<h3 id="upload">Upload</h3>
<p>An <code>upload</code> action sends a local file or directory to the release folder of the servers. The path is read on the machine that runs Automate, relative to the current directory, and recreated at the same relative path in the release.</p>
<table>
<thead>
<tr>
<th>Option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>upload</code></td>
<td>The local file or directory to send</td>
</tr>
<tr>
<td><code>exclude</code></td>
<td>Patterns of paths to leave out, relative to the uploaded directory</td>
</tr>
<tr>
<td><code>only</code></td>
<td>A server name or a list of server names. Default: all the servers</td>
</tr>
</tbody>
</table>
<pre><code class="language-yaml">pre_deploy:
    - upload: .
      exclude:
          - node_modules
          - .git
          - '/^tests\//'
    - upload: public/build
      only: [web-01]</code></pre>
<p>Automate packs the files in a <code>tar.gz</code> archive, sends it to each server and extracts it in the release folder. Each <code>exclude</code> entry is matched against the path of every file, relative to the uploaded directory:</p>
<ul>
<li>a <strong>plain string</strong> excludes every path that contains it: <code>node_modules</code> drops that folder wherever it appears;</li>
<li>a <strong>regular expression</strong>, with its delimiters, is matched against the same relative path: <code>'/^tests\//'</code> only drops the <code>tests</code> folder at the root of the upload. Escape the delimiter inside the expression: <code>'/^folder/subfolder/'</code> is not a valid pattern and is rejected.</li>
</ul>
<p><code>upload</code> is not allowed in <code>on_remove</code> and <code>post_rollback</code>.</p>
<h2 id="first-deployment">First deployment</h2>
<p>A server is in its <strong>first deployment</strong> when its <code>releases</code> folder contains no release. A release moved to <code>releases/failed</code> does not count, so the next attempt is still a first deployment. The detection is made per server: on a platform with one new server and one already deployed, only the new one runs <code>on_first_deploy</code>, and <code>only</code> can restrict it further.</p>
<p>Since the links to <code>shared/</code> already exist at that point, the hook can create the files that later deployments will find in place:</p>
<pre><code class="language-yaml">on_first_deploy:
    - 'cp .env.dist "$AUTOMATE_SHARED_PATH/.env.local"'
    - php bin/console doctrine:database:create</code></pre>
<p>Any command can also test the <code>AUTOMATE_FIRST_DEPLOY</code> variable, for example in <code>post_deploy</code>:</p>
<pre><code class="language-yaml">post_deploy:
    - 'if [ "$AUTOMATE_FIRST_DEPLOY" = 1 ]; then php bin/console doctrine:fixtures:load -n; fi'</code></pre>
<h2 id="removal-hook">Removal hook</h2>
<p><code>on_remove</code> lists the commands run by <a href="/docs/commands/#remove"><code>automate remove</code></a> in the <code>current</code> release, <strong>before</strong> anything is deleted. Use it to stop what the project started: containers, cron jobs, workers.</p>
<pre><code class="language-yaml">on_remove:
    - docker compose down --volumes
    - cmd: crontab -r
      only: [web-01]</code></pre>
<p>If a command fails, nothing is deleted on any server. The hook is skipped on a server that has no <code>current</code> release.</p>
<h2 id="rollback-hook">Rollback hook</h2>
<p><code>post_rollback</code> lists the commands run by <a href="/docs/commands/#rollback"><code>automate rollback</code></a> in the restored release, <strong>after</strong> <code>current</code> has been switched back to it and before the release that was left is moved to <code>releases/failed</code>. Use it to reload or restart what runs the code: PHP-FPM, workers, a Node server.</p>
<pre><code class="language-yaml">post_rollback:
    - cmd: php bin/console messenger:stop-workers
      only: [web-01]</code></pre>
<p>If a command fails, the rollback is reported as failed: <code>current</code> already points to the restored release, and the release that was left stays in <code>releases/</code>.</p>
<h2 id="hook-environment">Hook environment</h2>
<p>Every command runs with these environment variables, so scripts can locate the release and the shared folder without hard-coding paths:</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>AUTOMATE_PHASE</code></td>
<td><code>deploy</code> for the deployment hooks, <code>remove</code> for <code>on_remove</code>, <code>rollback</code> for <code>post_rollback</code>, <code>exec</code> for <code>automate exec --current</code></td>
</tr>
<tr>
<td><code>AUTOMATE_PLATFORM</code></td>
<td>The platform name</td>
</tr>
<tr>
<td><code>AUTOMATE_SERVER</code></td>
<td>The name of the server the command runs on</td>
</tr>
<tr>
<td><code>AUTOMATE_GIT_REF</code></td>
<td>The reference given to <code>deploy</code>, or the <code>default_branch</code> of the platform (deployment hooks only)</td>
</tr>
<tr>
<td><code>AUTOMATE_RELEASE_ID</code></td>
<td>The release id, for example <code>2026.10.23-1412.587</code></td>
</tr>
<tr>
<td><code>AUTOMATE_RELEASE_PATH</code></td>
<td><code>&lt;path&gt;/releases/&lt;release id&gt;</code></td>
</tr>
<tr>
<td><code>AUTOMATE_RELEASES_PATH</code></td>
<td><code>&lt;path&gt;/releases</code></td>
</tr>
<tr>
<td><code>AUTOMATE_SHARED_PATH</code></td>
<td><code>shared_path</code>, or <code>&lt;path&gt;/shared</code></td>
</tr>
<tr>
<td><code>AUTOMATE_CURRENT_PATH</code></td>
<td><code>&lt;path&gt;/current</code></td>
</tr>
<tr>
<td><code>AUTOMATE_FIRST_DEPLOY</code></td>
<td><code>1</code> on a server with no release yet, <code>0</code> otherwise. Always <code>0</code> outside of a deployment</td>
</tr>
<tr>
<td><code>AUTOMATE__&lt;name&gt;</code></td>
<td>Each <a href="/docs/variables/">variable</a> given with <code>--var</code> or resolved from the configuration, except the ones used as a <code>password</code></td>
</tr>
</tbody>
</table>
<p>During <code>on_remove</code>, <code>post_rollback</code> and <code>exec --current</code>, the <code>AUTOMATE_RELEASE_*</code> variables describe the release that <code>current</code> points to.</p>
<pre><code class="language-yaml">post_deploy:
    - 'echo "Deployed $AUTOMATE_GIT_REF to $AUTOMATE_SERVER" &gt;&gt; "$AUTOMATE_SHARED_PATH/deployments.log"'</code></pre>]]>
    </content>
    <link href="https://automate-deployer.com/docs/hooks/" rel="alternate" type="text/html" />
    <published>2026-10-30T11:10:35+00:00</published>
    <updated>2026-10-30T11:10:35+00:00</updated>
  </entry>
  <entry xml:lang="en">
    <id>https://automate-deployer.com/docs/installation/</id>
    <title>Installation</title>
    <content type="html">
      <![CDATA[<p>Automate is distributed as a <strong>static binary</strong> that embeds PHP: nothing needs to be installed beforehand. A <strong>PHAR</strong> archive is also published for platforms without a prebuilt binary.</p>
<h2 id="install-script">Install script</h2>
<pre><code class="language-bash">curl -LSs https://www.automate-deployer.com/install | bash</code></pre>
<p>The script:</p>
<ol>
<li>checks that <code>curl</code> or <code>wget</code> is available;</li>
<li>detects your operating system and architecture;</li>
<li>downloads the latest release from <a href="https://github.com/automatephp/automate/releases" rel="noopener noreferrer">GitHub Releases</a>;</li>
<li>installs it as <code>/usr/local/bin/automate</code> when run as root, or as <code>~/.automate/bin/automate</code> otherwise.</li>
</ol>
<p>When installed in your home directory, add it to your <code>PATH</code> or move it to a system directory:</p>
<pre><code class="language-bash">sudo mv ~/.automate/bin/automate /usr/local/bin/automate</code></pre>
<p>Then check the installation:</p>
<pre><code class="language-bash">automate --version</code></pre>
<blockquote>
<p><strong>Upgrading:</strong> run the install script again. It always downloads the latest release and replaces the binary.</p>
</blockquote>
<h2 id="supported-platforms">Supported platforms</h2>
<table>
<thead>
<tr>
<th>Platform</th>
<th>Binary</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Linux x86_64</td>
<td><code>automate-linux-amd64</code></td>
<td>Install script or manual download</td>
</tr>
<tr>
<td>macOS Apple Silicon</td>
<td><code>automate-darwin-arm64</code></td>
<td>Install script or manual download</td>
</tr>
<tr>
<td>macOS Intel</td>
<td>no binary</td>
<td>Use the PHAR</td>
</tr>
<tr>
<td>Linux ARM64</td>
<td>no binary</td>
<td>Use the PHAR</td>
</tr>
<tr>
<td>Windows</td>
<td>no binary</td>
<td>Use the PHAR. The <code>local</code> transport is not available on Windows</td>
</tr>
</tbody>
</table>
<h2 id="manual-download">Manual download</h2>
<p>Every release publishes the binaries on GitHub. Download the one for your platform, make it executable and move it to your <code>PATH</code>:</p>
<pre><code class="language-bash">sudo curl -LSs -o /usr/local/bin/automate https://github.com/automatephp/automate/releases/latest/download/automate-linux-amd64
sudo chmod +x /usr/local/bin/automate</code></pre>
<p>Give the output file a path that does not already exist as a directory: <code>-o automate</code> fails with <code>Failure writing output to destination</code> when run next to a folder named <code>automate</code>, such as a clone of the repository.</p>
<p>To pin a specific version, replace <code>latest/download</code> with <code>download/&lt;version&gt;</code>:</p>
<pre><code class="language-bash">sudo curl -LSs -o /usr/local/bin/automate https://github.com/automatephp/automate/releases/download/4.2.2/automate-linux-amd64</code></pre>
<p>Pinning is recommended in CI pipelines so that a new release never changes a build unexpectedly.</p>
<h2 id="phar">PHAR</h2>
<p>The PHAR requires <strong>PHP 8.4 or later</strong>. The <code>openssl</code> and <code>gmp</code> (or <code>bcmath</code>) extensions are recommended: they make the SSH key exchange much faster.</p>
<pre><code class="language-bash">curl -LSs -o automate.phar https://github.com/automatephp/automate/releases/latest/download/automate.phar
php automate.phar --version</code></pre>
<p>You can also make it executable and move it to your <code>PATH</code> as <code>automate</code>.</p>
<h2 id="shell-completion">Shell completion</h2>
<p>Automate is built with the Symfony Console component and supports its completion script. It needs a release newer than 4.2.2: older binaries do not embed the completion scripts and report <code>supported shells: ""</code>.</p>
<pre><code class="language-bash">automate completion bash &gt; /etc/bash_completion.d/automate   # bash
automate completion zsh &gt; "${fpath[1]}/_automate"             # zsh</code></pre>]]>
    </content>
    <link href="https://automate-deployer.com/docs/installation/" rel="alternate" type="text/html" />
    <published>2026-10-30T11:10:35+00:00</published>
    <updated>2026-10-30T11:10:35+00:00</updated>
  </entry>
  <entry xml:lang="en">
    <id>https://automate-deployer.com/docs/introduction/</id>
    <title>Introduction</title>
    <content type="html">
      <![CDATA[<p>Automate ships your application to Linux servers with a single command. You describe your platforms, servers and deployment steps in a <code>.automate.yml</code> file at the root of your project, then run <code>automate deploy production</code> from your workstation or from your CI pipeline.</p>
<p>Every deployment creates a new, isolated <strong>release</strong> on each server. When all your build steps succeed, a <code>current</code> symbolic link is switched to the new release. Your web server always points to <code>current</code>, so visitors never see a half-deployed application, and a failed deployment never goes live.</p>
<h2 id="features">Features</h2>
<ul>
<li><strong>Atomic, zero-downtime releases.</strong> Each deployment lives in its own timestamped folder; publishing is a single symlink switch, and so is rolling back with <code>automate rollback</code>.</li>
<li><strong>Multi-server platforms.</strong> Deploy to one server or to a whole fleet. Hooks run in parallel on every server, and any command can be restricted to a subset with <code>only</code>.</li>
<li><strong>Shared files and folders.</strong> Keep <code>.env.local</code>, uploads or caches across releases: Automate links them into every new release.</li>
<li><strong>Git, CI build, or both.</strong> Clone the repository on the server, upload the artifacts built by your CI, or mix the two. See <a href="/docs/deployment-strategies/">Deployment strategies</a>.</li>
<li><strong>Hooks at every stage.</strong> Run commands before the release is prepared, on the first deployment of a server, before publishing, after publishing, after a rollback, and when a project is removed. See <a href="/docs/hooks/">Hooks</a>.</li>
<li><strong>Secrets from the environment.</strong> Passwords and other values are resolved from <code>--var</code> options or <code>AUTOMATE__*</code> environment variables, and masked in the output. See <a href="/docs/variables/">Variables</a>.</li>
<li><strong>Safe by default.</strong> A lock file prevents two deployments from overlapping, failed releases are set aside, and old releases are pruned automatically.</li>
<li><strong>Any stack.</strong> PHP, Node, Ruby, Python, static sites: if it runs on a Linux server, Automate can deploy it. Automate itself is a single static binary with no runtime dependency.</li>
</ul>
<h2 id="how-it-works">How it works</h2>
<pre><code class="language-text">$ automate deploy production

 Start deployment
 ----------------
 ------------ -----------------------------------
  Repository   git@github.com:acme/shop.git
  Platform     production
  Servers      web-01 (web-01.acme.io)
               web-02 (web-02.acme.io)
  Version      main
 ------------ -----------------------------------

 Prepare Release
 ---------------
 git clone git@github.com:acme/shop.git -q --recursive -b main .

 Setting up shared items
 -----------------------
 [web-01] /var/www/shop/releases/2026.10.23-1412.587/.env.local --&gt; /var/www/shop/shared/.env.local
 [web-02] /var/www/shop/releases/2026.10.23-1412.587/.env.local --&gt; /var/www/shop/shared/.env.local

 On deploy
 ---------
 composer install --no-dev --optimize-autoloader

 Publish new release
 -------------------
 [web-01] /var/www/shop/current --&gt; /var/www/shop/releases/2026.10.23-1412.587
 [web-02] /var/www/shop/current --&gt; /var/www/shop/releases/2026.10.23-1412.587

 Post deploy
 -----------
 php bin/console doctrine:migrations:migrate -n

 Clear olds releases
 -------------------

 [OK] All is OK</code></pre>
<p>On each server, Automate maintains this layout under the configured <code>path</code>:</p>
<pre><code class="language-text">/var/www/shop
├── current -&gt; releases/2026.10.23-1412.587
├── releases
│   ├── 2026.10.22-1805.341
│   └── 2026.10.23-1412.587
└── shared
    ├── .env.local
    └── var/uploads</code></pre>
<p>The <a href="/docs/deployment-lifecycle/">Deployment lifecycle</a> page details every step.</p>
<h2 id="requirements">Requirements</h2>
<p><strong>On your machine</strong> (or your CI runner):</p>
<ul>
<li>Linux x86_64 or macOS Apple Silicon for the prebuilt binary, or PHP 8.4+ for the PHAR. See <a href="/docs/installation/">Installation</a>.</li>
<li><code>tar</code>, when you use <code>upload</code> actions.</li>
</ul>
<p><strong>On each server:</strong></p>
<ul>
<li>SSH access with a password or a private key, for the default <code>ssh</code> transport.</li>
<li>A POSIX shell and the usual tools: <code>sh</code>, <code>tar</code>, <code>find</code>, <code>ln</code>, <code>mkdir</code>.</li>
<li><code>git</code>, when you deploy from a repository. The server must be able to reach the repository, for example with a deploy key.</li>
<li>Whatever your hooks need: <code>composer</code>, <code>php</code>, <code>node</code>, and so on.</li>
</ul>
<h2 id="next-steps">Next steps</h2>
<ol>
<li><a href="/docs/installation/">Install Automate</a></li>
<li><a href="/docs/quick-start/">Follow the quick start</a> to deploy your first project</li>
<li><a href="/docs/configuration/">Browse the configuration reference</a></li>
</ol>]]>
    </content>
    <link href="https://automate-deployer.com/docs/introduction/" rel="alternate" type="text/html" />
    <published>2026-10-30T11:10:35+00:00</published>
    <updated>2026-10-30T11:10:35+00:00</updated>
  </entry>
  <entry xml:lang="en">
    <id>https://automate-deployer.com/docs/quick-start/</id>
    <title>Quick start</title>
    <content type="html">
      <![CDATA[<p>This guide deploys a Git repository to a single server. It takes about five minutes once <a href="/docs/installation/">Automate is installed</a>.</p>
<h2 id="1-prepare-the-server">1. Prepare the server</h2>
<p>Automate needs an SSH user on the server, a directory this user can write to, and access to your Git repository from the server.</p>
<pre><code class="language-bash"># on the server
sudo adduser deploy
sudo mkdir -p /var/www/shop
sudo chown deploy:deploy /var/www/shop</code></pre>
<p>Then make sure that <code>git</code> is installed and that the <code>deploy</code> user can clone your repository, for example by adding its public key as a <strong>deploy key</strong> on GitHub or GitLab:</p>
<pre><code class="language-bash"># on the server, as the deploy user
ssh-keygen -t ed25519 -C "deploy@web-01"
cat ~/.ssh/id_ed25519.pub   # add this key to the repository
git ls-remote git@github.com:acme/shop.git</code></pre>
<h2 id="2-write-the-configuration">2. Write the configuration</h2>
<p>Create a <code>.automate.yml</code> file at the root of your project:</p>
<pre><code class="language-yaml">repository: git@github.com:acme/shop.git

platforms:
    production:
        default_branch: main
        servers:
            web-01:
                host: web-01.acme.io
                user: deploy
                password: "%prod_password%"
                path: /var/www/shop

shared_files:
    - .env.local

shared_folders:
    - var/uploads

on_deploy:
    - composer install --no-dev --optimize-autoloader

post_deploy:
    - php bin/console doctrine:migrations:migrate -n</code></pre>
<p>A few things to note:</p>
<ul>
<li><code>production</code> is a <strong>platform</strong>: a named group of servers deployed together. <code>main</code> is deployed when you do not specify another branch, tag or commit.</li>
<li><code>"%prod_password%"</code> is a <a href="/docs/variables/">variable</a>. Automate reads it from the <code>AUTOMATE__prod_password</code> environment variable, and prompts for it in your terminal when it is not set. To authenticate with a key instead, replace <code>password</code> with <code>ssh_key: /home/me/.ssh/id_ed25519</code>.</li>
<li><code>shared_files</code> and <code>shared_folders</code> are kept between releases: the first deployment moves them to the <code>shared</code> folder, and every release links to them.</li>
<li><code>on_deploy</code> runs in the new release <strong>before</strong> it is published, <code>post_deploy</code> <strong>after</strong>. The <a href="/docs/hooks/">hooks</a> page lists all of them.</li>
</ul>
<h2 id="3-check-the-connection">3. Check the connection</h2>
<pre><code class="language-bash">automate check production</code></pre>
<p>Automate connects to every server of the platform and verifies that the repository is reachable from there with <code>git ls-remote</code>. Fix any error before deploying.</p>
<h2 id="4-deploy">4. Deploy</h2>
<pre><code class="language-bash">automate deploy production</code></pre>
<p>The first deployment creates the <code>releases</code>, <code>shared</code> and <code>current</code> entries under <code>/var/www/shop</code>. On the first deployment the hooks of <code>on_first_deploy</code> also run, which is a good place to create <code>.env.local</code> in the shared folder.</p>
<p>Point your web server at the <code>current</code> link, for example <code>/var/www/shop/current/public</code> for a Symfony or Laravel application. That path never changes: every deployment only updates the link.</p>
<h2 id="5-deploy-something-else">5. Deploy something else</h2>
<p>Deploy a branch, a tag or a commit by giving it after the platform name:</p>
<pre><code class="language-bash">automate deploy production v1.4.0
automate deploy production hotfix/payment
automate deploy production 3f2a9c1</code></pre>
<h2 id="where-to-go-next">Where to go next</h2>
<ul>
<li><a href="/docs/deployment-lifecycle/">Deployment lifecycle</a>: what happens on the server, step by step.</li>
<li><a href="/docs/deployment-strategies/">Deployment strategies</a>: clone on the server, upload a CI build, or both.</li>
<li><a href="/docs/ci-integration/">CI integration</a>: run Automate from GitHub Actions or GitLab CI.</li>
<li><a href="/docs/configuration/">Configuration reference</a>: every option of <code>.automate.yml</code>.</li>
</ul>]]>
    </content>
    <link href="https://automate-deployer.com/docs/quick-start/" rel="alternate" type="text/html" />
    <published>2026-10-30T11:10:35+00:00</published>
    <updated>2026-10-30T11:10:35+00:00</updated>
  </entry>
  <entry xml:lang="en">
    <id>https://automate-deployer.com/docs/recipes/</id>
    <title>Recipes</title>
    <content type="html">
      <![CDATA[<p>Starting points for common setups. Every example is a complete <code>.automate.yml</code>; adapt the hosts, paths and credentials.</p>
<h2 id="serving-the-current-release-with-nginx-or-caddy">Serving the current release with nginx or Caddy</h2>
<p>Point the document root at <code>&lt;path&gt;/current/public</code>, and make the web server <strong>resolve the symbolic link</strong> before passing the script path to PHP-FPM. PHP OPcache caches scripts by path: with the resolved path, each release gets fresh entries and there is nothing to reload after a deployment. Without it, OPcache keeps serving the previous release until PHP-FPM restarts.</p>
<p>With nginx, use <code>$realpath_root</code> instead of <code>$document_root</code>:</p>
<pre><code class="language-text">server {
    server_name www.acme.io;
    root /var/www/shop/current/public;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        internal;
    }
}</code></pre>
<p>Caddy resolves the link by default in <code>php_fastcgi</code> (<code>resolve_root_symlink</code> is enabled unless you turn it off):</p>
<pre><code class="language-text">www.acme.io {
    root * /var/www/shop/current/public
    php_fastcgi unix//run/php/php8.4-fpm.sock
    file_server
}</code></pre>
<h2 id="symfony">Symfony</h2>
<pre><code class="language-yaml">repository: git@github.com:acme/shop.git

platforms:
    production:
        default_branch: main
        servers:
            web-01:
                host: web-01.acme.io
                user: deploy
                ssh_key: /home/me/.ssh/id_ed25519
                path: /var/www/shop

shared_files:
    - .env.local

shared_folders:
    - var/log
    - public/uploads

on_first_deploy:
    - 'cp .env "$AUTOMATE_SHARED_PATH/.env.local"'

on_deploy:
    - composer install --no-dev --optimize-autoloader --no-interaction
    - php bin/console cache:clear --no-warmup
    - php bin/console cache:warmup
    - php bin/console asset-map:compile

post_deploy:
    - cmd: php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
      only: [web-01]</code></pre>
<p>Point the web server at <code>/var/www/shop/current/public</code> as shown <a href="#serving-the-current-release-with-nginx-or-caddy">above</a>. <code>var/cache</code> stays inside each release, which is what Symfony expects: a release always starts with a cache built for its own code.</p>
<h2 id="laravel">Laravel</h2>
<pre><code class="language-yaml">repository: git@github.com:acme/app.git

platforms:
    production:
        default_branch: main
        servers:
            web-01:
                host: web-01.acme.io
                user: deploy
                ssh_key: /home/me/.ssh/id_ed25519
                path: /var/www/app

shared_files:
    - .env

shared_folders:
    - storage

on_first_deploy:
    - 'cp .env.example "$AUTOMATE_SHARED_PATH/.env"'
    - php artisan key:generate --force

on_deploy:
    - composer install --no-dev --optimize-autoloader --no-interaction
    - php artisan config:cache
    - php artisan route:cache
    - php artisan view:cache

post_deploy:
    - cmd: php artisan migrate --force
      only: [web-01]
    - php artisan queue:restart</code></pre>
<p>The web server serves <code>/var/www/app/current/public</code>. The <code>storage</code> folder is shared, so logs, sessions and uploads survive deployments. <code>php artisan storage:link</code> can be added to <code>on_deploy</code> when you use the public disk.</p>
<h2 id="static-site-built-in-ci">Static site built in CI</h2>
<p>The servers only receive files. Build in the pipeline, then upload the output:</p>
<pre><code class="language-yaml">platforms:
    production:
        default_branch: main
        servers:
            web-01:
                host: web-01.acme.io
                user: deploy
                password: "%prod_password%"
                path: /var/www/site

pre_deploy:
    - upload: dist</code></pre>
<p>Serve <code>/var/www/site/current/dist</code>. Since there is no <code>repository</code>, nothing needs to be installed on the server, not even <code>git</code>.</p>
<h2 id="assets-built-in-ci-php-built-on-the-server">Assets built in CI, PHP built on the server</h2>
<p>The mixed strategy: the server clones and installs the PHP dependencies, the CI uploads the compiled assets so that the server does not need Node.</p>
<pre><code class="language-yaml">repository: git@github.com:acme/shop.git

platforms:
    production:
        default_branch: main
        servers:
            web-01: { host: web-01.acme.io, user: deploy, ssh_key: /tmp/deploy_key, path: /var/www/shop }

shared_files: [.env.local]
shared_folders: [var/log, public/uploads]

pre_deploy:
    - upload: public/build

on_deploy:
    - composer install --no-dev --optimize-autoloader --no-interaction

post_deploy:
    - php bin/console doctrine:migrations:migrate -n</code></pre>
<h2 id="docker-compose-on-the-host">Docker Compose on the host</h2>
<p>Deploy the sources, then rebuild and restart the containers from the published release. The <code>local</code> transport runs everything on the machine itself, which suits a runner installed on the host.</p>
<pre><code class="language-yaml">repository: git@github.com:acme/stack.git

platforms:
    production:
        default_branch: main
        max_releases: 2
        servers:
            host:
                transport: local
                path: /srv/stack

shared_files:
    - .env

post_deploy:
    - docker compose -f "$AUTOMATE_CURRENT_PATH/compose.yaml" up -d --build --remove-orphans

on_remove:
    - docker compose down --volumes</code></pre>
<p><code>on_remove</code> stops the stack before <code>automate remove</code> deletes the files.</p>
<h2 id="several-servers-behind-a-load-balancer">Several servers behind a load balancer</h2>
<p>Run the build everywhere, the migration once, and restart each server:</p>
<pre><code class="language-yaml">repository: git@github.com:acme/shop.git

platforms:
    production:
        default_branch: main
        servers:
            web-01: { host: web-01.acme.io, user: deploy, ssh_key: /home/me/.ssh/id_ed25519, path: /var/www/shop }
            web-02: { host: web-02.acme.io, user: deploy, ssh_key: /home/me/.ssh/id_ed25519, path: /var/www/shop }
            web-03: { host: web-03.acme.io, user: deploy, ssh_key: /home/me/.ssh/id_ed25519, path: /var/www/shop }

shared_files: [.env.local]
shared_folders: [var/log]

on_deploy:
    - composer install --no-dev --optimize-autoloader --no-interaction
    - php bin/console cache:warmup

post_deploy:
    - cmd: php bin/console doctrine:migrations:migrate -n
      only: web-01</code></pre>
<p>Uploads shared by all servers, such as user files, belong on a network mount declared with <code>shared_path</code>:</p>
<pre><code class="language-yaml">            web-01:
                host: web-01.acme.io
                user: deploy
                ssh_key: /home/me/.ssh/id_ed25519
                path: /var/www/shop
                shared_path: /mnt/nfs/shop/shared</code></pre>
<h2 id="staging-and-production-in-one-file">Staging and production in one file</h2>
<pre><code class="language-yaml">repository: git@github.com:acme/shop.git

platforms:
    staging:
        default_branch: develop
        max_releases: 2
        servers:
            staging-01: { host: staging.acme.io, user: deploy, ssh_key: /home/me/.ssh/id_ed25519, path: /var/www/shop }
    production:
        default_branch: main
        max_releases: 5
        servers:
            web-01: { host: web-01.acme.io, user: deploy, ssh_key: /home/me/.ssh/id_ed25519, path: /var/www/shop }

shared_files: [.env.local]

on_deploy:
    - composer install --no-dev --optimize-autoloader --no-interaction

post_deploy:
    - php bin/console doctrine:migrations:migrate -n
    - cmd: php bin/console doctrine:fixtures:load -n --group=demo
      only: staging-01</code></pre>
<pre><code class="language-bash">automate deploy staging              # deploys "develop"
automate deploy production v2.3.0    # deploys a tag</code></pre>]]>
    </content>
    <link href="https://automate-deployer.com/docs/recipes/" rel="alternate" type="text/html" />
    <published>2026-10-30T11:10:35+00:00</published>
    <updated>2026-10-30T11:10:35+00:00</updated>
  </entry>
  <entry xml:lang="en">
    <id>https://automate-deployer.com/docs/troubleshooting/</id>
    <title>Troubleshooting</title>
    <content type="html">
      <![CDATA[<h2 id="reading-the-output">Reading the output</h2>
<p>Automate prints every command it runs on the servers, and the output of each one prefixed with the server name in brackets. When a step fails, the error message of the failing server is shown and the deployment stops. Add <code>-v</code> for more details, including the stack trace of an internal error.</p>
<p>To reproduce a command exactly as a hook runs it, use <a href="/docs/commands/#exec"><code>automate exec --current</code></a>: it runs in the published release with the same environment variables.</p>
<h2 id="errors">Errors</h2>
<h3 id="a-deployment-a-removal-or-a-rollback-is-already-in-progress">A deployment, a removal or a rollback is already in progress</h3>
<p>An <code>automate.lock</code> file exists in the project <code>path</code> of a server. Either another command is really running, or a previous one crashed before releasing the lock (lost connection, killed process).</p>
<p>When you are sure nothing is running, deploy with <code>--force</code> or remove the lock:</p>
<pre><code class="language-bash">automate deploy production --force
# or
automate exec production web-01 "rm /var/www/shop/automate.lock"</code></pre>
<h3 id="invalid-user-or-password">Invalid user or password</h3>
<p>The SSH authentication failed. Check the <code>user</code>, the <code>password</code> or the <code>ssh_key</code> of the server, and that the key is accepted by the server (<code>~/.ssh/authorized_keys</code>). When the key is encrypted, its passphrase goes in <code>password</code>.</p>
<p>Note that Automate reads the key file itself: the SSH agent and <code>~/.ssh/config</code> are not consulted. Give the absolute path of the key.</p>
<h3 id="file-not-found">File &quot;...&quot; not found</h3>
<p>The <code>ssh_key</code> path does not exist on the machine that runs Automate. In CI, make sure the step that writes the key from a secret ran before the deployment, and that both use the same path.</p>
<h3 id="unable-to-resolve-the-variable-name">Unable to resolve the variable &quot;%name%&quot;</h3>
<p>A <code>%name%</code> token is used in the configuration but has no value. Provide it with <code>--var name=value</code> or with the <code>AUTOMATE__name</code> environment variable. If the <code>%</code> characters are not a variable, for example in <code>date +%Y%m%d</code>, double them: <code>date +%%Y%%m%%d</code>. See <a href="/docs/variables/">Variables</a>.</p>
<h3 id="missing-configuration-file">Missing configuration file</h3>
<p>No <code>.automate.yml</code> in the current directory. Run Automate from the root of the project, or give the file with <code>-c path/to/.automate.yml</code>.</p>
<h3 id="missing-platform-or-missing-server">Missing platform ... or Missing server ...</h3>
<p>The name given to the command does not match a key of <code>platforms</code> or <code>servers</code>. Names are case sensitive.</p>
<h3 id="unrecognized-option-under-automate">Unrecognized option &quot;...&quot; under &quot;automate&quot;</h3>
<p>A typo in a configuration key. The message lists the valid options at that level. See the <a href="/docs/configuration/">Configuration reference</a>.</p>
<h3 id="the-host-option-is-required-with-the-ssh-transport">The &quot;host&quot; option is required with the &quot;ssh&quot; transport</h3>
<p>A server of the default <code>ssh</code> transport is missing <code>host</code> or <code>user</code>. Conversely, <code>host</code>, <code>user</code>, <code>password</code> and <code>ssh_key</code> are rejected with <code>transport: local</code>.</p>
<h3 id="does-not-exist">&quot;...&quot; does not exist</h3>
<p>The path of an <code>upload</code> action does not exist on the machine that runs Automate. Paths are relative to the current directory. In CI, make sure the build step that produces the folder ran first.</p>
<h3 id="invalid-exclude-pattern">Invalid &quot;exclude&quot; pattern</h3>
<p>An <code>exclude</code> entry looks like a regular expression but is not valid. The usual cause is an unescaped delimiter: write <code>'/^folder\/subfolder/'</code>, not <code>'/^folder/subfolder/'</code>. See <a href="/docs/hooks/#upload">Upload actions</a>.</p>
<h3 id="git-clone-or-git-checkout-fails">git clone or git checkout fails</h3>
<p>The server cannot reach the repository, or the reference does not exist. Run <code>automate check &lt;platform&gt;</code> to verify the access from each server, and make sure the branch, tag or commit given to <code>deploy</code> exists in the repository. Private repositories need a deploy key on the server.</p>
<h3 id="a-hook-fails-with-the-command-failed-with-exit-code">A hook fails with &quot;The command failed with exit code ...&quot;</h3>
<p>The command exited with a non-zero status without printing anything. Run it with <code>automate exec &lt;platform&gt; &lt;server&gt; "&lt;command&gt;" --current</code> to see its full output, or add <code>set -x</code> at the beginning of your script.</p>
<h2 id="inspecting-a-failed-release">Inspecting a failed release</h2>
<p>When a deployment fails before publishing, its release is moved to <code>releases/failed</code> on each server. The release left by <code>automate rollback</code> goes there too. The folder is kept until the next deployment, so you can look at it:</p>
<pre><code class="language-bash">automate exec production web-01 "ls -la /var/www/shop/releases/failed"
automate exec production web-01 "cat /var/www/shop/releases/failed/var/log/prod.log"</code></pre>
<h2 id="the-application-still-shows-the-previous-version">The application still shows the previous version</h2>
<ul>
<li>The web server must serve <code>&lt;path&gt;/current/...</code>, not a specific release folder. Check the document root.</li>
<li>PHP OPcache caches scripts by their resolved path. When the web server passes <code>/var/www/shop/current/public/index.php</code> to PHP-FPM, OPcache keeps serving the file it resolved at the first request, which may belong to the previous release. Configure the web server to resolve the <code>current</code> link itself, so that every release has its own paths: see <a href="/docs/recipes/#serving-the-current-release-with-nginx-or-caddy">Serving the current release</a>.</li>
<li>Long-running processes (queue workers, Node servers) keep the old release until they restart: restart them in <code>post_deploy</code>.</li>
</ul>
<h2 id="rolling-back">Rolling back</h2>
<p>Run <a href="/docs/commands/#rollback"><code>automate rollback &lt;platform&gt;</code></a> to point <code>current</code> back to the previous release on every server, or give it a release id. See <a href="/docs/deployment-lifecycle/#rolling-back">Deployment lifecycle</a>.</p>
<h2 id="windows">Windows</h2>
<p>The install script and the static binaries target Linux and macOS. On Windows, run the PHAR with PHP 8.4+. The <code>local</code> transport is not available there; deploying to remote servers over SSH works.</p>
<h2 id="getting-help">Getting help</h2>
<ul>
<li>Browse the <a href="https://github.com/automatephp/automate/issues" rel="noopener noreferrer">existing issues</a>, or open a new one with your configuration (without secrets) and the output of the failing command.</li>
<li>Pull requests are welcome: see the <a href="https://github.com/automatephp/automate" rel="noopener noreferrer">repository</a>.</li>
</ul>]]>
    </content>
    <link href="https://automate-deployer.com/docs/troubleshooting/" rel="alternate" type="text/html" />
    <published>2026-10-30T11:10:35+00:00</published>
    <updated>2026-10-30T11:10:35+00:00</updated>
  </entry>
  <entry xml:lang="en">
    <id>https://automate-deployer.com/docs/variables/</id>
    <title>Variables</title>
    <content type="html">
      <![CDATA[<p>A <code>%name%</code> token can be used in <strong>any string value</strong> of the configuration: a <code>password</code>, a <code>host</code>, a <code>path</code>, a hook command. It is replaced by the value of the variable when the command starts. A name matches <code>[a-z][a-z0-9_]*</code>.</p>
<pre><code class="language-yaml">platforms:
    review:
        default_branch: main
        servers:
            vm:
                host: review.acme.io
                user: deploy
                password: "%review_password%"
                path: /srv/review/%slug%

post_deploy:
    - bash deploy/notify.sh   # reads $AUTOMATE__slug and $AUTOMATE_RELEASE_PATH</code></pre>
<h2 id="resolution-order">Resolution order</h2>
<p>Values are looked up in this order:</p>
<ol>
<li>the <code>--var name=value</code> option of <code>deploy</code>, <code>rollback</code>, <code>check</code>, <code>remove</code> and <code>exec</code>, which can be repeated:
<code>automate deploy review my-branch --var slug=my-branch</code></li>
<li>the environment variable <code>AUTOMATE__name</code>, for example <code>AUTOMATE__review_password</code>;</li>
<li>for a <code>password</code> only, an <strong>interactive prompt</strong> in the terminal.</li>
</ol>
<p>A variable that cannot be resolved stops the command with an error naming the configuration key and the ways to provide the value. Only the platform being deployed is resolved: the variables of the other platforms are not needed.</p>
<blockquote>
<p><strong>Passwords:</strong> the prompt is only offered when the whole <code>password</code> value is a token, such as <code>"%prod_password%"</code>. Any other password is used as is. A name used as a password is accepted in any case, such as <code>%PROD_PASSWORD%</code>, for compatibility with older configurations.</p>
</blockquote>
<h2 id="in-ci-pipelines">In CI pipelines</h2>
<p>Pipelines have no terminal, so passwords must come from the environment or from <code>--var</code>. Map your CI secrets to <code>AUTOMATE__*</code> variables:</p>
<pre><code class="language-yaml"># GitHub Actions
env:
    AUTOMATE__prod_password: ${{ secrets.PROD_PASSWORD }}</code></pre>
<p>See <a href="/docs/ci-integration/">CI integration</a> for complete examples.</p>
<h2 id="masking">Masking</h2>
<p>The deploy summary lists the <strong>names</strong> of the variables, never their values. In the commands printed to the console, each value is replaced by its <code>%name%</code> token. The values are still exported to the hooks as <code>AUTOMATE__name</code> environment variables, except the ones used as a <code>password</code>.</p>
<h2 id="escaping-and-quoting">Escaping and quoting</h2>
<ul>
<li>Write <code>%%</code> to get a literal <code>%</code>. Commands that contain a valid token are the usual trap: in <code>date +%Y%m%d</code>, <code>%m%</code> is a token, and in <code>printf '%s%s'</code>, <code>%s%</code> is a token. Write <code>date +%%Y%%m%%d</code> and <code>printf '%%s%%s'</code>.</li>
<li>Quote a YAML value that starts with <code>%</code>: <code>password: "%prod_password%"</code>.</li>
<li>Values are inserted <strong>as is</strong>, without shell escaping. Use trusted values, or reference the variable through the environment in a command: <code>"$AUTOMATE__slug"</code>.</li>
<li><code>--var</code> does not accept an empty value.</li>
</ul>]]>
    </content>
    <link href="https://automate-deployer.com/docs/variables/" rel="alternate" type="text/html" />
    <published>2026-10-30T11:10:35+00:00</published>
    <updated>2026-10-30T11:10:35+00:00</updated>
  </entry>
</feed>
