• Skip to main content
  • Skip to footer

TechGirlKB

Performance | Scalability | WordPress | Linux | Insights

  • Home
  • Speaking
  • Posts
    • Linux
    • Performance
    • Optimization
    • WordPress
    • Security
    • Scalability
  • About Janna Hilferty
  • Contact Me

Posts

phpdbg: Increase Unit Test speed dramatically

In our current deploy setup, there exists more than 200,000 lines of code for some of our apps. Naturally, this means there are a LOT of unit tests paired with this code which need to be run, and that code coverage reports take a long time. Running the unit tests by themselves (nearly 2600 tests) took around 30 minutes to complete. However, adding in the code coverage to that run bumped the time up dramatically, to nearly 3 hours:

./vendor/bin/phpunit --coverage-clover ./tests/coverage/clover.xml
...
...
... (a lot of unit tests later)
Time: 2.88 hours, Memory: 282.50MB
OK (2581 tests, 5793 assertions)
Generating code coverage report in Clover XML format … done
Generating code coverage report in HTML format … done
Thing move a little… slowly… around here…

The dilemma

In the existing setup, our deployment service received a webhook from our source code management software every time code was merged to the develop branch. The deployment service then pushed the code change to the server, ran our ansible deployment scripts, and then ran unit tests on the actual develop server environment. This was not ideal, for a few reasons:

  1. Bad code (malicious or vulnerable code, code that breaks functionality, or code that just doesn’t work) could be pushed to the server without testing happening first.
  2. Things could be left in a broken state if the deployment were to fail its unit tests, with no real accountability to fix the issue.
  3. The unit tests take so long it was causing the deployment service to reach its 40 minute timeout just on the unit tests, not even including the code coverage.
That’s gonna be a yikes from me, hombre

In a more ideal world, the deployment to the develop server environment should be gated by the unit tests (and security scanning as well) so that code is only deployed when tests are successful. And, the most ideal way to do this would be with an automated CI/CD pipeline.

We already had some regression testing setup in Jenkins, so creating a pipeline was certainly an option. The dilemma, however, was how to generate code coverage feedback in a reasonable amount of time, without waiting 3 hours for said feedback. Enter phpdbg.

The solution

phpdbg is an alternative to xdebug, and is an interactive PHP debugger tool. Unfortunately the documentation has very little information on usage or installation, but does mention that PHP 5.6 and higher come with phpdbg included.

That information, plus a few promising blog posts (including one from Sebastian Bergmann of phpunit himself and one from remi repo’s blog) gave us hope for a faster solution:

  • http://kizu514.com/blog/phpdbg-is-much-faster-than-xdebug-for-code-coverage/
  • https://hackernoon.com/generating-code-coverage-with-phpunite-and-phpdbg-4d20347ffb45
  • https://medium.com/@nicocabot/speed-up-phpunit-code-coverage-analysis-4e35345b3dad
  • https://blog.remirepo.net/post/2015/11/09/PHPUnit-code-coverage-benchmark
  • https://thephp.cc/news/2015/08/phpunit-4-8-code-coverage-support

If this tool worked as promised, it could save a massive amount of processing time for very similar code coverage calculation results, and a little bit more Memory. Relatively small trade-offs for some big benefits, if you ask me.

Making the solution work

As it turns out, the silver bullet was more like a “bang your head on your desk until it works” kind of solution. What I read was promising, but I kept running into issues in execution.

  • First, since our Jenkins instance had PHP 7.2 installed, it sounded like phpdbg should work right out of the box since it’s included in PHP from version 5.6+, right? Unfortunately, phpdbg wasn’t an available bin to be used, and wasn’t one of the packages installed with yum on our CentOS 7 servers.
  • This github (now archived) from user krakjoe indicated if I just installed PHP from source using this repo it would work, but this too failed (and caused all other PHP functions to stop working).
  • Eventually I stumbled upon these remi rpms that actually include phpdbg. The fun didn’t stop there, though…
  • Firstly, installing the yum package worked well enough, but it took me a minute to realize that the bin is actually under “php72-phpdbg” and not just “phpdbg”. No big deal, so far…
  • Now I actually had the php72-phpdbg command working and could enter the command line, but when I wrapped the phpunit commands with it, I was getting errors about other php packages (intl, pecl-zip, etc) not being installed. It turns out the php72-phpdbg package was from the “remi-safe” repo, which didn’t recognize the other php packages (which had been installed with the remi-php72 repo). To fix this, I had to install all the remi-php72 packages with the remi-safe repo instead.
