(*this post focuses on the usecase of using drush within a DDEV hosted docker container, i.e. running ddev drush ... command, if you are using globally installed drush a portion of this post may not apply to you.)
TLDR;#
STEP-1: Add localhost’s SSH private key to the ddev-ssh-agent container via running:
1
2
3
4
5
| > ddev auth ssh
Adding 1 SSH private key(s)...
Adding key id_rsa
Identity added: id_rsa (XXXX@YYYYYY.local)
Successfully added 1 SSH private key(s).
|
STEP-2: Create file ``$PROJECT/drush/sites/self.site.yml` with the following content
1
2
3
4
5
6
7
8
9
10
| live:
host: example.com.au
user: example
root: /home/example/public_html/web
uri: http://example.com.au
ssh:
options: "-p 2222"
local:
root: /var/www/html/web
uri: https://ddev-example-website-local.ddev.site
|
STEP-3: Trail checking Drupal status via running:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| > ddev drush @live status
Drupal version : 10.6.1
Site URI : http://example.com.au
DB driver : mysql
DB hostname : localhost
DB port : 3306
DB username : example_drupal
DB name : example_drupal
Database : Connected
Drupal bootstrap : Successful
Default theme : example_theme
Admin theme : gin
PHP binary : /opt/alt/php81/usr/bin/php
PHP config : /opt/alt/php81/etc/php.ini
PHP OS : Linux
PHP version : 8.1.33
Drush script : /home/example/public_html/vendor/bin/drush
Drush version : 12.5.3.0
Drush temp : /tmp
Drush configs : /home/example/public_html/vendor/drush/drush/drush.yml
Install profile : standard
Drupal root : /home/example/public_html
Site path : sites/default
Files, Public : sites/default/files
Files, Private : sites/default/files/private
Files, Temp : sites/default/files/private/tmp
Connection to example.com.au closed.
|
Preliminary Setup#
Part-1: Setup SSH Public Key Authentication#
generate ssh key pair cd ~/.ssh && ssh-keygen -t rsa -f ~/.ssh/id_rsa_example, copy content in ~/.ssh/id_rsa_example.pub file for later
add to target server: ssh example.com.au then cd ~/.ssh && vi authorised_keys, then paste the copied content from file ~/.ssh/id_rsa_example.pub in previous step
trial ssh key connection via ddev ssh then ssh example@example.com.au -i ~/.ssh/id_rsa_example (add -p port-number if applies to you)
Part-2: Setup $PROJECT/drush/sites/self.site.yml File#
create emptu self.site.yml file under $PROJECT/drush/sites directory
for each alias (development / stage / production) add an alias item in the self.site.yml file, for instance:
1
2
3
4
5
6
7
| + live:
+ host: example.com.au
+ user: example
+ root: /home/example/public_html/web
+ uri: http://example.com.au
+ ssh:
+ options: "-p 2222 -i ~/.ssh/id_rsa_example"
|
Note for parameters:
host : The fully-qualified domain name of the remote system hosting the Drupal instance.user : The username to log in as when using ssh or docker. If you don’t declare it here, you can declare it in the ~/.ssh/config file in DDEV container (NOT in your host machine !!!!)uri: The value of –uri should always be the same as when the site is being accessed from a web browserssh: Additional SSH options, here I’ve specified the flag and manually used certain private key for authentication
Part-3: Trial via ddev drush @alias-name status Command#
Run ddev drush @live status command, you should get something like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| Drupal version : 10.6.1
Site URI : http://example.com.au
DB driver : mysql
DB hostname : localhost
DB port : 3306
DB username : example_drupal
DB name : example_drupal
Database : Connected
Drupal bootstrap : Successful
Default theme : example_theme
Admin theme : gin
PHP binary : /opt/alt/php81/usr/bin/php
PHP config : /opt/alt/php81/etc/php.ini
PHP OS : Linux
PHP version : 8.1.33
Drush script : /home/example/public_html/vendor/bin/drush
Drush version : 12.5.3.0
Drush temp : /tmp
Drush configs : /home/example/public_html/vendor/drush/drush/drush.yml
Install profile : standard
Drupal root : /home/example/public_html
Site path : sites/default
Files, Public : sites/default/files
Files, Private : sites/default/files/private
Files, Temp : sites/default/files/private/tmp
Connection to example.com.au closed.
|
If you are encountering error, please refer to the “Common Issue” section for resolution for various error.
Part-4: (optional) Alias for @Local Environment#
If you would like the local environment to also have an alias, you can add the following to your self.site.yml file (only provide root and uri, do not specify host and user fields ):
1
2
3
4
5
6
7
8
9
10
| live:
host: example.com.au
user: example
root: /home/example/public_html/web
uri: http://example.com.au
ssh:
options: "-p 2222 -i ~/.ssh/id_rsa_example"
+ local:
+ root: /var/www/html/web
+ uri: https://ddev-example-website-local.ddev.site
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| > ddev drush @local status
Drupal version : 11.2.8
Site URI : https://ddev-example-website-local.ddev.site
DB driver : mysql
DB hostname : db
DB port : 3306
DB username : db
DB name : db
Database : Connected
Drupal bootstrap : Successful
Default theme : olivero
Admin theme : gin
PHP binary : /usr/bin/php8.4
PHP config : /etc/php/8.4/cli/php.ini
PHP OS : Linux
PHP version : 8.4.14
Drush script : /var/www/html/vendor/bin/drush.php
Drush version : 13.7.0.0
Drush temp : /tmp
Drush configs : /var/www/html/vendor/drush/drush/drush.yml
Install profile : standard
Drupal root : /var/www/html/web
Site path : sites/default
Files, Public : sites/default/files
Files, Temp : /tmp
Drupal config : sites/default/files/sync
Connection to example.com.au closed.
|
Common Usage#
Show Status#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| > ddev drush @live status
Drupal version : 10.6.1
Site URI : http://example.com.au
DB driver : mysql
DB hostname : localhost
DB port : 3306
DB username : example_drupal
DB name : example_drupal
Database : Connected
Drupal bootstrap : Successful
Default theme : example_theme
Admin theme : gin
PHP binary : /opt/alt/php81/usr/bin/php
PHP config : /opt/alt/php81/etc/php.ini
PHP OS : Linux
PHP version : 8.1.33
Drush script : /home/example/public_html/vendor/bin/drush
Drush version : 12.5.3.0
Drush temp : /tmp
Drush configs : /home/example/public_html/vendor/drush/drush/drush.yml
Install profile : standard
Drupal root : /home/example/public_html
Site path : sites/default
Files, Public : sites/default/files
Files, Private : sites/default/files/private
Files, Temp : sites/default/files/private/tmp
Connection to example.com.au closed.
|
Clear Cache / Update Database#
1
2
3
| > ddev drush @live cache:rebuild
[success] Cache rebuild complete.
Connection to example.com.au closed.
|
1
2
3
| > ddev drush @live updatedb
[success] No pending updates.
Connection to example.com.au closed.
|
Connect to Server via SSH (and Run Shell Command)#
1
2
3
4
5
6
7
| > ddev drush @live ssh
[example@newvps web]$ cd ~
[example@newvps web]$ ls -al | grep "public_html"
drwxr-x--- 17 example nobody 4096 Dec 24 09:05 public_html
lrwxrwxrwx 1 example example 11 Jan 17 2019 www -> public_html
# (then press ctrl+d to exit)
Connection to example.com.au closed.
|
1
2
3
4
| > ddev drush @live ssh "cd ~ && ls -al | grep "public_html""
drwxr-x--- 17 example nobody 4096 Dec 24 09:05 public_htm
rwxrwxrwx 1 example example 11 Jan 17 2019 www -> public_html
Connection to example.com.au closed.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| > ddev drush @live ssh "cd ~ && cd public_html && composer show --tree drupal/core"
drupal/core 10.6.1 Drupal is an open source content management platform powering millions of websites and applications.
|--asm89/stack-cors ^2.3
| |--php ^7.3|^8.0
| |--symfony/http-foundation ^5.3|^6|^7
| | |--php >=8.1
| | |--symfony/deprecation-contracts ^2.5|^3
| | | `--php >=8.1
|--symfony/yaml ^6.4
...
...
...
| |--php >=8.1
| |--symfony/deprecation-contracts ^2.5|^3
| | `--php >=8.1
| `--symfony/polyfill-ctype ^1.8
| `--php >=7.2
`--twig/twig ^3.22.0
|--php >=8.1.0
|--symfony/deprecation-contracts ^2.5|^3
| `--php >=8.1
|--symfony/polyfill-ctype ^1.8
| `--php >=7.2
`--symfony/polyfill-mbstring ^1.3
|--ext-iconv *
| `--php >=7.2
`--php >=7.2
Connection to example.com.au closed.
|
Rsync Files#
1
2
| # Rsync Drupal root from Drush alias "live" to the alias "local"
> ddev drush rsync @live @local
|
1
2
3
| # Rsync custom file path from @live to @local
# (note that you need to use relative path, tilda (~) may cause unexpected error)
> ddev drush rsync @live:../../public_html/custom_path @local:custom_path
|
1
2
3
4
| # Rsync public files from @live to @local
# (OR Rsync private files from @live to @local)
> ddev drush rsync @live:%public @stage:%public
> ddev drush rsync @live:%private @stage:%private
|
1
2
| # Rsync Drupal root from the Drush alias dev to the alias stage, excluding all .sql files and delete all files on the destination that are no longer on the source.
> ddev drush rsync @dev @stage -- --exclude=*.sql --delete
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # Synchronise (one-way) database from @source to @target
# (Equivalant to ddev drush sql-dump @source | drush @target sql-cli)
> ddev drush sql:sync @source @target
You will destroy data in db and replace with data from example.com.au/example_drupal.
┌ Do you really want to continue? ─────────────────────────────┐
│ Yes │
└──────────────────────────────────────────────────────────────┘
[notice] Starting to dump database on source.
[notice] Starting to discover temporary files directory on target.
[notice] Copying dump file from source to target.
// Copy new and override existing files at /tmp/example_drupal_20251230_010316.sql.gz. The source is
// example@example.com.au:/home/example/drush-backups/example_drupal/20251230010316/example_drupal_20251230_010316.sql.gz?: yes.
[notice] Starting to import dump file onto target database.
|
1
2
3
4
| # export database to file (@live VS @local)
> ddev drush @live ssh
mysqldump -u $username -p $database_name > file_name_database_export.sql
> ddev export-db -f "./file_name_database_export.sql.gz"
|
1
2
3
4
| # import database from file (@live VS @local)
> ddev drush @live ssh
mysql -u root -p database_name < file_name_database_export.sql
> ddev import-db --file=file_name_database_export
|
1
2
| # Export config from @prod and transfer to @stage.
> ddev drush config:pull @prod @stage
|
1
2
| # Export config from @prod and transfer to the vcs config directory of current site.
> ddev drush config:pull @prod @self --label=vcs
|
1
2
| Export config to a custom directory. Relative paths are calculated from Drupal root.
> ddev drush config:pull @prod @self:../config/sync
|
Common Issue#
Issue-1: SSH Authentication Issue when running ddev drush site-aliases#
During my trial experiment, I get held back for a long time because the SSH key authentication kept failing, claiming the private key I provided via -i ~/.ssh/id_rsa_cpanel is not found (since I am using SSH public key to establish the connection)
1
2
3
4
5
6
7
| > ddev drush @live status --debug
[preflight] Config paths: /var/www/html/vendor/drush/drush/drush.yml
...
[debug] Redispatch hook status [0.42 sec, 3.15 MB]
[info] Executing: ssh -t -p 2222 -i ~/.ssh/id_rsa example@example.com.au '/home/example/public_html/vendor/bin/drush status -vvv --uri=http://example.com.au' [0.43 sec, 3.29 MB]
Warning: Identity file /home/user_name/.ssh/id_rsa_cpanel not accessible: No such file or directory.
example@example.com.au's password: ...
|
I soon realised that since I am using ddev to run the drush command, it only have access to the files inside its own container (it has no access to the SSH keys in its host machine), hence in order for it to work, I need to copy the (already setup and working) SSH private keys in the localhost machine into the DDEV container:
copy the SSH keys into the container using ddev auth ssh command
1
2
3
4
5
6
7
8
9
10
11
12
| > cd ~/.ssh && ls
config
id_rsa
id_rsa.pub
known_hosts
known_hosts.old
> ddev auth ssh
Adding 1 SSH private key(s)...
Adding key id_rsa
Identity added: id_rsa (XXXX@YYYYYY.local)
Successfully added 1 SSH private key(s).
|
(in fact this command is not physically copying the SSH key flies, but rather adding adding SSH key authentication to the ddev-ssh-agent container, hence no private key is required) edit your self.site.yml file to remove the -i ... flag
1
2
3
4
5
6
7
8
| live:
host: example.com.au
user: example
root: /home/example/public_html/web
uri: http://example.com.au
ssh:
- options: "-p 2222 -i /path/to/ssh-key-in-host-machine" <-- unreachable from docker POV
+ options: "-p 2222"
|
(OR) Alternatively you can also generate a new SSH key and add it to the authorised_keys file in the target server:
generate ssh key pair cd ~/.ssh && ssh-keygen -t rsa -f ~/.ssh/id_rsa_example, copy content in ~/.ssh/id_rsa_example.pub file for later
add to target server: ssh example.com.au then cd ~/.ssh && vi authorised_keys, then paste the copied content from file ~/.ssh/id_rsa_example.pub in step-1
finally edit your self.site.yml file:
1
2
3
4
5
6
7
8
| live:
host: example.com.au
user: example
root: /home/example/public_html/web
uri: http://example.com.au
ssh:
- options: "-p 2222 -i /path/to/ssh-key-in-host-machine" <-- unreachable from docker POV
+ options: "-p 2222 -i ~/.ssh/id_rsa_example"
|
Issue-2: No such file or directory (Cannot find drush)#
On my trial, the drush site:alias seems to keep ignoring my last bit of the path in root attribute, for instance when I set root:/home/example/public_html, it seems to always run ssh -t example@example.com.au '/home/example/vendor/bin/drush status --uri=http://example.com.au instead:
1
2
3
4
5
6
7
| > ddev drush @live status --debug
[preflight] Config paths: /var/www/html/vendor/drush/drush/drush.yml
...
[debug] Redispatch hook status [0.27 sec, 3.15 MB]
[info] Executing: ssh -t -p 2222 -i ~/.ssh/id_rsa_cpanel example@example.com.au '/home/example/vendor/bin/drush status -vvv --uri=http://example.com.au' [0.28 sec, 3.29 MB]
bash: /home/exampleit/vendor/bin/drush: No such file or directory
Connection to example.com.au closed.
|
After some digging it is found that Drupal by default assumes your website to live inside a web folder and will escape from that folder level that contains the vendor/bin/drush file
1
2
3
4
5
6
7
8
9
10
11
| # FILE: src/Drupa Finder/DrushDrupalFinder.php
<?php
public function getDrupalRoot()
{
$core = InstalledVersions::getInstallPath('drupal/core');
return $core ? Path::canonicalize(realpath(dirname($core))) : false;
# ↑
# dirname($core) - Truncates/removes the last path segment (core), leaving the parent directory (e.g., /path/to/drupal/web)
# realpath() - Resolves any symlinks and relative paths to get the absolute path
# Path::canonicalize() - Normalizes the path format for the current OS
}
|
To accommodate this, no matter the website lives inside the web folder or not, always put trailing web in your root file path :
1
2
3
4
5
6
7
8
| live:
host: example.com.au
user: example
- root: /home/example/public_html
+ root: /home/example/public_html/web
uri: http://example.com.au
ssh:
options: "-p 2222 -i ~/.ssh/id_rsa_example"
|
(via running drush @live status and you can find the actual website root path:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| Drupal version : 10.6.1
Site URI : http://example.com.au
DB driver : mysql
DB hostname : localhost
DB port : 3306
DB username : example_drupal
DB name : example_drupal
Database : Connected
Drupal bootstrap : Successful
Default theme : example_theme
Admin theme : gin
PHP binary : /opt/alt/php81/usr/bin/php
PHP config : /opt/alt/php81/etc/php.ini
PHP OS : Linux
PHP version : 8.1.33
Drush script : /home/example/public_html/vendor/bin/drush
Drush version : 12.5.3.0
Drush temp : /tmp
Drush configs : /home/example/public_html/vendor/drush/drush/drush.yml
Install profile : standard
Drupal root : /home/example/public_html <-------[HERE]
Site path : sites/default
Files, Public : sites/default/files
Files, Private : sites/default/files/private
Files, Temp : sites/default/files/private/tmp
Connection to example.com.au closed.
|
Issue-3: rsync command not found#
When running rsync command (OR sql:sync command, that uses rsync for transferring files), if you ever see error like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| ddev drush rsync @live:~/public_html @local
┌ Copy new and override existing files at /var/www/html/web. The source is example@example.com.au:/home/example/public_html/web/… ┐
│ Yes │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
> bash: rsync: command not found
> rsync: connection unexpectedly closed (0 bytes received so far) [Receiver]
> rsync error: error in rsync protocol data stream (code 12) at io.c(232) [Receiver=3.2.7]
In RsyncCommands.php line 84:
Could not rsync from example@example.com.au:/home/example/public_html/web/~/public_html to /var/www/html/web
Failed to run drush rsync @live:~/public_html @local: exit status 1
|
This is likely caused by one of your alias (either @live or @local in this instance) does not have the rsync command line utility installed, you can verify this via running:
1
2
3
4
| > ddev drush @local ssh "which rsync"
/usr/bin/rsync
> ddev drush @live ssh "which rsync"
/usr/bin/which: no rsync in (/usr/local/cpanel/3rdparty/lib/path-bin:/usr/share/Modules/bin:/usr/local/cpanel/3rdparty/lib/path-bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin)
|
Reference#
Drush Documentation - site:aliases: link
Drush Documentation - site:ssh: link
Drush Documentation - sql:sync: link
- so you can synchronise
local/stage/production databases via drush sql:sync @source @target (it uses rsync to transfer the files between different environment) - (equivalent to running
drush sql-dump @source | drush @target sql-cli)
Drush Documentation - core:rsync: link
- so you can synchronise
local/stage/production files via drush rsync @source @target
Drush Documentation- config:pull: link