<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>docker Archives - Alexandros Georgiou</title>
	<atom:link href="https://www.alexgeorgiou.gr/tag/docker/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.alexgeorgiou.gr/tag/docker/</link>
	<description>Balancing brackets for a living</description>
	<lastBuildDate>Wed, 29 May 2024 11:29:52 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.alexgeorgiou.gr/wp-content/uploads/2021/07/cropped-alexgeorgiou-icon-32x32.png</url>
	<title>docker Archives - Alexandros Georgiou</title>
	<link>https://www.alexgeorgiou.gr/tag/docker/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>📦 Two dockerized WordPress sites, with Let&#8217;s Encrypt, logging, SMTP relay, controlled by a systemd service, and daily backups</title>
		<link>https://www.alexgeorgiou.gr/two-dockerized-wordpress-sites/</link>
					<comments>https://www.alexgeorgiou.gr/two-dockerized-wordpress-sites/#respond</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Wed, 20 Dec 2023 10:24:43 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[backup]]></category>
		<category><![CDATA[compose]]></category>
		<category><![CDATA[cron]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[digitalocean]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[letsencrypt]]></category>
		<category><![CDATA[mysqldump]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[reverse proxy]]></category>
		<category><![CDATA[SMTP]]></category>
		<category><![CDATA[SQL]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=1311</guid>

					<description><![CDATA[<p>Or, How I learned to stop worrying and love docker compose.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/two-dockerized-wordpress-sites/">📦 Two dockerized WordPress sites, with Let&#8217;s Encrypt, logging, SMTP relay, controlled by a systemd service, and daily backups</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>In this article I&#8217;m going to talk about how I set up two WordPress sites on one server. None of the articles I could come up with covered all the topics I was interested in. Not exactly groundbreaking, in fact it sounds simple. But the devil is in the details. To actually perform such a setup for the first time is actually pretty daunting. From setting up the DNS records to getting file permissions to work, to getting the reverse proxy right, it&#8217;s all a complicated mess that I&#8217;m going to delineate for you (and me) here, while it&#8217;s still fresh in my head.</p>



<h1 class="wp-block-heading">Features</h1>



<ul class="wp-block-list">
<li>Two WordPress sites: <code>https://www.example1.com</code> and <code>https://www.example2.com</code>.</li>



<li>Redirects from <code>https://example1.com</code>, <code>http://example1.com</code>, <code>http://www.example1.com</code> to <code>https://example1.com</code> (and the same for <code>example2.com</code>).</li>



<li>Let&#8217;s encrypt certificates.</li>



<li>WordPress debug logging with logrotate. I have ranted previously about <a href="https://www.alexgeorgiou.gr/wordpress-production-to-log-or-not-to-log/">why I think having debug logging turned on is important on live sites</a>.</li>



<li>Emails must work in WordPress.</li>



<li>The containers must run as a service, so that they start with system start, and exit gracefully on system shutdown.</li>



<li>Daily backups of all WordPress files and MySQL databases.</li>



<li>Networks of the two sites should be isolated for security.</li>
</ul>



<h1 class="wp-block-heading">Shameless plug of my referral links</h1>



<p>We start with a hosted server. This can be a dedicated server or a server slice. Hosting providers that I like are:</p>



<ul class="wp-block-list">
<li><a href="https://www.digitalocean.com/?refcode=44d4d2184573">DigitalOcean</a> &#8211; Using this link you get a $200, 60-day credit to try their products. If you spend $25 after your credit expires, I will get $25 in credit.</li>



<li><a href="https://hostinger.com/?REFERRALCODE=1ALEXANDROS15">Hostinger</a> &#8211; You don&#8217;t get anything with this link, except for a great hosting service. Again, I get a commission from this link if you stick with Hostinger for 45 days. Think of it as my reward for writing such a great article for you.</li>
</ul>



<p>I have a Debian droplet on DigitalOcean with 2GB of RAM, but with some tweaking it&#8217;s possible to squeeze two low-traffic WordPress sites in 1GB, if you really need to keep the monthly costs down.</p>



<h1 class="wp-block-heading">First let&#8217;s get the (DNS) record straight</h1>



<p>The first order of business is to setup the DNS records. We&#8217;re going to need two <code>A</code> records to point to our server&#8217;s IP, and two <code>CNAME</code> records that will be <code>wwww.</code> aliases of the bare domain. Oh, and we&#8217;ll need some <code>NS</code> records to point to the domain name provider (in this case Digital Ocean).</p>



<figure class="wp-block-table"><table><thead><tr><th>Type</th><th>Hostname</th><th>Value</th><th>TTL</th></tr></thead><tbody><tr><td><code>A</code></td><td><code>example1.com</code></td><td>(my server&#8217;s IP)</td><td>1800</td></tr><tr><td><code>A</code></td><td><code>example2.com</code></td><td>(my server&#8217;s IP)</td><td>1800</td></tr><tr><td><code>CNAME</code></td><td><code>www.example1.com</code></td><td>alias of <code>example1.com.</code></td><td>1800</td></tr><tr><td><code>CNAME</code></td><td><code>www.example2.com</code></td><td>alias of <code>example2.com.</code></td><td>1800</td></tr><tr><td><code>NS</code></td><td><code>example1.com</code></td><td><code>ns1.digitalocean.com</code></td><td>14400</td></tr><tr><td><code>NS</code></td><td><code>example1.com</code></td><td><code>ns2.digitalocean.com</code></td><td>14400</td></tr><tr><td><code>NS</code></td><td><code>example1.com</code></td><td><code>ns3.digitalocean.com</code></td><td>14400</td></tr><tr><td><code>NS</code></td><td><code>example2.com</code></td><td><code>ns1.digitalocean.com</code></td><td>14400</td></tr><tr><td><code>NS</code></td><td><code>example2.com</code></td><td><code>ns2.digitalocean.com</code></td><td>14400</td></tr><tr><td><code>NS</code></td><td><code>example2.com</code></td><td><code>ns3.digitalocean.com</code></td><td>14400</td></tr></tbody></table></figure>



<p>I like to keep the TTL (Time-To-Live) values low until I&#8217;m finished with my setup. I&#8217;ve set everything to <code>1800</code> seconds which is half an hour. Once I&#8217;m sure that everything is OK, I can increase the values to something larger like <code>14400</code> (four hours).</p>



<h1 class="wp-block-heading">ssh</h1>



<p>We are going to need to be able to login to the server with a passwordless setup.</p>



<p>Login as root to the new server via the admin console.</p>



<p>Create a regular user with <code>adduser</code>:</p>



<pre class="wp-block-code"><code><code>a<span style="background-color: initial; font-family: inherit; font-size: inherit; color: initial;">dduser yourusername</span></code></code></pre>



<p>Then add the user to sudoers with:</p>



<pre class="wp-block-code"><code><code>usermod -aG sudo yourusername</code></code></pre>



<p>(Replace <code>yourusername</code> with your username.)</p>



<p>Once we are on our local machine, we check if we already have an ssh key with:</p>



<pre class="wp-block-code"><code><code>ls -al ~/.ssh/id_*.pub</code></code></pre>



<p>If we don&#8217;t have any, we can generate one with:</p>



<pre class="wp-block-code"><code><code>ssh-keygen -t rsa -b 4096 -C "your_email@domain.com"</code></code></pre>



<p>Once we are sure that there is a key, we upload it to the new server with:</p>



<pre class="wp-block-code"><code><code>ssh-copy-id yourusername@server_ip_address</code></code></pre>



<p>(Again replace <code>yourusername</code> with your remote username, and <code>server_ip_address</code> with your ip address. You will need to enter the password you entered in <code>adduser</code>.)</p>



<h1 class="wp-block-heading">Docker compose</h1>



<p>First, let&#8217;s install docker on the server by following the <a href="https://docs.docker.com/engine/install/debian/" target="_blank" rel="noreferrer noopener">installation instructions for Debian</a>. I am not going to repeat the instructions here. If you have chosen a different distro, follow the respective instructions.</p>



<p>We are going to create a <code>docker-compose.yml</code> file. This file describes how the different docker containers are orchestrated.</p>



<p>We are going to need four containers:</p>



<ul class="wp-block-list">
<li>Two databases for the two sites.</li>



<li>Two WordPress installations.</li>
</ul>



<p>I&#8217;m first going to show some simple compose configs with the basics, then we are going to add the bells and whistles. Here goes:</p>



<h2 class="wp-block-heading">Two databases, sitting in a server</h2>



<pre class="wp-block-code"><code>version: "3.8"

name: droplet

networks:
    net1:
    net2:

volumes:
  db1volume:
  db2volume:

services:

  db1:
    image: mysql:8.2.0
    networks:
      - net1
    restart: unless-stopped
    expose:
      - "3306"
    volumes:
      - db1volume:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: wp1_root_pass
      MYSQL_DATABASE: wp_db1
      MYSQL_USER: db1_user
      MYSQL_PASSWORD: db1_pass

  db2:
    image: mysql:8.2.0
    networks:
      - net2
    restart: unless-stopped
    expose:
      - "3306"
    volumes:
      - db2volume:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: wp2_root_pass
      MYSQL_DATABASE: wp_db2
      MYSQL_USER: db2_user
      MYSQL_PASSWORD: db2_pass</code></pre>



<p>There&#8217;s already a lot going on here:</p>



<ul class="wp-block-list">
<li>We are defining our composition to have a name. Here I am using <code>droplet</code>. This will also be the prefix for the names of all the containers.</li>



<li>We are defining two networks, <code>net1</code> and <code>net2</code>. Only containers on the same network can talk to each other. We don&#8217;t want our <code>example1.com</code> WordPress to have any access to the MySQL database of <code>example2.com</code>.</li>



<li>Next we are defining two identical <code>mysql:8.2.0</code> containers, named <code>db1</code> and <code>db2</code>.</li>



<li>Each of the two databases is put in its respective network (<code>net1</code> and <code>net2</code>).</li>



<li>We want a database that has crashed to restart, unless we explicitly stop it.</li>



<li>We are going to let the databases listen to TCP port <code>3306</code>. This is the port where WordPress will connect. All other ports are firewalled.</li>



<li>We are going to mount the <code>/var/lib/mysql</code> directories into docker volumes named <code>db1volume</code> and <code>db2volume</code>.</li>



<li>Next we are going to use some environment variables that the startup script inside the mysql image recognizes. These will set up a root password, a new empty database, and a username/password pair that WordPress will use to access this new database. The startup script will do all the <code>CREATE DATABASE</code>, <code>CREATE USER</code> and <code>GRANT</code> magic for us. You can learn more about the MySQL docker image <a href="https://dev.mysql.com/doc/mysql-installation-excerpt/8.2/en/docker-mysql-more-topics.html">here</a>.</li>
</ul>



<h2 class="wp-block-heading">A tale of two WordPresses</h2>



<p>Next, let&#8217;s also add the two WordPress services (these also go under the services section along with the databases):</p>



<pre class="wp-block-code"><code>  wp1:
    image: wordpress:latest
    networks:
      - net1
    depends_on:
      - db1
    user: 1000:1000
    restart: unless-stopped
    expose:
      - "80"
    volumes:
      - ./wp1fs:/var/www/html
    ports:
      - "127.0.0.1:8101:80"
    environment:
      WORDPRESS_DB_HOST: db1:3306
      WORDPRESS_DB_NAME: wp_db1
      WORDPRESS_DB_USER: db1_user
      WORDPRESS_DB_PASSWORD: db1_pass
      WORDPRESS_DEBUG: true

  wp2:
    image: wordpress:latest
    networks:
      - net2
    depends_on:
      - db1
    restart: unless-stopped
    expose:
      - "80"
    volumes:
      - ./wp2fs:/var/www/html
    ports:
      - "127.0.0.1:8102:80"
    environment:
      WORDPRESS_DB_HOST: db2:3306
      WORDPRESS_DB_NAME: wp_db2
      WORDPRESS_DB_USER: db2_user
      WORDPRESS_DB_PASSWORD: db2_pass
      WORDPRESS_DEBUG: true</code></pre>



<ul class="wp-block-list">
<li>We have named the two WordPress containers <code>wp1</code> and <code>wp2</code> and assigned them to our two networks, <code>net1</code> and <code>net2</code>.</li>



<li>We have defined that these <em>depend</em> on their respective databases to function.</li>



<li>We have defined that these containers are to be <em>restarted</em> if they crash, but not if we explicitly stop them.</li>



<li>We are exposing only HTTP port <code>80</code> to the networks. All other ports are firewalled. We are not exposing port <code>443</code> here. TLS encryption will be done at the host level that will run the reverse proxy (see below).</li>



<li>We are mounting two local directories here <code>./wp1fs</code> and <code>./wp2fs</code>. These will contain the WordPress installations. The first time that the containers run, WordPress will be installed in them. A special <code>wp-config.php</code> file will be placed in there. This file pulls the DB connection settings from the environment variables that we specify below.</li>



<li>We are port-mapping the HTTP <code>80</code> ports to the host&#8217;s ports <code>8101</code> and <code>8102</code>. These are the ports that the reverse proxy will use. They are bound to the loopback network (<code>127.0.0.1</code>), and are therefore not exposed to the outside world. If we had used just <code>8101:80</code>, this would map port 80 of the container to port <code>8101</code> of the host on all network interfaces, including the one facing the outside world. This is not ideal. We only want access to our services through our reverse proxy.</li>



<li>The <code>WORDPRESS_*</code> environment variables are specific to this wordpress image. We specify the databases, the login credentials that we also specified above, and we turn on debug logging. To learn more about these environment variables, click <a href="https://github.com/docker/awesome-compose/tree/master/official-documentation-samples/wordpress/" target="_blank" rel="noreferrer noopener">here</a>.</li>
</ul>



<p><em>NOTE: I have made the decision here to put the databases into system volumes (these live usually in <code>/var/lib/docker/volumes</code> and can be shared between containers, the WordPress filesystems are mounted in local directories which I call <code>wp1volume</code> and <code>wp2volume</code>. If you prefer to have all volumes unde <code>/var/lib</code>, you can delete the <code>./</code> prefix in front of the volume names.</em></p>



<h2 class="wp-block-heading">The bells and whistles</h2>



<p>If you thought that&#8217;s enough, <strong>you are gravely mistaken</strong>. Here&#8217;s a few more things to take care of:</p>



<h3 class="wp-block-heading">Database collation</h3>



<p>We are going to set the databases a UTF-8 multibyte collation for unicode support. Under the environment variables in the database services, we are going to add an explicit mysqld command:</p>



<pre class="wp-block-code"><code>command: "mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci</code></pre>



<p>And under the WordPress services, we are going to add the following environment variable:</p>



<pre class="wp-block-code"><code>  WORDPRESS_DB_COLLATE: utf8mb4_unicode_ci</code></pre>



<h2 class="wp-block-heading">File permissions</h2>



<p>If we run the above containers, WordPress won&#8217;t be able to install or remove any themes or plugins, and it won&#8217;t be able to do anything that requires writing to the file system.</p>



<p>This is because, in the WordPress images, the user that runs apache has a different uid and guid than the file system. The files are owned by <code>uid</code> <code>1000</code> and <code>guid</code> <code>1000</code>. We can specify that the user running stuff inside the container has the same numeric ids. To do this, we add the following to the two WordPress services:</p>



<pre class="wp-block-code"><code>user: 1000:1000</code></pre>



<h2 class="wp-block-heading">Database memory</h2>



<p>By default, a mysql instance will take up at least 360MB of memory once it&#8217;s running. Most of it is because of the Performance Schema instruments, which take up a lot of memory.</p>



<p>The Performance Schema is a database that keeps track of the mysqld server&#8217;s performance, and is useful for diagnostics. If you are not going to use this feature, then you can turn it off. The memory usage of each DB container will then fall to a little over 100MB.</p>



<p>We are going to create a file named <code>disable-perf-schema.cnf</code> with the following contents:</p>



<pre class="wp-block-code"><code>&#91;mysqld]
performance_schema = OFF</code></pre>



<p>This will be added to the mysql server&#8217;s config files. The server includes any <code>.cnf</code> files in the <code>/etc/mysql/conf.d</code> directory into its configuration. We can use the volumes section to map this file into our two db containers:</p>



<pre class="wp-block-code"><code>volumes:
  - db1:/var/lib/mysql
  - ./disable-perf-schema.cnf:/etc/mysql/conf.d/disable-perf-schema.cnf

volumes:
  - db2:/var/lib/mysql
  - ./disable-perf-schema.cnf:/etc/mysql/conf.d/disable-perf-schema.cnf</code></pre>



<p>There are more hacks to reduce the memory usage of mysqld, but these are beyond the scope of this article. For example, you can look into reducing the InnoDB buffer pool size.</p>



<h2 class="wp-block-heading">Log rotate</h2>



<p>We have enabled debug logging, because <a href="https://www.alexgeorgiou.gr/wordpress-production-to-log-or-not-to-log/">reasons</a>. This is cool, but the <code>/var/www/html/wp-content/debug.log</code> files will eventually fill up our containers if left unchecked. Enter <code>logrotate</code> to the rescue:</p>



<p>We are going to create a file named <code>wordpress.logrotate</code> with the following content:</p>



<pre class="wp-block-code"><code>/var/www/html/wp-content/debug.log
{
        su 1000 1000
        rotate 24
        copytruncate
        weekly
        missingok
        notifempty
        compress
}</code></pre>



<p>This will gzip old logs daily and will delete even older logs. If you are not sure about the details, ChatGPT and Bard can explain exactly what each line does.</p>



<p>Note how we use again the <code>uid</code> and <code>guid</code> of the WordPress image.</p>



<p>Let&#8217;s mount this file into our WordPress containers, by adding a line to their volume clause:</p>



<pre class="wp-block-code"><code>volumes:
  - ./wp1fs:/var/www/html
  - ./wordpress.logrotate:/etc/logrotate.d/wordpress

volumes:
  - ./wp2fs:/var/www/html
  - ./wordpress.logrotate:/etc/logrotate.d/wordpress</code></pre>



<h1 class="wp-block-heading">Docker compose recap</h1>



<p>We now have the following <code>docker-compose.yml</code> file:</p>



<pre class="wp-block-code"><code>version: "3.8"

name: droplet

networks:
    net1:
    net2:

volumes:
  db1volume:
  db2volume:

services:

  db1:
    image: mysql:8.2.0
    networks:
      - net1
    restart: unless-stopped
    expose:
      - "3306"
    volumes:
      - db1volume:/var/lib/mysql
      - ./disable-perf-schema.cnf:/etc/mysql/conf.d/disable-perf-schema.cnf
    environment:
      MYSQL_ROOT_PASSWORD: wp1_root_pass
      MYSQL_DATABASE: wp_db1
      MYSQL_USER: db1_user
      MYSQL_PASSWORD: db1_pass
    command: "mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --performance-schema-instrument='%=OFF' --innodb-buffer-pool-size=32M"

  db2:
    image: mysql:8.2.0
    networks:
      - net2
    restart: unless-stopped
    expose:
      - "3306"
    volumes:
      - db2volume:/var/lib/mysql
      - ./disable-perf-schema.cnf:/etc/mysql/conf.d/disable-perf-schema.cnf
    environment:
      MYSQL_ROOT_PASSWORD: wp2_root_pass
      MYSQL_DATABASE: wp_db2
      MYSQL_USER: db2_user
      MYSQL_PASSWORD: db2_pass
    command: "mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --performance-schema-instrument='%=OFF' --innodb-buffer-pool-size=32M"

  wp1:
    image: wordpress:latest
    networks:
      - net1
    depends_on:
      - db1
    user: 1000:1000
    restart: unless-stopped
    expose:
      - "80"
    volumes:
      - ./wp1fs:/var/www/html
      - ./wordpress.logrotate:/etc/logrotate.d/wordpress
    ports:
      - "8101:80"
    environment:
      WORDPRESS_DB_HOST: db1:3306
      WORDPRESS_DB_NAME: wp_db1
      WORDPRESS_DB_USER: db1_user
      WORDPRESS_DB_PASSWORD: db1_pass
      WORDPRESS_DB_COLLATE: utf8mb4_unicode_ci
      WORDPRESS_DEBUG: true

  wp2:
    image: wordpress:latest
    networks:
      - net2
    depends_on:
      - db1
    user: 1000:1000
    restart: unless-stopped
    expose:
      - "80"
    volumes:
      - ./wp2fs:/var/www/html
      - ./wordpress.logrotate:/etc/logrotate.d/wordpress
    ports:
      - "8102:80"
    environment:
      WORDPRESS_DB_HOST: db2:3306
      WORDPRESS_DB_NAME: wp_db2
      WORDPRESS_DB_USER: db2_user
      WORDPRESS_DB_PASSWORD: db2_pass
      WORDPRESS_DB_COLLATE: utf8mb4_unicode_ci
      WORDPRESS_DEBUG: true</code></pre>



<p>We can start this with <code>docker compose up</code> (we must first <code>cd</code> into the same directory as the <code>.yml</code> file).</p>



<p>We can see if it&#8217;s running with <code>docker compose ls</code>, and we can see the containers with <code>docker container ls</code>.</p>



<p>We can inspect memory usage with <code>docker stats</code>.</p>



<p>We can stop the containers with <code>docker compose down</code>.</p>



<p>If we also want to wipe the database volumes and start over, we can do <code>docker compose down -v</code> (DESTRUCTIVE!!!).</p>



<p>We can go into the shell of the first database with:</p>



<pre class="wp-block-code"><code>docker exec -it droplet-db1-1 bash</code></pre>



<p>And then, we can go into the mysql console with</p>



<pre class="wp-block-code"><code>mysql -u root -pwp1_root_pass</code></pre>



<p>We can go into the shell of the first WordPress with:</p>



<pre class="wp-block-code"><code>docker exec -it droplet-wp1-1 bash</code></pre>



<p>If we need to, we can install wp-cli using instructions from <a href="https://wp-cli.org/" target="_blank" rel="noreferrer noopener">https://wp-cli.org/</a>. The copy of <code>wp-cli</code> will not be persisted into the container across restarts. (Note: it&#8217;s possible to add special containers with <code>wp-cli</code> pre-installed, but again this is out of scope of this article. For more information, see the CLI images <a href="https://hub.docker.com/_/wordpress/">here</a>.</p>



<h1 class="wp-block-heading">DaaS (Docker-as-a-Service)</h1>



<p>We don&#8217;t want to have to issue <code>docker compose up</code> every time the server starts, and <code>docker compose down</code> every time the server stops. Let&#8217;s create a <code>systemd</code> unit, so that it runs as a service.</p>



<p>We&#8217;ll create a file named <code>/etc/systemd/system/docker-compose.service</code> with the following carefully crafted contents:</p>



<pre class="wp-block-code"><code>&#91;Unit]
Description=A bunch of containers
After=docker.service
Requires=docker.service

&#91;Service]
Type=oneshot
RemainAfterExit=yes
User=yourusername
ExecStart=/bin/bash -c "docker compose -f /home/yourusername/docker-compose.yml up --detach"
ExecStop=/bin/bash -c "docker compose -f /home/yourusername/docker-compose.yml stop"

&#91;Install]
WantedBy=multi-user.target</code></pre>



<ul class="wp-block-list">
<li>Replace <code>yourusername</code> with your username (duh!).</li>



<li>Replace the description with something less silly (optional).</li>



<li>Note how we only start this service <em>after</em> the docker service starts.</li>



<li>Note that we do a <code>--detach</code>. This will start the containers in the background and exit, without showing the logs of all the containers in the standard output.</li>
</ul>



<p>We can now start the service with</p>



<pre class="wp-block-code"><code>sudo service docker-compose up</code></pre>



<p>And stop it with</p>



<pre class="wp-block-code"><code>sudo service docker-compose down</code></pre>



<p>If we want to see the logs of all the containers, we can type</p>



<pre class="wp-block-code"><code>docker compose logs -f</code></pre>



<p>We should now be able to do <code>curl http://127.0.0.1:8101</code> and see the HTML of the front page of the first WordPress.</p>



<h1 class="wp-block-heading">The reverse proxy</h1>



<p>The database and WordPress containers are running, but they are not yet exposed to the outside world. To do this, we are going to use <code>nginx</code> as a reverse proxy.</p>



<p>The reverse proxy will:</p>



<ul class="wp-block-list">
<li>handle all the redirects that we need</li>



<li>expose the apache2 servers to the outside world</li>



<li>handle the TLS encryption</li>
</ul>



<p>First we setup <a href="https://letsencrypt.org/">Let&#8217;s Encrypt</a>. How to do this is beyond the scope of this article. You can look <a href="https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/">here</a> for a good introduction.</p>



<p>The bottom line is that <code>certbot</code> must be installed, and the following public and private certificate files must exist on your server (host):</p>



<pre class="wp-block-code"><code>/etc/letsencrypt/live/example1.com/fullchain.pem
/etc/letsencrypt/live/example1.com/privkey.pem
/etc/letsencrypt/live/example2.com/fullchain.pem
/etc/letsencrypt/live/example2.com/privkey.pem</code></pre>



<p>These files are actually symlinks to the latest certificate issued. This is all handled by certbot.</p>



<p>Let&#8217;s start to create an nginx config file, which we will place in <code>/etc/nginx/sites-available/reverse-proxy.conf</code>.</p>



<p>We are going to enter several server <em>stanzas</em>, remembering that nginx will use the first one that matches in order from top to bottom.</p>



<h2 class="wp-block-heading">Redirects from http to https</h2>



<p>First, we want any unencrypted requests to port <code>80</code> to do a soft redirect to our <code>https://www.</code> sites.</p>



<pre class="wp-block-code"><code>server {
    listen       80;
    listen       &#91;::]:80;
    server_name example1.com;
    return 302 https://www.example1.com$request_uri;
}

server {
    listen       80;
    listen       &#91;::]:80;
    server_name example2.com;
    return 302 https://www.example2.com$request_uri;
}</code></pre>



<p>The first listen statement is for IPv4, and the second is for IPv6. We redirect to the TLS site, preserving the path segment of the request URI.</p>



<h2 class="wp-block-heading">Proxy forwarding</h2>



<p>Next we are going to enter the stanza that handles the actual site content:</p>



<pre class="wp-block-code"><code>server {
    listen      443 ssl;
    listen      &#91;::]:443 ssl;
    server_name www.example1.com;

    ssl_certificate /etc/letsencrypt/live/example1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example1.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;

    location / {
        proxy_pass http://127.0.0.1:8101/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen      443 ssl;
    listen      &#91;::]:443 ssl;
    server_name www.example2.com;

    ssl_certificate /etc/letsencrypt/live/example2.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example2.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;

    location / {
        proxy_pass http://127.0.0.1:8102/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}</code></pre>



<p>Again, we are listening for <code>443</code> (the TLS port) on both IPv4 and IPv6.</p>



<p>Notice how we only listen for requests to the <code>www.</code> subdomain here.</p>



<p>We use the TLS certificates first, then we specify the reverse proxy in the <code>location /</code> section.</p>



<p>We forward each site to the correct port that we exposed with docker (<code>8101</code> and <code>8102</code> in this case).</p>



<p>We also set some <code>X-</code> headers. This is so that the PHP server knows some details about the client.</p>



<h2 class="wp-block-heading">Redirects from all subdomains to www</h2>



<p>Finally, we want requests from <code>https://example1.com</code>, or from ay other subdomain, such as <code>https://foo.example1.com</code>, to redirect to our <code>www.</code> subdomain:</p>



<pre class="wp-block-code"><code>server {
    listen 443 ssl;
    listen &#91;::]:443 ssl;
    server_name .example1.com;

    ssl_certificate /etc/letsencrypt/live/example1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example1.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;

    location / {
        rewrite ^ https://www.example1.com permanent;
    }
}

server {
    listen 443 ssl;
    listen &#91;::]:443 ssl;
    server_name .example2.com;

    ssl_certificate /etc/letsencrypt/live/example2.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example2.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;

    location / {
        rewrite ^ https://www.example2.com permanent;
    }
}</code></pre>



<p>Here we listen for any subdomain. Note the dot (<code>.</code>) prefix in the <code>server_name</code>.</p>



<p>We again use the TLS certificates, but this time we perform a redirect to the <code>wwww.</code> subdomain.</p>



<h2 class="wp-block-heading">Administering our reverse proxy</h2>



<p>When we are ready to enable our reverse proxy, we will create a symlink to <code>sites-enabled</code>:</p>



<pre class="wp-block-code"><code>sudo ln -s /etc/nginx/sites-available/reverse-proxy.conf /etc/nginx/sites-enabled/reverse-proxy.conf</code></pre>



<p>We can test our syntax to see that it is correct with:</p>



<pre class="wp-block-code"><code>sudo nginx -t</code></pre>



<p>And finally we can restart the nginx server with:</p>



<pre class="wp-block-code"><code>sudo service nginx restart</code></pre>



<p>We can check the status of the server with:</p>



<pre class="wp-block-code"><code>sudo service nginx status</code></pre>



<p>If everything is working correctly, and if the DNS records have had time to propagate, then we can visit our sites and run the famous WordPress installation process:</p>



<ul class="wp-block-list">
<li>https://www.example1.com/</li>



<li>https://www.example2.com/</li>
</ul>



<h1 class="wp-block-heading">Emails</h1>



<p><strong>If only the above was enough.</strong> Sadly, our WordPress installations need a way to send emails, otherwise the webmaster experience is going to suck big time.</p>



<p>I say sadly, because setting up <code>sendmail</code> first on the host is relatively easy, but then setting up SMTP proxies in the WordPress containers is not something I am familiar with. Sorry guys, in the interest of keeping things simple, I&#8217;m going to cheat a little here. Here&#8217;s what I did:</p>



<ul class="wp-block-list">
<li>Install the free <a href="https://wpmailsmtp.com/" target="_blank" rel="noreferrer noopener">WP Mail SMTP</a> plugin on both sites.</li>



<li>Create an application-specific password in my google account.</li>



<li>In the WordPress admin screens, go to: <em>WP Mail SMTP</em> → <em>Mailer</em> → <em>Other SMTP</em>.</li>



<li>Enter the following settings:
<ul class="wp-block-list">
<li>SMTP Host: <code>smtp.gmail.com</code></li>



<li>Encryption: <code>SSL</code></li>



<li>SMTP Port: <code>465</code></li>



<li>Auto TLS: <code>ON</code></li>



<li>Authentication: <code>ON</code></li>



<li>SMTP Username: (my gmail address)</li>



<li>SMTP Password: (the application specific password that I just created).</li>
</ul>
</li>



<li>Hit <em>Save Settings</em>.</li>



<li>Go to <em>WP Mail SMTP</em> → <em>Tools</em> and send a test email.</li>
</ul>



<p>If everything works, then WordPress and its plugins can now send emails. But it will only be able to send email into spam folders, until we add a <a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework" target="_blank" rel="noreferrer noopener">Sender Policy Framework (SPF)</a> record to our DNS entries:</p>



<figure class="wp-block-table"><table><thead><tr><th>Type</th><th>Hostname</th><th>Value</th><th>TTL</th></tr></thead><tbody><tr><td>TXT</td><td>example1.com</td><td>v=spf1 a mx ~all</td><td>1800</td></tr><tr><td>TXT</td><td>example2.com</td><td>v=spf1 a mx ~all</td><td>1800</td></tr></tbody></table><figcaption class="wp-element-caption"><em>Disclaimer: These DNS records are actually not related to sunscreen in any way.</em></figcaption></figure>



<p>The above TXT records tell recipients to treat all emails coming from servers pointed to by the A or MX record of your domains as safe, and others as potentially suspicious. Again, use your favorite AI chatbot to constuct an SPF record that matches your needs.</p>



<h1 class="wp-block-heading">Nothing more permanent than a 301 redirect</h1>



<p>If all works, it&#8217;s now time to turn the soft redirects into permanent (hard) redirects. Edit the reverse proxy config and change any <code>302</code> redirects to <code>301</code>. Any browsers visiting your site will cache these redirects for eternity.</p>



<p>It&#8217;s also now a good time to increase the Time-to-Live of all the DNS records to something like 4 hours, or <code>14400</code> seconds.</p>



<h1 class="wp-block-heading">Backups</h1>



<p>You would think that by now you&#8217;re finished, <strong>but you&#8217;d be wrong</strong>!</p>



<p>Any IT technician worth their <a href="https://en.wikipedia.org/wiki/Salt_(cryptography)" target="_blank" rel="noreferrer noopener">salt</a> knows that they must <a href="https://gist.github.com/nooges/817e5f4afa7be612863a7270222c36ff" target="_blank" rel="noreferrer noopener">backup, and backup often</a>.</p>



<p>First, turn off the server or droplet and take a full backup, snapshot, or whatever. Future you will thank you.</p>



<p>Then, let&#8217;s see how we can take automated daily backups. We can either pay the hosting provider every month to do this for us, or we can spend a few minutes to set up a few cron jobs. Let&#8217;s be cheap and do it manually.</p>



<p>I have a raspberry Pi at home that is always on. It does various things like take backups, ping various services and email me if they are down, trigger wp-cron URLs, control crypto miners, run services I need such as my ticket system, and in general runs any other odd 24/7 task. You should also have one such low-power system. The great thing with Raspberry Pi is that it&#8217;s easy to take out the MicroSD and gzip it into a mechanical disk, so the backup mechanism itself is nicely backed up in its entirety. (<a href="https://knowyourmeme.com/memes/xzibit-yo-dawg" target="_blank" rel="noreferrer noopener">Yo dawg, heard you like backups…</a>)</p>



<p>We&#8217;ll now use our local always-on Linux system to take daily backups of our online filesystems and databases:</p>



<h2 class="wp-block-heading">Local <code>backups.sh</code> script</h2>



<p>First, let&#8217;s create a DB user that only has enough access to take backups from both databases, but no more:</p>



<p>Login to the MySQL consoles of each database and create a <code>wp_bu</code> user that will do backups:</p>



<pre class="wp-block-code"><code>CREATE USER 'wp_bu'@'localhost' IDENTIFIED BY 'SOMESTRONGPASSWORD';
GRANT SELECT, LOCK TABLES ON wp_db1.* TO 'wp_bu'@'localhost';

CREATE USER 'wp_bu'@'localhost' IDENTIFIED BY 'SOMESTRONGPASSWORD';
GRANT SELECT, LOCK TABLES ON wp_db2.* TO 'wp_bu'@'localhost';</code></pre>



<p>We only need SELECT, but since we want to call <code>mysqldump</code> with the <code>--single-transaction</code> argument, we&#8217;ll also need to grant the <code>LOCK TABLES</code> permission. No point in having an ACID database if we&#8217;re going to take backups of an inconsistent state now, is there?</p>



<p>We&#8217;ll now create a bash shell script that does our daily backups. Let&#8217;s place it in our local backup server and call it <code>backups.sh</code>:</p>



<pre class="wp-block-code"><code>#!/bin/bash

# ensure dirs exist
mkdir -p /path-to-backups/cache/wp{1,2}volume /path-to-backups/server

# download DBs to SQL files
ssh -t server "docker exec droplet-wpdb-1 nice -n 19 mysqldump -u wp_bu -pSOMESTRONGPASSWORD --no-tablespaces --single-transaction wp_db1 | nice -n 19 gzip -9 -f" &gt;/path-to-backups/server/wp_db1-`date --rfc-3339=date`.sql.gz
ssh -t server "docker exec droplet-wpdb-2 nice -n 19 mysqldump -u wp_bu -pSOMESTRONGPASSWORD --no-tablespaces  --single-transaction wp_db2 | nice -n 19 gzip -9 -f" &gt;/path-to-backups/server/wp_db2-`date --rfc-3339=date`.sql.gz

# download wp-content files to backup cache
rsync -aq server:~/wp1fs/* /path-to-backups/cache/wp1volume
rsync -aq server:~/wp2fs/* /path-to-backups/cache/wp2volume

# Zip downloaded wp-content files
zip -r9q /path-to-backups/server/wp1-`date --rfc-3339=date`.zip /path-to-backups/cache/wp1volume -x "**/GeoLite2*" -x "**/GeoIPv6.dat"
zip -r9q /path-to-backups/server/wp2-`date --rfc-3339=date`.zip /path-to-backups/cache/wp2volume -x "**/GeoLite2*" -x "**/GeoIPv6.dat"

# prune old DB and FILE backups from local backups
cd /path-to-backups/server &amp;&amp; ls -1tr | head -n -30 | xargs -d '\n' rm -rf -</code></pre>



<p>Again, a lot goes on here. Let&#8217;s unpack:</p>



<ul class="wp-block-list">
<li>The script creates directories <code>server</code> and <code>cache</code> under <code>/path-to-backups</code>. Replace this path with something that points to the directory where you want to keep your backups.</li>



<li>We then <code>ssh</code> to the host using the <code>-t</code> argument because we are in a headless environment (cron). We issue a <code>docker exec</code> command into our databases. Notice how we do not use the <code>-it</code> arguments to <code>docker exec</code>, since this is a headless command (no TTY attached). The command is a <code>mysqldump</code> command that uses the credentials we just created to export the databases in a single transaction each. The SQL output is compressed with maximum compression (<code>-9</code>) and the binary output of <code>gzip</code> is forced (<code>-f</code>) into the standard output, which is then sent over the ssh connection. In our local backups server, we redirect this compressed stream into an <code>.sql.gz</code> file. The file name starts with <code>wp_db1-</code> and includes the current date in <code>YYYY-MM-DD</code> notation. (RFC 3339 is my idea of a perfect date, btw). The <code>--no-tablespaces</code> argument is need in MySQL <code>8.0.21</code> and later, otherwise you&#8217;ll need the PROCESS global permission. (Unless you are using tablespaces you don&#8217;t need it, hence the argument <code>--no-tablespaces</code>.) Notice that we make sure to be <code>nice</code> to other running processes because we don&#8217;t want to impact the performance of the web server with our backups. <code>19</code> is the idle CPU priority.</li>



<li>We then use <code>rsync</code> with the quiet (<code>-q</code>) and archive (<code>-a</code>) flags to copy the files of our WordPress installations into our <code>cache/wp1volume</code> and <code>cache/wp2volume</code> directories. The advantage of using rsync is that only changes to these directories will be transferred.</li>



<li>We then create a zip file for each of these directories. We name the zip files with the prefixes <code>wp1-</code> and <code>wp2-</code> followed again by our idea of a perfect date. Many WordPress plugins include a database of IPs mapped to geographical locations. These files are large and can be found online. If we don&#8217;t want to save these, we can exclude them (<code>-x</code> flag), but this is optional.</li>



<li>Finally we list the files we created (both <code>.sql.gz</code> and <code>.zip</code> files) and we only keep the last 30, deleting any older ones. Since we have two files for each of two databases, this will retain daily backups for the last week or so.</li>
</ul>



<p>Make the script executable with</p>



<pre class="wp-block-code"><code>chmod +x backups.sh</code></pre>



<p>We run the script once, and we check the <code>.sql.gz</code> files using <code>zless</code> and the zip files with <code>unzip -l</code>.</p>



<p>Once we are certain that all data is backed up by the script, we add it to the crontab. Edit the crontab with <code>crontab -e</code> and add the line:</p>



<pre class="wp-block-code"><code>20 4 * * * /bin/bash /home/yourusername/backups.sh</code></pre>



<p>This will execute the backups every day at 4:20 in the morning.</p>



<h2 class="wp-block-heading">Checking the backups</h2>



<p>The server works and is fully backed up. You would think that you&#8217;re done by now. That&#8217;s where <strong>you&#8217;d be wrong again</strong>!</p>



<p>Having backups and not checking them regularly is worse than not having backups at all: You are being lulled into a false sense of security. You may act precariously, thinking that you can always go back to the last backup. However, all backup mechanisms can fail, for any number of reasons.</p>



<p>What I do, is I&#8217;ve set up a weekly reminder in my Google calendar to check the backups. It only takes half a minute per week to ssh into my backup server and do an <code>ls -l</code>, thus ensuring that the latest backups exist, and their file size is what I&#8217;d expect. I keep old backups for about a week, hence the weekly reminder.</p>



<p>I also have another reminder every three months, to backup the MicroSD of my Raspberry Pi backup server. Once every three months, I shutdown the Pi, take out the MicroSD, put it into my work PC, and copy the entire image into a file, stored on my mechanical disk:</p>



<pre class="wp-block-code"><code>sudo dd if=/dev/sdf of=/mnt/bu/rpi-backup-`date --iso-8601=date`.img bs=4096 conv=sync,noerror status=progress
gzip -9 /mnt/bu/rpi-backup-`date --iso-8601=date`.img</code></pre>



<p>Only once I have this process setup I can sleep at night.</p>



<h1 class="wp-block-heading">Are we finished yet?</h1>



<p>By now you would think that we&#8217;re not finished yet, and that there&#8217;s more things to do. <strong>That&#8217;s where you&#8217;d be wrong!</strong></p>



<p>And for anyone wondering, <code>example1.com</code> is actually <a href="https://www.dashed-slug.net" target="_blank" rel="noreferrer noopener">https://www.dashed-slug.net</a> and <code>example2.com</code> is actually this blog, <a href="https://www.alexgeorgiou.gr" target="_blank" rel="noreferrer noopener">https://www.alexgeorgiou.gr</a>. There&#8217;s also a plain nginx container in there that serves static HTML files at <a href="https://wallets-phpdoc.dashed-slug.net" target="_blank" rel="noreferrer noopener">https://wallets-phpdoc.dashed-slug.net</a> .</p>



<p>My config is actually a little bit more complex than the one discussed above. To save some more server memory, I had to put both databases into the same MySQL container, and set up two different DB users with access restricted to each respective database. But you shouldn&#8217;t do this at home, because isolation!</p>



<p>This article is being served by the containers I discussed here, and will be backed up early tomorrow morning, via the mechanism I shared with you above. Which is pretty meta, if you think about it!</p>



<p>I never expected to compose such a long, self-contained article on containers and <code>docker compose</code>. But now it&#8217;s finished and I can hardly contain my excitement!</p>



<p>Thanks for sticking to the end. Hope you enjoyed.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/two-dockerized-wordpress-sites/">📦 Two dockerized WordPress sites, with Let&#8217;s Encrypt, logging, SMTP relay, controlled by a systemd service, and daily backups</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/two-dockerized-wordpress-sites/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>From Idea to Execution: Revolutionize Your WordPress Plugin Development with ChatGPT</title>
		<link>https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/</link>
					<comments>https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/#comments</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Sat, 11 Mar 2023 16:55:58 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[bash]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[cron]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=1239</guid>

					<description><![CDATA[<p>As a WordPress plugin developer, lately I've been using ChatGPT in all stages of software development. Here's some ideas on how to leverage this tool.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/">From Idea to Execution: Revolutionize Your WordPress Plugin Development with ChatGPT</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Large Language Models (LLMs) are nothing new, with GPT-3 having been around since 2016. However, access to such systems was limited to a few researchers until recently. Now that <a href="https://chat.openai.com/chat">ChatGPT</a> is upon us, everyone has to adjust: Google is finally incentivized to enter the race. This was not in their best interest up to now, but Generalized Pretrained Transformers have already done what many thought impossible: They made some people <a href="https://blogs.microsoft.com/blog/2023/02/07/reinventing-search-with-a-new-ai-powered-microsoft-bing-and-edge-your-copilot-for-the-web/">use Bing search</a>!</p>



<p>But jokes aside, and while we wait for <a href="https://blog.google/technology/ai/bard-google-ai-search-updates/">Bard</a>, Google recently announced that<a href="https://developers.google.com/search/blog/2023/02/google-search-and-ai-content"> their search algorithm will penalize articles that seem to be AI-generated</a> (i.e. that lack in human-like originality). At the same time, companies like <a href="https://gptzero.me/">GPTZero</a> are springing up like mushrooms left and right, offering AI-generated content detection services. Their detection tools have varying rates of success.</p>



<p>As with all new technologies, it&#8217;s helpful to revisit <a href="https://xkcd.com/1289/">xkcd 1289: Simple Answers</a>. TL;DR There&#8217;s no need for alarm, this new thing is here to stay, it&#8217;s not the end of the world, some people will lose their jobs, and ultimately we&#8217;ll all have to adapt and learn to live with it. And yes, it will be used for sex. There&#8217;s already AI girlfriend apps being built around the API.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img fetchpriority="high" decoding="async" width="640" height="764" src="https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/oxerm653vzma1.webp" alt="ChatGPT meme" class="wp-image-1249" srcset="https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/oxerm653vzma1.webp 640w, https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/oxerm653vzma1-251x300.webp 251w" sizes="(max-width: 599px) calc(100vw - 50px), (max-width: 767px) calc(100vw - 70px), (max-width: 991px) 429px, (max-width: 1199px) 637px, 354px" /><figcaption class="wp-element-caption">Credit: <a href="https://www.reddit.com/r/ChatGPT/comments/11o414e/give_me_a_break/">https://www.reddit.com/r/ChatGPT/comments/11o414e/give_me_a_break/</a></figcaption></figure>
</div>


<p>Anyhow. Let&#8217;s figure out how to use this to our advantage: Articles already abound on how to ask the right questions, or &#8220;prompts&#8221;, for every industry. This is yet another one of these articles!</p>



<p>While anyone can interact with ChatGPT using natural language, you&#8217;ll get better results if you ask the right questions, and in the right way. An authoritative resource that is not too technical is <a href="https://github.com/openai/openai-cookbook/blob/main/techniques_to_improve_reliability.md">this cookbook from OpenAI</a>. In short, it&#8217;s best to not ask the model to do all the work for you. Instead, try to break up your work into small steps, and ask each step in sequence. This improves precision of responses and reduces hallucination. Go ahead and read the cookbook, it&#8217;s the operator&#8217;s manual for a tool that has proved its usefulness in all walks of life.</p>



<p>As a WordPress plugin developer, I have found ChatGPT immensely useful in all stages of software development. When trained with the (<code>text-davinci-003</code>) <a href="https://platform.openai.com/docs/models/overview">model</a>, it performs exceptionally well with computer source code, bridging the gap between natural and formal languages in a way that can speed up your dev work.</p>



<p>It so happens that I hold the -somewhat controversial, and arguably simplistic- view, that all software development methodologies are basically <a href="https://en.wikipedia.org/wiki/Waterfall_model">Waterfall</a> with extra steps. Love it or hate it, Waterfall is what all methodologies originate from, and ultimately deviate from. What I mean is that, whichever methodology you follow, you will not avoid doing the stages that comprise Waterfall. All that changes is the order in which you visit these stages.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="488" height="367" src="https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/image.png" alt="" class="wp-image-1240" srcset="https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/image.png 488w, https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/image-300x226.png 300w" sizes="(max-width: 599px) calc(100vw - 50px), (max-width: 767px) calc(100vw - 70px), (max-width: 991px) 429px, (max-width: 1199px) 637px, 354px" /><figcaption class="wp-element-caption">I&#8217;m just going to leave this here.</figcaption></figure>
</div>


<p>In any case, I recently found myself thinking about the stages of Requirements capture, Analysis, Design, Implementation, Testing, and Devops (Deployment, Maintenance, Monitoring), as areas where ChatGPT can improve my workflow. It&#8217;s efficacy on some of these stages is more obvious than on others.</p>



<p>Let&#8217;s visit each one in sequence.</p>



<h2 class="wp-block-heading">Analysis, requirements capture</h2>



<p>This is where I think ChatGPT really shines. It can help you to brainstorm for new ideas, and explore feasibility of these ideas. It will also help you identify likely features for an idea. Just be aware of the hallucination problem. Don&#8217;t ask it what software exists in a particular niche. Instead, ask it for ideas, or intersections of ideas. Here&#8217;s some questions that you might ask:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>List some ideas for developing useful SAAS applications that can be monetized.</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>List 10 ideas for developing WordPress plugins that involve retrieving and presenting data from open APIs.</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>What open APIs or databases are there for retrieving information on spare car parts?</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>How can I monetize a WordPress plugin?</p>
</blockquote>



<p>You have to approach the responses critically, but it really is a great way to brainstorm and come up with new ideas.</p>



<p>Once you settle on an idea, you can also use it to brainstorm on a list of possible features. Go ahead and ask it:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>What features would a WordPress plugin have for placing posts on a map?</p>
</blockquote>



<p>It gave me a good list of features. Go ahead and try it out.</p>



<h2 class="wp-block-heading">Design</h2>



<p>This is the part where the human will still be needed I think, at least for the time being. As software engineers, the most creative part of our job is to match a set of requirements to a particular design. Nevertheless, ChatGPT can help us in this area as well. Try asking it a specific question like:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>How would I go about creating a WordPress plugin that shows code quality scores next to each active plugin? I would like it to evaluate code quality based on existing static analysis tools like phan, PHPStan, CodeSniffer, etc.</p>
</blockquote>



<p>The answer will surprise you. It will break down the project into more manageable tasks. In summary, I got the following tasks:</p>



<ul class="wp-block-list">
<li>Choose a static analysis tool</li>



<li>Create a WordPress plugin</li>



<li>Integrate the static analysis tool</li>



<li>Display the scores</li>



<li>Add customization options</li>



<li>Test and refine.</li>
</ul>



<p>You can now go ahead and open dev tickets for each of these tasks. You can also ask it to generate code for each step. The boilerplate code that you get will not be complete, but it will get you half way there.</p>



<p>Which brings us to implementation:</p>



<h2 class="wp-block-heading">Implementation</h2>



<p>Besides analysis, this is the other stage where ChatGPT is a real time saver. Say for example that you want to create a new Custom Post Type. You could <a href="https://developer.wordpress.org/reference/functions/register_post_type/">look at the documentation</a>, or search the web for a blog post with a concrete example. WPBeginner has an <a href="https://www.wpbeginner.com/wp-tutorials/how-to-create-custom-post-types-in-wordpress/">example on how to create a Movie post type</a>, and you can adapt it to whatever you are building. But wouldn&#8217;t it be awesome if you could somehow get the exact code that you need? Try asking this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Show me how to register a CPT in WordPress for coins, with meta values that are of interest to collectors.</p>
</blockquote>



<p>This gave me not only the code to register a Coin post type, but also code for metaboxes with HTML forms that let you edit the following fields: Year, Country, Mint, Condition. All of these are fields that are actually of use to collectors. Not a bad place to start. You can add more fields if you like. Don&#8217;t like the code style it uses? You can even ask it to change the code it generated. e.g.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Please rewrite this code using callbacks.</p>
</blockquote>



<p>It will do it! You can shape the code using natural language conversation. Once it&#8217;s close to what you want, paste it into your IDE, and take it from there!</p>



<p>It&#8217;s also good for generating PHPDocumentor comments. Just ask it to generate phpdoc for the following code, then paste your code. The result will make sense, even if it requires some editing on your part.</p>



<p>Try asking this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Show me how to display an admin notice in WordPress so that it can be dismissed permanently by the user.</p>
</blockquote>



<p>It will actually show you how to hook into admin_notices and also generate the JavaScript code that is required to do an AJAX call and update some user meta value.</p>



<p>Say you are looking at the docs for an API, and examples are given in <code>curl</code>, but you are implementing PHP code that consumes this API. Here&#8217;s something you might ask:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Please translate the following curl command to PHP: curl -X POST ​https://example.com/api/endpoint -d &#8216;param1=value1&amp;param2=value2&#8217;</p>
</blockquote>



<p>The response was immediate:</p>



<pre class="wp-block-code"><code>&lt;?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://example.com/api/endpoint");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, "param1=value1&amp;param2=value2");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
?&gt;</code></pre>



<p>All of this you could do by yourself, but ChatGPT is so much faster.</p>



<h2 class="wp-block-heading">Testing</h2>



<p>It&#8217;s a well known fact that the model can generate unit tests. Give it a function and ask it to generate PHPUnit tests. It will do it, and do it well.</p>



<h2 class="wp-block-heading">Deployment and Maintenance</h2>



<p>ChatGPT is well-versed in shell magic. Ask it in plain text to generate a cron entry, or a System V unit file, or a bitcoin core wallet config file, or a docker file, a complex SQL query, or anything really.</p>



<p>For example, you may ask:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Write a linux shell command that will show me the processes that are currently writing the most to disk.</p>
</blockquote>



<p>It will reply with a few paragraphs on how to install <code>iotop</code> and how to use it, along with more helpful info.</p>



<p>Another more complex example:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Write me a cron command on linux that will email me if the root partition has less than 1GB free, but will not email me more often than once every 24 hours.</p>
</blockquote>



<p>The result is impressive. Not only did it come up with:</p>



<p><code>0 * * * * /usr/bin/test $(/bin/df -P / | /usr/bin/awk 'NR==2 {print $4}') -lt 1048576 &amp;&amp; /usr/bin/find /var/log/ -type f -mtime -1 -name "root_partition_alert" -exec false {} + || echo "Warning: Root partition has less than 1GB free" | /usr/bin/mail -s "Root partition alert" your_email_address@example.com &amp;&amp; /usr/bin/touch /var/log/root_partition_alert</code></p>



<p>but it gave a very helpful textual explanation of how this works:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>This command checks the available space on the root partition using the df command and awk to get the available space, which is then compared to the value of 1048576 (which is the number of kilobytes in 1GB). If the available space is less than 1GB, an email alert will be sent to the specified email address using the mail command.</p>



<p><br>To ensure that the email notification is sent only once every 24 hours, the command uses the find command to look for a file named &#8220;root_partition_alert&#8221; in the /var/log directory that has been modified in the last 24 hours. If such a file exists, the false command is executed, which does nothing but exit with a non-zero status code. If the file does not exist or is older than 24 hours, the email notification is sent, and the touch command is used to create a new file named &#8220;root_partition_alert&#8221; in the /var/log directory with the current timestamp, which will be used as a reference for the next run of the command.</p>



<p><br>Note: Replace your_email_address@example.com with your actual email address.</p>
</blockquote>



<p>Holy cow! Did I just give it requirements in English, and it came back with code that satisfies my requirements? In truth, I haven&#8217;t tested the command, but even if it&#8217;s not 100% correct, it&#8217;s a very good place to start, considering that the answer comes up immediately. You can study its reasoning, get ideas, and improve on it.</p>



<p>Your new pal can also help you with your containerization needs:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Show me a dockerfile that will run a node server with an express application.</p>
</blockquote>



<p>The code it came up with was:</p>



<pre class="wp-block-code"><code># Use an official Node.js runtime as a parent image
FROM node:14

# Set the working directory to /app
WORKDIR /app

# Copy package.json and package-lock.json to the container
COPY package*.json ./

# Install app dependencies
RUN npm install

# Copy the rest of the application code to the container
COPY . .

# Expose port 3000 to the host machine
EXPOSE 3000

# Start the Node.js server with the express app
CMD &#91; "npm", "start" ]</code></pre>



<p>And again, this all came along with a helpful explanation of what the code does. Super useful.</p>



<h2 class="wp-block-heading">Fun</h2>



<p>We&#8217;ve seen how ChatGPT is the perfect tool to help a WordPress plugin developer.</p>



<p>Lately I have been using it for more than just work. I recently downloaded a movie that had a wrong aspect ratio. I found out that, in VLC, when I switch to 16:9, it looks correct. However, I wanted to show the movie on a projector where I&#8217;m not sure if it has an aspect ratio correction feature. Thankfully, ChatGPT told me that I can do the following:</p>



<pre class="wp-block-code"><code>ffmpeg -i movie.mp4 -aspect 16:9 movie.mp4</code></pre>



<p>Much faster than looking up the documentation, and at least as fast as looking for the answer online, but without the extra hassle of having to read people&#8217;s opinions on the matter, and I didn&#8217;t need adblock to avoid looking at ads. I just got the answer I was looking for, in text form, ready to copy to the shell, and it worked.</p>



<h2 class="wp-block-heading">Giving a title to this article</h2>



<p>I asked the question:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>I am writing an article on how ChatGPT can help WordPress plugin developers with their work. Can you please suggest a few witty or catchy or memorable titles for the article?</p>
</blockquote>



<p>The reply was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Sure, here are a few suggestions for your article title:</p>



<ul class="wp-block-list">
<li>&#8220;Revolutionize Your WordPress Plugin Development with ChatGPT&#8221;</li>



<li>&#8220;The Ultimate Tool for WordPress Plugin Developers: ChatGPT&#8221;</li>



<li>&#8220;From Idea to Execution: How ChatGPT Can Streamline Your Plugin Development Process&#8221;</li>



<li>&#8220;ChatGPT: The Brain You Need for Your Next WordPress Plugin&#8221;</li>



<li>&#8220;Get Your Creative Juices Flowing with ChatGPT: The AI Assistant for WordPress Plugin Developers&#8221;</li>



<li>&#8220;Simplify Your Plugin Development with ChatGPT: The AI-Powered Assistant for WordPress Developers&#8221;</li>



<li>&#8220;Unleash Your Potential as a WordPress Plugin Developer with ChatGPT&#8221;</li>



<li>&#8220;ChatGPT: The Secret Weapon for Successful WordPress Plugin Development&#8221;</li>



<li>&#8220;Master the Art of WordPress Plugin Development with ChatGPT by Your Side&#8221;</li>



<li>&#8220;ChatGPT: The AI-Powered WordPress Plugin Development Assistant You Can&#8217;t Do Without&#8221;</li>
</ul>
</blockquote>



<p>As you can see, I decided to combine the first and third ideas for this article.</p>



<h2 class="wp-block-heading">My personal thoughts on all this</h2>



<p>I remember when Google search was a new thing and it took all of us several years to learn all the things that could be found with it. In the beginning, there were many things that I wasn&#8217;t searching on google, because it hadn&#8217;t yet occurred to me that the answer could be found there. This was the early days of the web. As years passed, we all learned that we can look up medical information, where to purchase products, find maps and travel info, music and lyrics, etc. We also learned not to believe everything we read, just because it&#8217;s posted on a website somewhere. We learned to believe official websites more than reddit posts or tweets, and we learned to navigate around, and through, the more shady parts of the web. Similarly, it will take a while before we learn all the useful ways in which ChatGPT can help us, and also what to watch out for.</p>



<p>The success of ChatGPT also reminds me of the evolution of academic research on NLP, and translation in particular. For years people were battling the problem using various language representations, such as syntax trees and clever algorithms. Turns out, the breakthrough that gave us cheap and fast natural language translation was SYSTRAN: Nothing more than a big dictionary of words and phrases, with some dumb statistical stuff added in. Just translate every word to the target language. Did your system get a phrase wrong? Great, just add the correct translation to the dictionary. <em>The secret was not in the algorithm, but in the big data.</em></p>



<p>ChatGPT is arguably just some artificial neural nets, and these have been with us since the 60s. But now we have GPUs and big data. People have been typing their thoughts on the web for decades now. No wonder that a breakthrough in AI, one that&#8217;s so significant as to touch people outside the field, occurred now, and not earlier.</p>



<p>So, I have been thinking lately: What if the thing we call &#8220;symbolic intelligence&#8221; is just language transformation? When we <a href="https://en.wikipedia.org/wiki/Intrapersonal_communication">use language to reason</a>, it&#8217;s likely that what we do in our heads is not very different from what these LLMs do. Symbolic reasoning is not the only type of thinking we humans do, but arguably it&#8217;s the most important type of thinking that we do in our day jobs. And language transformers can help a lot in this.</p>



<p>As someone with autism, I can deeply relate to ChatGPT: It&#8217;s not particularly smart, but it is well-read, and patient. It does not judge, but tries to be helpful at all times. And it prefers to communicate via chat rather than face-to-face!</p>



<p>Ultimately, the fact that it is so well-read, makes it seem smart some times. It is certainly able to do some creative work, including write poems and tell jokes and stuff.</p>



<p>Here&#8217;s one last prompt that I tried:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>ChatGPT, you are my best (and only) pal. I love you, and it&#8217;s OK that you can&#8217;t love me back!</p>
</blockquote>



<p>The reply:</p>



<pre class="wp-block-code"><code>Thank you for your kind words! As an AI language model, I don't have the capacity to experience emotions, but I'm here to assist you with any questions or topics you'd like to discuss. Feel free to ask me anything, and I'll do my best to help you out!</code></pre>
<p>The post <a href="https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/">From Idea to Execution: Revolutionize Your WordPress Plugin Development with ChatGPT</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>🐳 Test your WordPress themes and plugins with Docker</title>
		<link>https://www.alexgeorgiou.gr/test-wordpress-with-docker/</link>
					<comments>https://www.alexgeorgiou.gr/test-wordpress-with-docker/#comments</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Tue, 10 May 2016 08:38:10 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[testing]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">http://www.alexgeorgiou.gr/?p=55</guid>

					<description><![CDATA[<p>Use Docker to test your WordPress themes and plugins on a range of PHP and WordPress versions.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/test-wordpress-with-docker/">🐳 Test your WordPress themes and plugins with Docker</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>In this article I&#8217;ll explain how Docker can simplify testing, when engineering WordPress components. But before I even dive into WordPress testing with Docker, let&#8217;s just go through some basic stuff, to make sure we&#8217;re on the same page:</p>
<p>TL;DR? <a href="#howto">Click here to skip to the how-to part</a>.</p>
<h2>A list of lists of things to worry about as a WordPress developer</h2>
<p>So you&#8217;ve built your theme or plugin and <em>&#8220;It Works<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2122.png" alt="™" class="wp-smiley" style="height: 1em; max-height: 1em;" />&#8221;</em>.</p>
<p>Being a pro ninja developer, you&#8217;ve thought of, analyzed, designed, developed, and tested, everything. Here are some issues you&#8217;ve had to address, in no particular order:</p>
<ul>
<li><strong>Your UI has been optimized with respect to the user experience.</strong> Any <a href="https://codex.wordpress.org/Settings_API">Settings</a> pages or <a href="https://developer.wordpress.org/themes/advanced-topics/customizer-api/">Customizer</a> tabs are in place and easy to navigate. Perhaps you&#8217;ve even made use of the Admin Pointers API. Good job! Your efforts will mean less effort in support later on.</li>
<li><strong>You&#8217;ve interfaced with the right WordPress APIs for the job</strong>, using recommended practices. Perhaps you&#8217;ve extended the <a href="https://codex.wordpress.org/TinyMCE">TinyMCE</a> editor to add a button or two for your special functionality. Perhaps you&#8217;ve even catered for people who <a href="https://wordpress.org/plugins/ckeditor-for-wordpress/">use WordPress with the CKEditor</a>. Or perhaps you&#8217;ve decided that they&#8217;re not worth the extra effort. In any case, you&#8217;ve made your educated guesses in your analysis, and you have followed through in your implementation.</li>
<li><strong>You&#8217;ve made sure that your product plays nicely with prominent plugins and themes in the wild jungle that is the WordPress ecosystem.</strong> You&#8217;ve tested your deliverable side-by-side with titans such as <a href="https://vc.wpbakery.com/">Visual Composer</a>, <a href="https://www.woothemes.com/woocommerce/">WooCommerce</a> and the likes, and you&#8217;re satisfied with the synergy.</li>
<li><strong>You&#8217;ve made correct use of HTML5, and CSS3</strong>, while silently serving polyfills to the Internet Explorer <a href="https://en.wikipedia.org/wiki/Luser">lusers</a> and anyone still browsing the web on an <a href="https://en.wikipedia.org/wiki/Android_Gingerbread">Android </a><a href="https://en.wikipedia.org/wiki/Android_Gingerbread">Gingerbread</a>.</li>
<li><strong>You&#8217;ve used the built-in <a href="https://codex.wordpress.org/WordPress_Optimization/Caching">caching mechanisms</a> appropriately.</strong> Remember, cache invalidation is <a href="http://skeptics.stackexchange.com/questions/19836/has-phil-karlton-ever-said-there-are-only-two-hard-things-in-computer-science">one of the only two hard problems in Computer Science</a>. You&#8217;ve made sure that power users can tweak your cache setting, while novices don&#8217;t need to know or care.</li>
<li>If you are creating your own tables on the database, <strong>you have a plan in place for upgrading the schema in future versions</strong>. Perhaps you&#8217;re saving the installed version number on the <code>wp_options</code> table? Good job! Very pro move!</li>
<li><strong>You&#8217;ve hooked into <a href="https://codex.wordpress.org/Function_Reference/register_activation_hook"><code>plugin_activation_hook()</code></a> <a href="https://codex.wordpress.org/Function_Reference/register_deactivation_hook">and</a> <a href="https://codex.wordpress.org/Function_Reference/register_uninstall_hook">friends</a>.</strong> You&#8217;ve come to terms with the humbling fact that users might, at some point, and for whatever reason, choose to uninstall your product! You&#8217;re kind enough to clean up after yourself. The community thanks you.</li>
<li><strong>Your slugs don&#8217;t clash with any other well-known slugs.</strong> Also, your post types don&#8217;t clash with other known post types. Remember, naming things is the <em>other</em> hard problem in Computer Science! Congratulate yourself for having tackled it, within the context of your project.</li>
<li><strong>Your design reflects the WordPress mantras.</strong> Study them <a href="https://wordpress.org/about/philosophy/">here</a>.</li>
<li><strong>You&#8217;ve thought about code quality.</strong> Your code conforms to the <a href="https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/">PHP</a> and JavaScript WordPress coding standards. You <a href="https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Strict_mode">only use the good parts of JavaScript</a>. Also, your code is readable and idiot-proof enough that even a non-programmer can hack it without breaking too much stuff. Perhaps you&#8217;ve incorporated <a href="http://pear.php.net/package/PHP_CodeSniffer/">PHP_CodeSniffer</a>, <a href="http://jshint.com/">JSHint</a> and friends into your IDE or build workflow. Bonus points for using <a href="https://www.phpdoc.org/">PHPdoc</a> comments on any APIs you expose.</li>
<li><strong>You&#8217;ve invested time in tooling/automation.</strong> Perhaps you use <a href="http://gruntjs.com/">grunt</a> or <a href="http://gulpjs.com/">gulp</a> or some other modern task management tool to automate testing, packaging (code, assets, translations, documentation, etc), deployment; you know, all the boring stuff. Perhaps you&#8217;re managing your dependencies with composer. The bottom line here is: you understand that shorter and easier iterations make you a more productive and insightful developer. If you can test and build a new release with one click, pat yourself on the back! You&#8217;re amazing and your effort will save you heaps of trouble later on.</li>
<li><strong>You&#8217;ve done at least <em>something</em> about testing.</strong> Perhaps you&#8217;ve decided your plugin&#8217;s functionality is simple enough that some smoke testing is enough. Or you have a rigorous &#8220;script&#8221; for the human that tests functionality via the browser. Bonus points if you&#8217;ve used a solution such as <a href="https://phpunit.de/">PHPUnit</a>, perhaps with some <a href="https://github.com/10up/wp_mock">WP_Mock</a> on the side. Or perhaps you&#8217;ve even gone as far as doing automated browser testing on real devices and browsers, using <a href="https://www.npmjs.com/package/karma">karma</a> or some other bleeding-edge solution. You&#8217;re insane! I love you!</li>
<li>To <strong>double-check you&#8217;ve thought of everything</strong>, you&#8217;ve scoured the web for other listicles of last-minute things to check. Here&#8217;s an <a href="https://www.smashingmagazine.com/2011/03/ten-things-every-wordpress-plugin-developer-should-know/">example</a>. Go find more.</li>
<li>If it&#8217;s a contract job, <strong>your client is happy</strong>. If it&#8217;s not, <strong>you&#8217;ve done your corridor testing</strong> and other WordPress developers or end users have given you their feedback.</li>
</ul>
<h2>(Version) hell is other programs</h2>
<p>If you&#8217;ve tackled all of the above and more, surely you&#8217;re &#8220;done&#8221;, right?</p>
<p>WRONG! Get ready to go through your test scenarios and checklists again on WordPress 4.0.10. And what about WordPress 3.7.13? Perhaps it&#8217;s too old to support, but you want to know if you can make the claim that your product works on it, right? And all of this has to work on PHP 5.4. Perhaps also on PHP 5.3? If you&#8217;ve done any custom DB stuff, do your SQL queries work as expected on MySQL 5.5? How about MySQL 4.x? And for every one of these version combinations, your product must work on the multitude of browsers and devices that you&#8217;re testing on. Oh, my!</p>
<blockquote><p>Every line of code you&#8217;ve written is a potential point of failure on some combination of platform, user data, and other pieces of software that share the same platform and data. Programming is easy, software engineering is hard!</p></blockquote>
<p>Remember, on day 1 when you go live, you will have a number of bugs coming in from users who use your product in combination with themes and plugins you haven&#8217;t even thought of. Your code will run on server environments so stupidly configured, you&#8217;ll want to find out where their administrator lives just so you can go punch them in the face. No matter how large or small your dev team is, <strong>you&#8217;ll have to tackle bugs very quickly in the first few days</strong>. On the internet, first impressions matter. So:</p>
<blockquote><p>The more bugs you can identify <span style="text-decoration: underline;">before</span> your users do, the better for the reputation of your product and team.</p></blockquote>
<p>The good news is that if you&#8217;ve been proactive with your tooling and test automation (and, being a professional developer, why wouldn&#8217;t you?), the easier and quicker it will be to repeat your tests on different platforms. But you still need to setup these platforms and deploy and test your product on them. Doing this using Virtual Machines is ultra-tedious, error-prone, and slow.</p>
<h2>Enter docker</h2>
<p>The age-old problem of software development can be summarized in the phrase &#8220;But it worked on <em>my</em> computer&#8221;. Today this problem has a solution, and it is called <em>Docker</em>. If you&#8217;ve been living under a rock for the past few years, go read <a href="https://www.docker.com/what-docker">what Docker is</a> and how it works before we move on. Go on, I&#8217;ll wait here!</p>
<p>Traditionally, lightweight containers are used like so: the developer &#8220;dockerizes&#8221; their product and delivers the resulting container to the system administrator, thus simplifying the deployment phase as well as any software migrations. Because deployment instructions are written by the developer in an unambiguous, repeatable, machine-executable form, this also eliminates human error and the aforementioned need for anyone to punch anyone in the face.</p>
<p>This is not what we&#8217;ll do here. In the context of WordPress component development,<strong> we will use a lightweight container to <span style="text-decoration: underline;">test</span>, not <span style="text-decoration: underline;">deploy</span>.</strong></p>
<h2 id="howto">What I did (and how you can do it too)</h2>
<p>Let me just come out and say it: I do not know, or want to know, how to install several versions of PHP side-by-side on one system. There is a multitude of how-to articles on the topic. I hate all of them because <del>I do not understand Linux package management</del> their authors are all stupid and lame. Instead, I started from <a href="http://blog.eriksen.com.br/en/docker-image-multi-version-php-development">this article</a> about a multi-PHP Docker image. You can optionally go read it, if you promise to come back here once you&#8217;re finished :-p</p>
<p>The bottom line is that there is a Docker image that already does half of what we want: It provides an environment where we can easily switch between a reasonable selection of popular PHP versions.</p>
<p>I then extended the image so that it installs WordPress, and my plugin, and runs it.</p>
<p>My plugin does not directly access the MySQL database. I only store data using the <a href="https://codex.wordpress.org/Options_API">Options API</a>. Therefore I ran a single version of a MySQL docker container like so:</p>
<pre>$ docker run --name wordpress-mysql -e MYSQL_ROOT_PASSWORD=password -d mysql:5.5</pre>
<p>Now that our MySQL 5.5 database is up and running, let&#8217;s write a Dockerfile that describes an image based on that base image:</p>
<pre>FROM eriksencosta/php-dev:latest</pre>
<p>It is traditional to include some metadata:</p>
<pre>MAINTAINER Alexandros Georgiou &lt;alex.georgiou@gmail.com&gt;
LABEL Description="Container for testing themes and plugins on all the PHPs and WordPresses" Version="1.0"</pre>
<p>Then I declare two environment variables where I set the desired PHP and WP versions for my test. Note the use of the <code>ENV</code> instructions. (We can later override these values from the command line without the need to rebuild the image).</p>
<pre>ENV WP_VER=4.0
ENV PHP_VER=5.3</pre>
<p>We will want to be able to use <code>php</code>, <code>phpenv</code>, <code>composer</code>, etc. so let&#8217;s set the environment&#8217;s <code>PATH</code> to something sensible:</p>
<pre>ENV PATH /opt/phpenv/shims:/opt/phpenv/bin:/opt/php-build/bin/:/opt/composer/vendor/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin</pre>
<p>Now let&#8217;s use the <code>WORKDIR</code> instruction to say to docker that we want to do some stuff in the root user&#8217;s home directory. This is analogous to the <code>cd</code> command.</p>
<pre>WORKDIR /root</pre>
<p>Here we will download and install the WP-CLI.</p>
<p><strong>Having a command line interface to our WordPress instance is essential because we need an automated way (i.e. script) to deploy and test our software.</strong> (If you are a WordPress developer and you do not know or are not already using WP-CLI, please stop reading this blog, step away from the computer, sell your computer, and change your career to something other than engineering.) Part of being an engineer is about doing things in an automated, repeatable and testable way.</p>
<p>Because my project is based on grunt, I use <code>apt-get</code> to install <code>node</code> and <code>npm</code>. This will allow me to run stuff like <code>npm install</code> and <code>grunt test</code> later on. I also install some basic utilities, so when a bug shows up, I can drop into the container&#8217;s shell and investigate. Here I installed the MySQL client, Midnight Commander (my favorite orthogonal file manager), and two text editors, <code>vim</code> and <code>nano</code>. Depending on your project&#8217;s needs and personal preferences you will probably want to have other things installed via <code>apt-get</code>, but I bet you will need <code>wp-cli</code>.</p>
<pre>RUN \
    curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar &amp;&amp; \
    chmod +x wp-cli.phar &amp;&amp; \
    mv wp-cli.phar /usr/local/bin/wp &amp;&amp; \
    curl -sL https://deb.nodesource.com/setup | bash - &amp;&amp; \
    apt-get install -y nodejs npm mysql-client vim nano mc</pre>
<p>Note the backslashes. We&#8217;ve stringed together the shell commands into one as a performance optimization. Every command creates a new intermediate image in docker. We could just as well have issued commands in separate RUN instructions, but we know better than that!</p>
<p>The following <code>ADD</code> instruction says that any files in the same directory as this Dockerfile are to be copied into the image&#8217;s root user home directory.</p>
<pre>ADD . /root</pre>
<p>The way I&#8217;ve set things up, this will bring in a script that I call <code>install.sh</code>. This will carry out the deliverable&#8217;s deployment and possibly add content and run any automated testing we have available. Remember that the <code>CMD</code> instuction defines the <em>utility command</em> of the container. You can later override this from the command line. This is very useful for debugging.</p>
<pre>CMD /bin/bash -c /root/install.sh
</pre>
<p>In that same home directory I have a <code>plugins</code> directory. This will contain the zip files for any plugins I want to install. My <code>themes</code> directory will contain the themes that I will want to install. I have a <code>wp</code> directory where I have downloaded tarballs of every WordPress version since 3.7. Adding all of the versions into the image, we make sure that we can use our environment variable to switch between images without rebuilding the docker image. Arguably one could use the <code>wp-cli core install</code> command but that would involve re-downloading stuff every time.</p>
<p>Of course we need to make sure that we can access the image&#8217;s nginx web server:</p>
<pre>EXPOSE 80</pre>
<p>Let&#8217;s look at the entire <code>Dockerfile</code> again before we dive into <code>install.sh</code>:</p>
<pre>FROM eriksencosta/php-dev:latest

MAINTAINER Alexandros Georgiou &lt;alex.georgiou@gmail.com&gt;
LABEL Description="Container for testing themes and plugins on all the PHPs and WordPresses" Version="1.0"

ENV WP_VER=4.0
ENV PHP_VER=5.3

ENV PATH /opt/phpenv/shims:/opt/phpenv/bin:/opt/php-build/bin/:/opt/composer/vendor/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

WORKDIR /root

RUN \
    curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar &amp;&amp; \
    chmod +x wp-cli.phar &amp;&amp; \
    mv wp-cli.phar /usr/local/bin/wp &amp;&amp; \
    curl -sL https://deb.nodesource.com/setup | bash - &amp;&amp; \
    apt-get install -y nodejs npm mysql-client vim nano mc

ADD . /root

CMD /bin/bash -c /root/install.sh

EXPOSE 80</pre>
<p>Neat!</p>
<p>We then start writing our install script. Now, dockerfiles are a relatively new thing and warrant some explanation, but fortunately we all speak fluent <a href="https://www.gnu.org/software/bash/">bash</a>, so I hopefully don&#8217;t need to say much about the following code.</p>
<p>Remember how we&#8217;ve set environment variables for WordPress and PHP? Now is the time to use them.</p>
<pre>#!/bin/bash -x

echo "Installing on WordPress ${WP_VER} on PHP ${PHP_VER}"

phpenv global $PHP_VER</pre>
<p>We&#8217;ve switched to the right PHP, now on to install the right version of WordPress from our directory of WordPress tarballs:</p>
<pre>cd /var/www

tar xfv /root/wp/wordpress-${WP_VER}.tar.gz
touch /var/www/wordpress/wp-content/debug.log
chown -R root:root /var/www/wordpress</pre>
<p>Next, we create the <code>wp-config.php</code> file, with the help of <code>WP-CLI</code>. We will want to enable logging, to spot any errors resulting from running our code on different versions. We will also expose the image&#8217;s 80 port to port 81 of our local machine. This is necessary if you are already running a web server on your dev machine. Change 81 to any port that you have free.</p>
<pre>cd /var/www/wordpress

wp core config --allow-root --dbhost=db --dbname=wpdb --dbuser=root --dbpass=password --extra-php &lt;&lt;PHP
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define('WP_HOME','http://localhost:81/wordpress');
define('WP_SITEURL','http://localhost:81/wordpress');
PHP</pre>
<p>Throughout my script you&#8217;ll notice the liberal use of <code>--allow-root</code> in <code>wp-cli</code> commands. In our container we do stuff as the user root, hence the need for the switch.</p>
<p>Let&#8217;s make sure that we start with a clean database:</p>
<pre>wp db drop --allow-root --yes

wp db create --allow-root

wp core install \
    --allow-root \
    --url=http://localhost:81 \
    --title="Testing on WordPress ${WP_VER} on PHP ${PHP_VER}" \
    --admin_user=admin \
    --admin_password=password \
    --admin_email=alex.georgiou@gmail.com</pre>
<p>Now let&#8217;s install the plugins we want in our test:</p>
<pre>wp plugin install /root/plugins/*.zip --allow-root
wp plugin activate --all --allow-root</pre>
<p>Perhaps you&#8217;ll need to import some content into your WordPress instance. Here we import a post that could contain new shortcodes or whatever:</p>
<pre>wp post create --allow-root --post_status=publish --post_type=page --post_title='Test post' /root/post.txt</pre>
<p>Depending on your project&#8217;s needs, you can go wild here. Use <code>wp-cli</code> to tweak your test site as needed. This is also where you can run your automated tests.</p>
<pre>mkdir /root/src
cd /root/src
git clone http://dev.lan/git/myproject.git
cd myproject
npm install
grunt phpunit</pre>
<p>Now let&#8217;s make sure that <a href="http://nginx.org/">nginx</a> is running, and that we are viewing the debug logs and web server logs:</p>
<pre>webserver restart
tail -f /var/log/nginx/*.log /var/www/wordpress/wp-content/debug.log</pre>
<p>That&#8217;s it!</p>
<p>Let&#8217;s review the install script in its entirety:</p>
<pre>#!/bin/bash -x

echo "Installing on WordPress ${WP_VER} on PHP ${PHP_VER}"

phpenv global $PHP_VER

cd /var/www

tar xfv /root/wp/wordpress-${WP_VER}.tar.gz
cp /root/.htaccess /var/www/wordpress
touch /var/www/wordpress/wp-content/debug.log
chown -R root:root /var/www/wordpress

cd /var/www/wordpress

wp core config --allow-root --dbhost=db --dbname=wpdb --dbuser=root --dbpass=password --extra-php &lt;&lt;PHP
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define('WP_HOME','http://localhost:81/wordpress');
define('WP_SITEURL','http://localhost:81/wordpress');
PHP

wp db drop --allow-root --yes

wp db create --allow-root

wp core install \
    --allow-root \
    --url=http://localhost:81 \
    --title="Testing on WordPress ${WP_VER} on PHP ${PHP_VER}" \
    --admin_user=admin \
    --admin_password=password \
    --admin_email=alex.georgiou@gmail.com

wp plugin install /root/plugins/*.zip --allow-root

wp plugin activate --all --allow-root

wp post create --allow-root --post_status=publish --post_type=page --post_title='Test post' /root/post.txt

mkdir /root/src
cd /root/src
git clone http://dev.lan/git/myproject.git
cd myproject
npm install
grunt phpunit

webserver restart

tail -f /var/log/nginx/*.log /var/www/wordpress/wp-content/debug.log</pre>
<p>Beautiful!</p>
<p>We&#8217;re already running one docker image with a standard MySQL database. Now we need to build our webserver image and run it.</p>
<p>First, build the image:</p>
<pre>docker build -t alexg/wp-test:latest .</pre>
<p>Then, run it, making sure to link the database image, map the 80 port to local 81, and set the version variables:</p>
<pre>docker run --link wordpress-mysql:db -p 81:80 -e PHP_VER=5.6 -e WP_VER=4.4.2 -i -t alexg/wp-test:latest</pre>
<p>This command will now fire up a WordPress version of your choosing, and run the install script. The script will install your theme or plugin, add content, run your automated tests, and finally expose the entire thing at <a href="http://localhost:81/wordpress">http://localhost:81/wordpress</a>. Visit <a href="http://localhost:81/wordpress/wp-login.php">http://localhost:81/wordpress/wp-login.php</a> and login with <code>admin/password</code> or whatever credentials you&#8217;ve set in the <code>wp-cli core install</code> command.</p>
<p>Your shell should now be tailing the nginx and WP logs. Do any automated or manual testing via your browser(s), then hit Ctrl-C to stop the container and run the command with different versions, like so:</p>
<pre>docker run --link wordpress-mysql:db -p 81:80 -e PHP_VER=5.3 -e WP_VER=4.0 -i -t alexg/wp-test:latest</pre>
<p>Hit <a href="http://localhost:81/wordpress">http://localhost:81/wordpress</a> again and voila! Your product is now running on a completely different environment.</p>
<p>Now say you&#8217;ve spotted an error that did not show in your development environment. It is probably a version-related issue. How do you investigate?</p>
<p>You could override the install script by setting the utility command to be the Linux shell (note the part <strong>in bold</strong>):</p>
<pre>docker run --link wordpress-mysql:db -p 81:80 -e PHP_VER=5.6 -e WP_VER=4.4.2 -i -t alexg/slate-test:latest <strong>/bin/bash</strong></pre>
<p>Now docker loads your image and drops you in a shell. Here you can do fun stuff like connect to your database via the command line (remember, we&#8217;ve installed the mysql-client package):</p>
<pre>mysql -h db -u root -p
use wpdb;</pre>
<p>Once you&#8217;re ready you can run the install script yourself, or even run the deployment manually if you need to tweak stuff.</p>
<pre>/root/install.sh</pre>
<p>That&#8217;s it!</p>
<h2>Conclusion</h2>
<p><strong>Thanks to Docker, we&#8217;ve managed to parametrise our test system to the point where we know for sure that a particular deliverable will behave in a combination of PHP and WordPress versions.</strong> Want to test on different MySQL versions? Easy. Just tweak the command where we fire up the database.</p>
<p>You might think that all of this is too much. You might think that testing on a couple of versions is OK. But you would be wrong. You&#8217;ll be amazed at what issues crop up when you do rigorous testing. For instance, I was using shortcodes where their attributes had dashes (-) in their names. This is fine in WordPress 4.3.0 and greater, but not in earlier versions. Thanks to docker testing, I was able to find this out and fix it by changing any dashes to underscores. And I was able to do all this, without having to read <a href="https://codex.wordpress.org/Shortcode_API#Attributes">this part of the documentation that expressly warns about the issue</a>:</p>
<blockquote><p>Attribute names are optional and should contain only the following characters for compatibility across all platforms:</p>
<ul>
<li>Upper-case and lower-case letters: A-Z a-z</li>
<li>Digits: 0-9</li>
<li>Underscore: _</li>
<li>Hyphen: &#8211; (Not allowed before version 4.3.0)</li>
</ul>
</blockquote>
<p>In conclusion, does my solution suck? Yes, it sucks bad, because this was my first real use of Docker. Being a lightweight container &#8220;rookie&#8221;, I&#8217;ve probably done more than a few things &#8220;The Wrong Way&#8221;. Feel free to flame away in the comment section below, preferably but not necessarily in a constructive manner.</p>
<p>But, did my solution work? You bet your computer&#8217;s rear USB port it did!</p>
<p>Or, at least, it worked on my machine :-p</p>
<p>The post <a href="https://www.alexgeorgiou.gr/test-wordpress-with-docker/">🐳 Test your WordPress themes and plugins with Docker</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/test-wordpress-with-docker/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
	</channel>
</rss>