Just shake it off, buddy

At the end of the day when the dust settled, we got the results we were hoping for:

php72-phpdbg -qrr ./vendor/bin/phpunit --coverage-clover ./tests/coverage/clover.xml 
...
...
... (a lot of unit tests later)
Time: 36.37 minutes, Memory: 474.50MB
OK (2581 tests, 5793 assertions)
Generating code coverage report in Clover XML format … done
Generating code coverage report in HTML format … done

Our coverage generator showed results were about half a percent difference lower than with phpunit alone (using Xdebug). Some users have reported coverage differences more than this, or are more concerned about the differences. For us, the difference is not in our favor (lower than original results), so we are less concerned. The benefits far outweigh the concern in our situation.

Conclusion

There was a steep curve in figuring out how to install and properly use phpdbg on our servers, but in the end, saving over 2 hours per run and allowing ourselves to gate deploys to the develop server environment based on quality and security in this way made the effort totally worth it. The biggest struggle in this process was the lack of documentation out there on phpdbg, so hopefully this article helps others who may be in the same boat!

smell ya later homies!

Adding version control to an existing application

Most of us begin working on projects, websites, or applications that are already version controlled in one way or another. If you encounter one that’s not, it’s fairly easy to start from exactly where you are at the moment by starting your git repository from that point. Recently, however, I ran into an application which was only halfway version controlled. By that I mean, the actual application code was version controlled, but it was deployed from ansible code hosted on a server that was NOT version controlled. This made the deploy process frustrating for a number of reasons.

  • If your deploy fails, is it the application code or the ansible code? If the latter, is it because something changed? If so, what? It’s nearly impossible to tell without version control.
  • Not only did this application use ansible to deploy, it also used capistrano within the ansible roles.
  • While the application itself had its own AMI that could be replicated across blue-green deployments in AWS, the source server performing the deploy did not — meaning a server outage could mean a devastating loss.
  • Much of the ansible (and capistrano) code had not been touched or updated in roughly 4 years.
  • To top it off, this app is a Ruby on Rails application, and Ruby was installed with rbenv instead of rvm, allowing multiple versions of ruby to be installed.
  • It’s on a separate AWS account from everything else, adding the fun mystery of figuring out which services it’s actually using, and which are just there because someone tried something and gave up.

As you might imagine, after two separate incidents of late nights trying to follow the demented rabbit trail of deployment issues in this app, I had enough. I was literally Lucille Bluth yelling at this disaster of an app.

It was a hot mess.

Do you ever just get this uncontrollable urge to take vengeance for the time you’ve lost just sorting through an unrelenting swamp of misery caused by NO ONE VERSION-CONTROLLING THIS THING FROM THE BEGINNING? Well, I did. So, below, read how I sorted this thing out.

Start with the basics

First of all, we created a repository for the ansible/deployment code and put the existing code on this server in place. Well, kind of. It turns out there were some keys and other secure things that shouldn’t be just checked into a git repo willy-nilly, so we had to do some strategic editing.

Then I did some mental white-boarding, planning out how to go about this metamorphosis. I knew the new version of this app’s deployment code would need a few things:

  • Version control (obviously)
  • Filter out which secure items were actually needed (there were definitely some superfluous ones), and encrypt them using ansible-vault.
  • Eliminate the need for a bastion/deployment server altogether — AWS CodeDeploy, Bitbucket Pipelines, or other deployment tools can accomplish blue-green deployments without needing an entirely separate server for it.
  • Upgrade the CentOS version in use (up to 7 from 6.5)
  • Filter out unnecessary work-arounds hacked into ansible over the years (ANSIBLE WHAT DID THEY DO TO YOU!? :sob:)
  • Fix the janky way Passenger was installed and switch it from httpd/apache as its base over to Nginx
  • A vagrant/local version of this app — I honestly don’t know how they developed this app without this the whole time, but here we are.

So clearly I had my work cut out for me. But if you know me, you also know I will stop at nothing to fix a thing that has done me wrong enough times. I dove in.

