Automate

Recipes

Ready-to-adapt configurations for Symfony, Laravel, static sites, Docker Compose, multi-server platforms and more.

Starting points for common setups. Every example is a complete .automate.yml; adapt the hosts, paths and credentials.

Serving the current release with nginx or Caddy

Point the document root at <path>/current/public, and make the web server resolve the symbolic link 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.

With nginx, use $realpath_root instead of $document_root:

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;
    }
}

Caddy resolves the link by default in php_fastcgi (resolve_root_symlink is enabled unless you turn it off):

www.acme.io {
    root * /var/www/shop/current/public
    php_fastcgi unix//run/php/php8.4-fpm.sock
    file_server
}

Symfony

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]

Point the web server at /var/www/shop/current/public as shown above. var/cache stays inside each release, which is what Symfony expects: a release always starts with a cache built for its own code.

Laravel

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

The web server serves /var/www/app/current/public. The storage folder is shared, so logs, sessions and uploads survive deployments. php artisan storage:link can be added to on_deploy when you use the public disk.

Static site built in CI

The servers only receive files. Build in the pipeline, then upload the output:

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

Serve /var/www/site/current/dist. Since there is no repository, nothing needs to be installed on the server, not even git.

Assets built in CI, PHP built on the server

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.

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

Docker Compose on the host

Deploy the sources, then rebuild and restart the containers from the published release. The local transport runs everything on the machine itself, which suits a runner installed on the host.

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

on_remove stops the stack before automate remove deletes the files.

Several servers behind a load balancer

Run the build everywhere, the migration once, and restart each server:

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

Uploads shared by all servers, such as user files, belong on a network mount declared with shared_path:

            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

Staging and production in one file

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
automate deploy staging              # deploys "develop"
automate deploy production v2.3.0    # deploys a tag