Technote FYI

  • Code
  • Sysadmin
  • Docker Container to Develop a WordPress Plugin

    Docker Container to Develop a WordPress Plugin

    I asked Gemini to write a docker-compose file to create a wordpress development environment with mariadb. Note that this is using old software, like MariaDB 10.6. This is pretty typical of LLMs.

    version: '3.8'
    
    services:
      wordpress:
        image: wordpress:latest
        restart: always
        ports:
          - "8080:80"
        environment:
          WORDPRESS_DB_HOST: mariadb
          WORDPRESS_DB_USER: ${MYSQL_USER}
          WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
          WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
        volumes:
          - wordpress_data:/var/www/html
          - ./plugin:/var/www/html/wp-content/plugins/plugin # Bind mount for plugins
        depends_on:
          - mariadb
        networks:
          - mynetwork
    
      mariadb:
        image: mariadb:10.6
        restart: always
        environment:
          MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
          MYSQL_DATABASE: ${MYSQL_DATABASE}
          MYSQL_USER: ${MYSQL_USER}
          MYSQL_PASSWORD: ${MYSQL_PASSWORD}
        volumes:
          - mariadb_data:/var/lib/mysql
        networks:
          - mynetwork
    
    volumes:
      wordpress_data:
      mariadb_data:
    
    networks:
      mynetwork:
    

    (The prompt was more complicated, and I did a back and forth with it.)

    Here’s the .env file:

    MYSQL_ROOT_PASSWORD=wordpress
    MYSQL_DATABASE=wordpress
    MYSQL_USER=wordpress
    MYSQL_PASSWORD=wordpress
    

    In the same directory, do:

    mkdir plugin

    Then, in the plugin directory, use this file, plugin.php:

    <?php
    /*
    Plugin Name: Minimalist Plugin
    Description: A very basic plugin example.
    Version: 1.0
    Author: Your Name
    */
    
    function minimalist_plugin_message() {
        echo '<p>This message is from the Minimalist Plugin!</p>';
    }
    
    add_action('wp_footer', 'minimalist_plugin_message');
    ?>
    

    This setup requires you to call your plugin “plugin”. If you want to name it something else, you can alter the docker-compose.yml config file.

    If you want to develop a more complex plugin that, for example, relies on other plugins, alter the docker-compose.yml file and put the entire plugin directory into your work directory.

    Linux Configuration

    On the Linux side, I’m using MINT 22. I just used the default docker installation, which is out of date, but worked:

    johnk@mini ~ % sudo apt search docker | grep ^i
    i docker-compose - define and run multi-container Docker appl
    i A docker.io - Linux container runtime
    i A python3-docker - Python 3 wrapper to access docker.io's con
    i A python3-dockerpty - Pseudo-tty handler for docker Python clien

    johnk@mini ~ % docker version
    Client:
    Version: 26.1.3
    API version: 1.45
    Go version: go1.22.2
    Git commit: 26.1.3-0ubuntu1~24.04.1
    Built: Mon Oct 14 14:29:26 2024
    OS/Arch: linux/amd64
    Context: default

    Server:
    Engine:
    Version: 26.1.3
    API version: 1.45 (minimum version 1.24)
    Go version: go1.22.2
    Git commit: 26.1.3-0ubuntu1~24.04.1
    Built: Mon Oct 14 14:29:26 2024
    OS/Arch: linux/amd64
    Experimental: false
    containerd:
    Version: 1.7.24
    GitCommit:
    runc:
    Version: 1.1.12-0ubuntu3.1
    GitCommit:
    docker-init:
    Version: 0.19.0
    GitCommit:
    johnk@mini ~ % docker-compose version
    docker-compose version 1.29.2, build unknown
    docker-py version: 5.0.3
    CPython version: 3.12.3
    OpenSSL version: OpenSSL 3.0.13 30 Jan 2024
    johnk@mini ~ %

    I’m still having some trouble with having a reasonably fast system boot, and running Docker. I tried this:

    # start a sudo shell, and then run commands
    sudo -s
    systemctl disable docker
    systemctl enable docker.socket
    systemctl start docker.socket

    Starting and Stopping the Container to Work

    When you start working, fire up a terminal and do this:

    cd <to the work directory>
    docker-compose up

    You’ll see the log messages (good). You should be able to see your server at localhost:8080

    To shut down:

    # press CONTROL-C. This initiates the shutdown process.
    # It doesn't remove the containers, though.
    docker-compose down

    You’ll see docker-compose remove the two containers for WordPress and MariaDB.

    Note About Containers: they contain state

    A container is like a little “computer” with an image (aka files), processes, and CPU. When you press control-C, you just tell the process to exit. You can still tell these containers to start by running the “docker-compose start” command.

    That’s why you need to run the “docker-compose down” command, to destroy the containers.

    You might be wondering about the database state. That isn’t kept in the container. Instead, Docker has a feature called named volumes, under the volumes key, which are containers that aren’t destroyed. To see the named volumes:

    docker volume ls
    DRIVER    VOLUME NAME
    local wp-plugin-dev_mariadb_data
    local wp-plugin-dev_wordpress_data

    Backups

    Backup the Work Volumes

    Use vackup, a script that produces compressed tarballs from volumes. I did this, and it worked well:

    vackup export wp-plugin-dev_mariadb_data m.tgz

    Also consider using Docker Desktop’s extension, if you are using Docker Desktop.

    This saves out your file uploads, settings, and other difficult-to-restore things in your environment.

    Be warned that this is all state. You shouldn’t be creating a lot of state in the containers. It’s better to create scripts to create the state (setup scripts, upload tools that use WP-CLI, etc.) or the setup should be included in the plugin.

    Backup the Application Data with an Export

    It’s generally best to perform backups of the application data through the WordPress Import/Export tool. The main advantages are: it tests that export and import work; it’s database neutral.

    The WordPress Import tool supports custom post types. So if you implement your “database” as custom posts (rather than traditional database tables) you get export and import without writing code. If you use a tool like Advanced Custom Fields or another plugin GUI to create CPTs, you can design this part of your plugin interactively.

    (To really exercise the code, you will need to make docker-compose configs that also use different versions of the databases, different versions of PHP, WordPress, etc.)

    Wiping Away the State

    Every once in a while, you need to wipe out all the state in your dev environment (but not your code, of course), and start fresh. To do this:

    • Stop and delete all your project containers.
    • Delete all the volumes used by your containers.
    • Delete the images used to build the containers.

    ToDo

    • Set up Git
    • Set up a remote Git repo on GitHub
    • Set up a remote Git repo over SSH
    • Create releases and push to live

    March 1, 2025
    admin
    Categories: Docker, PHP
  • WordPress Admin Emails Not Getting Through to Gmail addresses, Docker Container Based WordPress with BusyBox

    My server wasn’t getting admin emails through to Gmail addresses. Some emails from my newer WordPress installation were working, but the oldest WordPress installation was not. They were all running on a Docker server. It’s described in Configuring Alpine Linux on Docker to Send Mail for WordPress

    Email was getting through on Hotmail, though. So here are some differences:

    For the site decluttermethod.com, on the newer install, email going to a Gmail account:

    ARC-Authentication-Results: i=1; mx.google.com;
           spf=neutral (google.com: 216.240.146.66 is neither permitted nor denied by best guess record for domain of dapache@db44432c0e62) smtp.mailfrom=dapache@db44432c0e62
    Return-Path: <dapache@db44432c0e62>
    Received: from d.slaptech.net (ns1.slaptech.net. [216.240.146.66])
            by mx.google.com with ESMTP id y4-20020*******000b00502ee712648si4508975pgc.578.2023.05.22.02.43.56
            for <landofziploc@gmail.com>;
            Mon, 22 May 2023 02:43:56 -0700 (PDT)
    Received-SPF: neutral (google.com: 216.240.146.66 is neither permitted nor denied by best guess record for domain of dapache@db44432c0e62) client-ip=216.240.146.66;
    Authentication-Results: mx.google.com;
           spf=neutral (google.com: 216.240.146.66 is neither permitted nor denied by best guess record for domain of dapache@db44432c0e62) smtp.mailfrom=dapache@db44432c0e62
    Received: from db44432c0e62 (zanon.local [172.16.239.1]) by d.slaptech.net (Postfix) with ESMTP id D2D3910EED55 for <landofziploc@gmail.com>; Mon, 22 May 2023 09:43:55 +0000 (UTC)
    To: landofziploc@gmail.com
    Subject: [Declutter Method] Please moderate: "Decluttering on Mercari"
    Date: Mon, 22 May 2023 09:43:55 +0000
    From: WordPress <wordpress@onami.slaptech.net>
    

    Here’s a header from an admin email from the old install, to a Hotmail account:

    Authentication-Results: spf=temperror (sender IP is 216.240.146.66)
     smtp.mailfrom=da8bdb99b49e; dkim=none (message not signed)
     header.d=none;dmarc=none action=none
     header.from=greenslocal.org;compauth=fail reason=001
    Received-SPF: TempError (protection.outlook.com: error in processing during
     lookup of da8bdb99b49e: DNS Timeout)
    Received: from d.slaptech.net (216.240.146.66) by
     DM6NAM10FT058.mail.protection.outlook.com (10.13.153.48) with Microsoft SMTP
     Server id 15.20.6521.21 via Frontend Transport; Mon, 19 Jun 2023 19:06:48
     +0000
    X-IncomingTopHeaderMarker:
    OriginalChecksum:C74990698868478**********02B3C607DD13081A3A6FCABD04BFC9D54854D9;UpperCasedChecksum:50045DFDFDF0E000*********7213AF316CB04E8287BCD3301FB4238EECEA1B;SizeAsReceived:550;Count:9
    Received: from da8bdb99b49e (zanon.local [172.16.239.1])
    	by d.slaptech.net (Postfix) with ESMTP id 37FBA10EDDBE
    	for <johnkawakami@hotmail.com>; Mon, 19 Jun 2023 19:06:48 +0000 (UTC)
    To: johnkawakami@hotmail.com
    Subject: [Greens Local Sites] Password Reset
    Date: Mon, 19 Jun 2023 19:06:48 +0000
    From: WordPress <wordpress@greenslocal.org>
    

    After some thought, I figured out that the email getting through to Gmail was a fluke. This isn’t a consistent email problem. Explanation is below.

    The Solution

    The problem was visible in these two headers:

    Received-SPF: TempError (protection.outlook.com: error in processing during
     lookup of da8bdb99b49e: DNS Timeout)
    ...
    Received: from da8bdb99b49e (zanon.local [172.16.239.1])
    	by d.slaptech.net (Postfix) with ESMTP id 37FBA10EDDBE
    	for <johnkawakami@hotmail.com>; Mon, 19 Jun 2023 19:06:48 +0000 (UTC)

    The boldfaced part is the hostname of the container. It matches the ID of the container in Docker.

    The outlook.com service was looking for the SPF record for da8bdb99b49e, which is an invalid domain name.

    The fix is to set the hostname of the container to one of the domains on the server. I chose greenslocal.org. This is how it’s done in the docker-compose.yml config file. Config is boldfaced:

        web:
            build: ../alpine-apache2-php7
            hostname: greenslocal.org
            networks:...

    Note that I’m setting the hostname of the web service that runs that domain.

    The SPF record for greenslocal.org allows email from a range of IP addresses, including the one used to send the mail.

    Rebuild the container, and then restart it:

    docker-compose build web
    docker-compose stop web
    docker container prune
    docker-compose up -d web

    Testing

    I found a nice plugin called Email Test that works.

    To install it:

    wp plugin install email-test

    What Was Happening?

    Most of the server is running in Docker containers. They’re like virtual machines. The containers have names like “cb23401bd3”. When email was being sent out, that name was associated with the origin of the email. That caused problems. It failed because it tried to find the SPF record for that random string — I will explain SPF below.

    I had to change the name to “greenslocal.org”, so that would be associated with the email. When email was sent, it would get to Google or Hotmail, which would check the SPF record for greenslocal.org. SPF is a string that indicates which servers can send email for greenslocal.org. The record said the server was allowed to send mail, so everything worked.

    The failure was inconsistent because SPF validation is not ironclad. It’s just one factor in deliverability, and if an SPF record doesn’t exist, the recipient server can still choose to let the email through.

    June 19, 2023
    admin
    Categories: Uncategorized
  • Run a Shiny Server with the rocker/shiny and Docker-Compose on Debian or Ubuntu

    How to set up Ubuntu to run Docker, a Shiny Server container, with an existing Apache proxy server configuration. This setup will work for a low-volume application. It runs a single container, on a single host.

    (more…)
    May 1, 2021
    admin
    Categories: Uncategorized
  • Configuring Alpine Linux on Docker to Send Mail for WordPress

    I got WordPress working, but the email wasn’t working. Nothing was going out from the blog application, but other mail in the system worked fine. The fix is to alter the sendmail configuration in php.ini.

    Alpine Linux is a lean Linux, and doesn’t come with a complete sendmail. Rather, it uses BusyBox’s sendmail that forward email to an SMTP server. This is usually enough for most systems, but the problem arises because there’s no config file to change the SMTP server.

    Most bloggers are suggesting installing ssmtp. While that works, you can also just reconfigure sendmail in the php.ini file.

    FROM alpine:3.9
    
    EXPOSE 80 443
    
    VOLUME ["/opt/docker/apache/"]
    # directory contains www/, config/, sites-enabled/
    
    COPY docker-httpd.conf /etc/apache2/conf.d/docker-httpd.conf
    
    RUN rm -f /var/www/localhost/htdocs/index.html
    COPY index.php /var/www/localhost/htdocs/
    
    RUN apk update && apk upgrade && \
        apk add --no-cache \
        apache2 \
        # apache2-http2 \
        # apache-mod-fcgid \
        # php7-fpm \
        php7-apache2 \
        php7-pdo \
        php7-gd php7-pdo_mysql php7-curl php7-mysqli \
        php7-iconv php7-json php7-xml php7-openssl \
        php7-sockets php7-ftp php7-session \
        php7-xml php7-ctype php7-dom php7-tokenizer
    
    RUN apk add procps
    RUN apk add php7-mbstring
    
    RUN addgroup -g 2001 -S dapache
    RUN adduser -S -D -H -u 2001 -G dapache dapache
    
    COPY httpd.conf /etc/apache2/httpd.conf
    COPY php.ini /etc/php7/php.ini
    
    ENTRYPOINT ["/usr/sbin/httpd", "-D", "FOREGROUND"]

    The php.ini is the default php.ini, with one change:

    sendmail_path = /usr/sbin/sendmail -S 172.16.239.1 -t -i

    The -S option specifies a relay server. The IP address 172.16.219.1 is an internal mail relay network, and it’s running a container with Postfix.

    Set that value to your SMTP server. The sendmail command also supports options for authentication.

    BusyBox v1.29.3 (2019-01-24 07:45:07 UTC) multi-call binary.
    
    Usage: sendmail [-tv] [-f SENDER] [-amLOGIN 4<user_pass.txt | -auUSER -apPASS]
                    [-w SECS] [-H 'PROG ARGS' | -S HOST] [RECIPIENT_EMAIL]...
    
    Read email from stdin and send it
    
    Standard options:
            -t              Read additional recipients from message body
            -f SENDER       For use in MAIL FROM:<sender>. Can be empty string
                            Default: -auUSER, or username of current UID
            -o OPTIONS      Various options. -oi implied, others are ignored
            -i              -oi synonym, implied and ignored
    
    Busybox specific options:
            -v              Verbose
            -w SECS         Network timeout
            -H 'PROG ARGS'  Run connection helper. Examples:
                    openssl s_client -quiet -tls1 -starttls smtp -connect smtp.gmail.com:25
                    openssl s_client -quiet -tls1 -connect smtp.gmail.com:465
                            $SMTP_ANTISPAM_DELAY: seconds to wait after helper connect
            -S HOST[:PORT]  Server (default $SMTPHOST or 127.0.0.1)
            -amLOGIN        Log in using AUTH LOGIN (-amCRAM-MD5 not supported)
            -auUSER         Username for AUTH
            -apPASS         Password for AUTH
    
    If no -a options are given, authentication is not done.
    If -amLOGIN is given but no -au/-ap, user/password is read from fd #4.
    Other options are silently ignored; -oi is implied.
    Use makemime to create emails with attachments.
    

    To test your configuration, I like to use the code from php.net:

    <?php
    
    $to      = 'youremailaddress@example.com';
    $subject = 'the subject';
    $message = 'hello';
    $headers = 'From: www-data@slaptech.net' . "\r\n" .
        'X-Mailer: PHP/' . phpversion();
    
    mail($to, $subject, $message, $headers);
    

    Save that to a script, and run it. If you receive a mail, the configuration worked.

    December 7, 2019
    admin
  • Dead Pages, Alpine, Docker, Cloud

    I’m having some Google indexing problems, with some traffic going to riceball.com, and some coming to technote.fyi. According to Search Console, I still have 367 riceball.com pages; in reality, I have maybe 40 pages. Some of these pages haven’t been crawled since the middle of 2018.

    They don’t show up in search results.

    (more…)
    September 29, 2019
    admin
    Categories: Uncategorized
  • Multiple WordPress Installations on Multiple Docker Containers Behind a Single Reverse Proxy

    How to set up a server to host WordPress MultiSite, on multiple containers, all behind a single IP address.

    Apache as a Reverse Proxy for Virtual Hosts

    We all know how to do VirtualHosts on Apache, but what if you want to run a web application inside a container, or on another box, or in a virtual machine?

    You need to turn Apache in to a reverse proxy. This is also called a gateway. It’s operation is pretty straightforward, and described in the Apache2 docs:

    ProxyPass "/foo" "http://example.com"

    That’s pretty simple. When the user hits /foo, they get the pages from example.com.

    You can use the ProxyPass directive in a VirtualHost section.

    On Debian systems, you set up virtual hosts with separate config files. My current one looks like this:

    <VirtualHost 216.240.146.66:80>
        ServerName slaptech.net
        ProxyPass "/" "http://example.com/"
        ProxyPassReverse "/" "http://example.com/"
    </VirtualHost>

    Again, it’s pretty straightforward. I have a domain, slaptech.net, that resolves to the above IP address.

    When the Host header matches slaptech.net, it matches this VirtualHost scope. The server then maps the root of the URL, / , to requests at example.com.

    See the Apache mod_proxy docs for more details.

    nginx as a Reverse Proxy

    Setting Up Let’s Encrypt Certificates

    Setting Up a Network of Apache + PHP Containers

    Setting Up Minimal Email Sending on Alpine Linux

    Setting Up a Shared File Space

    Setting Up Shell Aliases to Run WP-CLI Within Containers

    Installing WordPress MultiSite

    Setting Up a New Site for a Domain

    Adjusting the File Upload Limits

    In WordPress

    In the Container’s PHP and Apache2

    In the nginx Reverse Proxy

    Migrating a Site from One Installation to Another

    Create a new.example.com Domain Name

    Set Up a New Site on the Target WP Installtion

    Export the Content from the Original Site

    Import the Content on the New Domain

    Changing the Reverse Proxy and Virtual Hosts to Send Traffic to the New Site

    September 5, 2019
    admin
1 2 3
Next Page

Proudly Powered by WordPress