Creating a vagrant

Since I knew what operating system and version I was going to build, I started with my basic ansible + vagrant template. I had it pull the regular “centos/7” box as our starting point. To start I was given a layout like this to work with:

+ app_dev
- deploy_script.sh
- deploy_script_old.sh
- bak_deploy_script_old_KEEP.sh
- playbook.yml
- playbook2.yml
- playbook3.yml
- adhoc_deploy_script.sh
+ group_vars
- localhost
- localhost_bak
- localhost_old
- localhost_template
+ roles
+ role1
+ tasks
- main.yml
+ templates
- application.yml
- database.yml
- role2
+ tasks
- main.yml
+ templates
- application.yml
- database.yml
- role3
+ tasks
- main.yml
+ templates
- application.yml
- database.yml

There were several versions of old vars files and scripts leftover from the years of non-version-control, and inside the group_vars folder there were sensitive keys that should not be checked into the git repo in plain text. Additionally, the “templates” seemed to exist in different forms in every role, even though only one role used it.

I re-arranged the structure and filtered out some old versions of things to start:

+ app_dev
- README.md
- Vagrantfile
+ provisioning
- web_playbook.yml
- database_playbook.yml
- host.vagrant
+ group_vars
+ local
- local
+ develop
- local
+ staging
- staging
+ production
- production
+ vaulted_vars
- local
- develop
- staging
- production
+ roles
+ role1
+ tasks
- main.yml
+ templates
- application.yml
- database.yml
- role2
+ tasks
- main.yml
- role3
+ tasks
- main.yml
+ scripts
- deploy_script.sh
- vagrant_deploy.sh

Inside the playbooks I lined out the roles in the order they seemed to be run from the deploy_script.sh, so they could be utilized by ansible in the vagrant build process. From there, it was a lot of vagrant up, finding out where it failed this time, and finding a better way to run the tasks (if they were even needed, as often times they were not).

Perhaps the hardest part was figuring out the capistrano deploy part of the deploy process. If you’re not familiar, capistrano is a deployment tool for Ruby, which allows you to remotely deploy to servers. It also does some things like keeping old versions of releases, syncing assets, and migrating the database. For a command as simple as bundle exec cap production deploy (yes, every environment was production to this app, sigh), there was a lot of moving parts to figure out. In the end I got it working by setting a separate “production.rb” file for the cap deploy to use, specifically for vagrant, which allows it to deploy to itself.

# 192.168.67.4 is the vagrant webserver IP I setup in Vagrant
role :app, %w{192.168.67.4}
role :primary, %w{192.168.67.4}
set :branch, 'develop'
set :rails_env, 'production'
server '192.168.67.4', user: 'vagrant', roles: %w{app primary}
set :ssh_options, {:forward_agent => true, keys: ['/path/to/vagrant/ssh/key']}

The trick here is allowing the capistrano deploy to ssh to itself — so make sure your vagrant private key is specified to allow this.

Deploying on AWS

To deploy on AWS, I needed to create an AMI, or image from which new servers could be duplicated in the future. I started with a fairly clean CentOS 7 AMI I created a week or so earlier, and went from there. I used ansible-pull to checkout the correct git repository and branch for the newly-created ansible app code, then used ansible-playbook to work through the app deployment sequence on an actual AWS server. In the original app deploy code I brought down, there were some playbooks that could only be run on AWS (requiring data from the ansible ec2_metadata_facts module to run), so this step also involved troubleshooting issues with these pieces that did not run on local.

After several prototype servers, I determined that the AMI should contain the base packages needed to install Ruby and Passenger (with Nginx), as well as rbenv and ruby itself installed into the correct paths. Then the deploy itself will install any additional packages added to the Gemfile and run the bundle exec cap production deploy, as well as swapping new servers into the ELB (elastic load balancer) on AWS once deemed “healthy.”

This troubleshooting process also required me to copy over the database(s) in use by the old account (turns out this is possible with the “Share” option for RDS snapshots from AWS, so that was blissfully easy), create a new Redis instance, copy over all the s3 assets to a bucket in the new account, and create a Cloudfront instance to serve those assets, with the appropriate security groups to lock all these services down. Last, I updated the vaulted variables in ansible to the new AMIs, RDS instances, Redis instances, and Cloudfront/S3 instances to match the new ones. After verifying things still worked as they should, I saved the AMI for easily-replicable future use.

Still to come

A lot of progress has been made on this app, but there’s more still to come. After thorough testing, we’ll need to switch over the DNS to the new ELB CNAME and run entirely from the new account. And there is pipeline work in the future too — whereas before this app was serving as its own “blue-green” deployment using a “bastion” server of sorts, we’ll now be deploying with AWS CodeDeploy to accomplish the same thing. I’ll be keeping the blog updated as we go. Until then, I can rest easy knowing this app isn’t quite the hot mess I started with.

Rewriting history — git history, that is

If you’ve ever worked with a team of contractors in software development, you may notice some of their (or even your!) commits are made under the incorrect email address or username. If you need to clean things up for compliance or maybe just your own sanity, turns out there’s a fairly easy way to rewrite any of those invalid authors’ bad commits.

Gather your list(s)

Start by checking out all branches of your repository on your local machine — we’ll need to scan them for invalid authors. Here’s a nifty shell script from user octasimo on github that does just that:

#!/bin/bash

for branch in `git branch -a | grep remotes | grep -v HEAD | grep -v master `; do

git branch --track ${branch#remotes/origin/} $branch

done

Once all branches are checked out, we’ll need to scan them for invalid authors. Use this command to get a list of those emails:

git log --all --format='%cE' | sort -u

Now you can sort that list to show only the invalid ones. For example, if my corporate company email is “@company.com” I can use grep to sort out the bad ones like so:

git log --all --format='%cE' | sort -u | grep -v '@company.com'

Now you have a list of all the invalid emails that have been used to commit changes to your repository. From there, make a list determining which proper (corporate) email should have been used.

Pipe the list into the script

For the rewriting of these emails, you can use the script provided by Github themselves. This script asks you for an “old email” (the list of bad emails we found above) and a “correct email” — the email that should have been used to make the commit.

#!/bin/sh

git filter-branch --env-filter '
OLD_EMAIL="[email protected]"
CORRECT_NAME="Your Correct Name"
CORRECT_EMAIL="[email protected]"

if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]
then
export GIT_COMMITTER_NAME="$CORRECT_NAME"
export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]
then
export GIT_AUTHOR_NAME="$CORRECT_NAME"
export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags

To use this script, just insert the invalid email as the value of the “OLD_EMAIL” variable, the correct name for this user as the value for the “CORRECT_NAME” variable, and the corporate email as the value for the “CORRECT_EMAIL” variable.

Quick tip: if you have to rewrite multiple emails as I did, you will want to change that first line to git filter-branch -f

Once you’ve run the script for each email that needs to be rewritten, push your changes to the repository with git, and all should be cleaned up!

Migrating PHP 5.6 to PHP 7.2 with Ansible: DevOps Digest

If you’re hip to the news in the PHP community, you’ve probably heard that as of December 2018, PHP 5.6 (the most widely-used version of PHP) has reached End of Life, and will also no longer receive back-patches for security updates. To top it off, PHP 7 also reached End of Life and end of security support in the same month. That means a wealth of PHP users are now needing to upgrade in a hurry.

Generally speaking, upgrading your website or application to a newer PHP version isn’t quite as easy as it sounds. Sure, you can type the command to install the new version, but that doesn’t offer any guarantee that your site will still work once that update completes. In this article I’ll explain how I went about updating my organization’s apps to PHP 7.2 in a programmatic way, since we use Ansible to deploy apps and build local vagrants.

Start with the Ansible code

As a DevOps Engineer, part of my job is maintaining and updating our Ansible playbooks and deployments. In our Ansible setup, we have a shared repository of playbooks that all our apps use, and then each app also has an Ansible repository with playbooks specific to that app as well. In order to update to PHP 7.2, I had to undertake updating the shared playbooks and the app-specific playbooks. I started with the shared ones.

To start, I looked at the remi repo blog to see how they suggested upgrading PHP. Our shared ansible repository installs the basics – your LAMP stack, or whatever variation of that you may use. So first, I located where our ansible code installed the remi and epel repos.

- name: install remi and epel repo from remote 
yum:
name:
- "{{ remi_release_url }}"
- "{{ epel_release_url }}"
become: true

Notice the place to insert the URL to install the URL from is set as a variable – this means any one of the apps that uses this shared ansible code could set its own value for “remi_release_url” or “epel_release_url” to upgrade to a new version going forward. I set the “default” value for these to the URLs for PHP7 as specified in the remi repo blog.

Next, we get the correct key for the remi repo as specified on the blog:name: get remi key
get_url: url={{ remi_key_url }} dest=/etc/pki/rpm-gpg/RPM-GPG-KEY-remi
become: true

- name: get remi key
  get_url: 
    url: "{{ remi_key_url }}" 
    dest: /etc/pki/rpm-gpg/RPM-GPG-KEY-remi
  become: true 

Notice we’ve also set “remi_key_url” as a variable, so that if an app chooses to define a new PHP version, they can set the correct key to use for that version as well.

Now that we’ve got the right repos and keys installed, we can install packages using yum. But in doing so, we can define the correct remi repo to select from — in our case, remi-php72.

- name: install php7 packages 
yum:
name:
- php
- nginx
- php-fpm
- php-mysql
- php-pdo
- php-mbstring
- php-xml
- php-gd
enablerepo: "remi-{{ php_version }},epel"
become: true

Your list of packages may be the same or different, depending on your app’s requirements. The important thing to note here is another variable: “php_version”. This variable is then set in each of the apps to “php72” for now, and can easily be swapped for “php73” or higher as support ends for those versions in the future.

App-specific Ansible changes

Once I had committed my changes to a branch in the shared ansible code, all that was left was to make slight changes to each of my ansible app repos that used this code.

I started by defining the php_version to “php72” and defining the correct repo URLs and key:

php_version: php72
remi_release_url: "http://rpms.remirepo.net/enterprise/remi-release-6.rpm"
epel_release_url: "https://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarch.rpm"
remi_key_url: "http://rpms.remirepo.net/RPM-GPG-KEY-remi"

This allowed remi to do its thing installing the right versions for the right PHP version.

Next, I went through all the playbooks specific to the app and looked for more yum install sections that might be installing more packages, and ensured they used the “enable_repo” flag with the “remi, remi-{{ php_version }}” value. This means all the additional packages installed for each app will also be installed from the correct remi repo for PHP 7.2.

Last, I ensured our local vagrants built successfully and with no errors using the new PHP version and packages. We ran into very few errors in building the vagrants locally, but the app code itself did need some work, which brings us to the last step.

Update app code

As the DevOps Engineer, I partnered with the lead developer of each application we support to fix any compatibility issues. We use the phpunit tests, as well as phpcs (code sniffing) to detect any issues. We ended up updating our versions of these to check for PHP 7.2 compatibility, and this pointed the developers of each project to the compatibility issues. Some apps certainly had more errors to fix than others, but having the visibility and working vagrants built with Ansible was the key to success.

The other important thing that helped our development teams in this process was having a true local > dev > stage > prod workflow. This allowed us to push to dev, have the developers troubleshoot and fix issues, promote it to staging in a structured release, have QA team members run test suites against it, and finally (only when all is verified as good), push to production. Deploying through all these stages allowed us to work out any kinks before the code made it to production, and it took us about a month from start to finish.


I hope you enjoyed learning about our path to PHP 7! If you have any comments, feedback, or learnings from your journey as well, feel free to leave them in the comments or contact me.

Resolving 502, 503, and 504 errors

If you’ve ever run into 502, 503, or 504 errors (we’ll refer to them as 50x errors from here on out), you probably know how frustrating they can be to troubleshoot. Learn what each means in this article!

What are 50x errors?

Let’s start at the beginning: what does a 50x error mean? According to the HTTP Status Code guide, here’s what each translates to:

502 Bad Gateway: “the server, while acting as a gateway or proxy, received an invalid response from the upstream server“

503 Service Unavailable: “the server is not ready to handle the request.”

504 Gateway Timeout: “the server, while acting as a gateway or proxy, cannot get a response in time.”

Those descriptions, unfortunately, aren’t specific enough to be very useful to most users.

Here’s how I would describe these errors:

A service (whichever service first received the request) attempted to forward it on to somewhere else (proxy/gateway), and didn’t get the response it expected.

The 502 error

In the case of a 502 error, the service which forwarded the request onward received an invalid response. This could mean that the request was killed on the receiving service processing the request, or that the service crashed while processing it. It could also mean it received another response that was considered invalid. The best source to look would be in the logs of the service that received the request (nginx, varnish, etc), and the logs of the upstream service to which it is proxying (apache, php-fpm, etc).

For example, in a current server setup I am managing, I have nginx sitting as essentially a “traffic director” or “reverse proxy” that receives traffic first on the server. It then forwards the request to backend processing service for PHP called php-fpm. When I received a 502 error, I saw an error like this in my nginx error logs:

2019/01/11 08:11:31 [error] 16467#0: *7599 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: 10.0.0.1, server: localhost, request: "GET /example/authenticate?code=qwie2347jerijowerdsb23485763fsiduhwer HTTP/1.1", upstream: "fastcgi://unix:/run/php-fpm/www.sock:", host: "example.com"

This error tells me that nginx passed the request “upstream” to my php-fpm service, and did not receive response headers back (i.e. the request was killed). When looking in the php-fpm error logs, I saw the cause of my issue:

[11-Jan-2019 08:11:31] WARNING: [pool www] child 23857 exited on signal 11 (SIGSEGV) after 33293.155754 seconds from start
[11-Jan-2019 08:11:31] NOTICE: [pool www] child 20246 started

Notice the timestamps are exactly the same, confirming this request caused php-fpm to crash. In our case, the issue was corrupted cache – as soon as the cache files were cleared, the 502 error was gone. However, often times you will need to enable core dumps or strace the process to diagnose further. You can read more about that in my article on Segmentation Faults.

A 502 error could also mean the upstream service killed the process (a timeout for long processes, for example), or if the request is proxying between servers, that the destination server is unreachable.

The 503 error

A 503 error most often means the “upstream” server, or server receiving the request, is unavailable. I’ve most often experienced this error when using load balancers on AWS and Rackspace, and it almost always means that the server configured under the load balancer is out of service.

This happened to me once or twice when building new servers and disabling old ones, without adding the new configuration to the load balancer. The load balancer, with no healthy hosts assigned, receives a 503 error because it could not forward the request to any host.

Luckily this error is easily resolved, as long as you have the proper access to your web management console to edit the load balancer configuration! Simply add a healthy host into the configuration, save, and your change should take effect pretty quickly.

The 504 error

Last but not least, a 504 error means there was a gateway timeout. For services like Cloudflare, which sit in front of your website, this often means that the proxy service (Cloudflare in this example) timed out while waiting for a response from the “origin” server, or the server where your actual website content resides.

On some web hosts, it could also mean your website is receiving too much concurrent traffic. If you are using a service like Apache as a backend for PHP processing, it’s likely you have limited threading capabilities, limiting the number of concurrent requests your server can accommodate. As a result, requests left waiting to be processed could be kicked out of queue, resulting in a 504 error. If your website receives a lot of concurrent traffic, using a solution like nginx with php-fpm is ideal in that it allows for higher concurrency and faster request processing. Introducing caching layers is another way to help requests process more quickly as well. In this situation, note that 504 errors will likely be intermittent as traffic levels vary up and down on your website.

Last, checking your firewall settings is another good step. If the “upstream” service is rejecting the request or simply not allowing it through, it could result in a networking timeout which causes a 504 error for your users. Note that in this scenario, you would see the 504 error consistently rather than intermittently as with high traffic.

Conclusion

To wrap things up, remember that a 50x error indicates that one service is passing a request “upstream” to another service. This could mean two servers talking to each other, or multiple services within the same server. Using the steps above will hopefully help in guiding you to a solution!

Have you encountered other causes of these errors? Have any feedback, suggestions, or tips? Let me know in the comments below, or contact me.

Writing a basic cron job in Linux

Cron jobs are one of the common ways developers schedule repeated tasks on a Linux system. A cron job is a simple way to schedule a script to run on your system on a regular basis. Writing a cron job happens in three basic steps:

  1. Write the script you wish to execute (e.g. mycron.php)
  2. Determine how often you’d like the script to run
  3. Write a crontab for it

Writing the script

Start by writing your script or command. It can be in any language you like, but most often crons are written in bash or PHP. Create your cron.txt or cron.sh file if needed. Here’s a basic PHP file that just prints “hello world” in stdout (standard out):

<?php
echo "hello world";
?>

Or a bash script that does the same thing:

!/usr/bin/env bash
echo "hello world";

Or if you want to have some fun, on a Mac you can use the say command to have your computer speak a line instead.

say -v fred "I love Linux"

You can save the say command as a bash file (cron.sh for example) and invoke it with bash. 

Save your file as the appropriate file extension, then move on to the next step: determining how often the command should run. 

Determining cron schedule

Now that you have your script written, it’s time to make some decisions about how frequently you wish the script to be executed. 

Crontab allows you the freedom to run the script or command every minute, hour, day, month, or any combination thereof. If you want the script to be run at 5pm every 3rd Thursday of the month, or only Wednesday through Friday at 10am daily, that’s possible with cron.

When it comes to deciding a cron schedule, less is more. By that I mean, for the health of your server it’s best to run it as infrequently as possible to maximize your server resources. Keep in mind that the more resource intensive your script is, the more possible it is that your crons could be disrupting to the experience of your end users.

The basic format of a cron is:

* * * * * command to run 

Each of those asterisks has a particular unit of time it represents, however.

  1. minute (0-59) of the hour you wish the command to execute. * means every minute of the hour.
  2. hour (0-23) of the day you wish the command to execute. * means every hour of the day.
  3. day (1-31) of the month you wish the command to execute. * means every day of the month.
  4. month (1-12) of the year you wish the command to execute. * means every/any month of the year.
  5. day of the week (0-6) you wish the command to execute. * means any day of the week.

Using these guidelines you can format your schedule. Below are some examples:

# every minute of every hour/day/month/year
* * * * *

# every hour, on the hour
0 * * * *

# every day at 10am
0 10 * * *

# every Thursday at midnight
0 0 * * 4

# every Monday, Wednesday, and Friday at 3:30pm
30 15 * * 1,3,5

If you want to check your syntax, there’s a handy website called crontab.guru which can help you test out some schedule combinations.

Writing the crontab

The crontab command is how scheduled cron jobs are executed. With crontab we will take the script and the schedule and combine them to actually execute the script or command on the schedule you’d like.

Start by listing any existing cron schedules on your machine with crontab -l

If you’ve never written a cron before, you’ll probably see the following:

$ crontab -l
crontab: no crontab for [yourusername]

Now to edit the crontab, we’ll use crontab -e. This will automatically put you in the vim editor for Linux, so here’s a brief tutorial if you’ve never used it before.

Hit i to enter “insert” mode and copy your schedule first. Then you can either type the command directly after. If your cron script is written in PHP and is located in /var/www/html/cron.php you can use the syntax below:

* * * * * /usr/bin/php /var/www/html/cron.php

Or if you want to use the command right in crontab, you can do that too:

* * * * * echo "I love Linux" >> /var/log/cron-stdout.log

Notice in the command above I also added a log file to store the stdout results of the command. This is an option as well and can help you ensure your cron is running as scheduled! 

And of course, if you wanted to prank a coworker when they leave their Mac unlocked at work:

* * * * * say -v fred "I am buying nachos for everyone at 5pm"

Once you’ve finished editing, hit the esc key to exit “insert” mode, then type wq! to force write and quit the file. You should see a message, “crontab: installing new crontab” after saving. 

… And that’s it! Now your cron will execute on your schedule. Before we part ways, a few words of caution: 

  • Most managed web hosts don’t allow you to edit system crons. Most times this requires root SSH access on the Linux machine. 
  • If you’re using WordPress, you can easily get around this restraint by using WordPress cron, and managing it using a simple tool like WP Crontrol. 
  • Cron is a word that’s easy to mistype. Pro-tip: nobody knows what you want when you’re asking about “corn jobs” 😉

How about you? Any pro-tips for those looking to create a cron job? Leave a comment, or Contact Me. 

  • « Previous Page
  • Page 1
  • Page 2
  • Page 3
  • Page 4
  • …
  • Page 8
  • Next Page »

Footer

Categories

  • Ansible
  • AWS
  • Git
  • Linux
  • Optimization
  • Performance
  • PHP
  • Scalability
  • Security
  • Uncategorized
  • WordPress

Copyright © 2025 · Atmosphere Pro on Genesis Framework · WordPress · Log in