What started as a quick fix to switch from mail to smtp in a config file ended up becoming a five-day journey into secure, repeatable, real-world DevOps practices. This post outlines that journey โ€” the lessons learned, the tools explored, and why I'm actually glad I "took the long way around."


๐Ÿš€ The Starting Point: Just Change a Mail Protocol

I began with a self-hosted deployment of Easy!Appointments, trying to force it to use SMTP instead of the default mail protocol.

Why?

The default mail protocol relies on the underlying PHP mail() function, which in many server environments:

  • Isn't configured properly
  • Can result in mail getting silently dropped or marked as spam
  • Lacks support for authentication or encryption (TLS/SSL)

For reliability and control, I wanted the application to use SMTP with authentication.


๐Ÿ”ง My Initial Plan

The initial plan? Hardcode new settings into email.php and config.php, and copy them into the container during image build via Dockerfile:

COPY config.php /var/www/html/config.php
COPY email.php /var/www/html/application/config/email.php

It worked โ€” but it also meant:

  • My database and SMTP credentials were now stored in the image
  • And those files were also committed to my Git repo (ouch)

Yes, it was a private repo. But still โ€” I knew I was cutting corners.

I quickly realised: this is not sustainable, nor secure.


๐Ÿ” From Hardcoded to Runtime Environment

This led me to re-architect the deployment using environment variables and a custom docker-entrypoint.sh script.

At startup, this script would:

  • Read values from a .env file
  • Dynamically generate config.php and email.php

Now I could inject credentials securely at runtime, without rebuilding the image or leaking secrets into version control.

cat <<EOF > /var/www/html/config.php
<?php
class Config {
  const BASE_URL = "${BASE_URL}";
  const DB_HOST  = "${DB_HOST}";
  ...
}
EOF

๐Ÿ“ฆ Building My First Custom Image

This project was also my first real experience creating and publishing a custom Docker image. I started by hosting my code and Dockerfile in a private self-hosted Gitea instance, where I could iterate and test locally.

Once I had the docker-entrypoint.sh working to generate the dynamic configs, I needed to make the image available for deployment. This is where I branched out into using GitHub Container Registry (GHCR) โ€” another first.

I created a GitHub repository specifically for the container image, added a GitHub Actions workflow to build and push it to GHCR, and ensured I could pull the tagged images securely from my deployment environment.

This involved:

  • Creating and managing a GitHub personal access token (PAT) with correct scopes
  • Debugging image visibility and authentication issues
  • Learning how to tag and version releases clearly for future deployments

It turned into a real hands-on introduction to CI/CD workflows, container registries, and environment-secure image publishing.


๐Ÿณ Docker, GHCR, and Git

Along the way, I:

  • Rebuilt the Docker image multiple times to include the new script
  • Learned to tag and push images to GitHub Container Registry (GHCR)
  • Set up GitHub Actions to automate the build process
  • Had to reset and reconnect local Git branches and force-push history when needed

This wasn't just tweaking config anymore โ€” it was full-cycle DevOps:

  • Build โ†’ Push โ†’ Deploy โ†’ Debug โ†’ Document

๐Ÿง  What I Learned

  • How .env files and entrypoint scripts can securely inject secrets at runtime
  • Why const vs public static matters in PHP apps (especially legacy ones)
  • How GHCR handles authentication, permissions, and visibility
  • That Portainer has limitations for .env usage unless you're uploading manually
  • How to write proper release notes and version image tags
  • How to manage a custom image repository from development through to production deployment

๐Ÿ› ๏ธ Real-World Outcome

By the end of the journey, I had:

  • A secure, repeatable containerized deployment
  • Full SMTP support with user-defined variables
  • Clean, versioned builds pushed to GHCR
  • Configs generated dynamically at runtime
  • A better understanding of Git/GHCR authentication flows
  • My first fully custom-built and published Docker image

๐Ÿ’ญ Reflection: Was It Overkill?

I asked myself this several times. All I wanted to do was change mail to smtp.

But that friction led to:

  • Understanding how to do it the right way
  • A scalable pattern I can now apply to other apps
  • A deep dive into config management, secrets, and Docker best practices

Using ChatGPT throughout felt like having a senior engineer pair-programming with me โ€” not cheating. I still had to troubleshoot, rebuild, and think critically at each stage.


๐Ÿ”ฎ Whatโ€™s Next

  • Expand this pattern into more services I self-host
  • Explore Gitea webhooks or CI pipelines for self-hosted automation
  • Keep tracking my learning on opensourceitsolutions.co.uk via Ghost
  • Apply this all to my AZ-400 DevOps certification study path

If you've ever found yourself solving one small problem and ending up with a full-on infrastructure revamp โ€” you're not alone.

And if you havenโ€™t yet? Just wait. ๐Ÿ˜„


๐Ÿ™Œ What This Deployment Was For

A big thank you to Alex Tselegidis, the creator of Easy!Appointments, for building such a flexible and self-hostable appointment booking solution.

This entire deployment journey wasnโ€™t just for fun โ€” Iโ€™ve implemented Easy!Appointments as part of my own startup, Open Source IT Solutions.

We now use it to offer free 30-minute consultation sessions where we help individuals and businesses explore how open source software can simplify, secure, or scale their day-to-day operations.

Whether youโ€™re a new startup or an established business looking to migrate from expensive proprietary systems, Iโ€™d love to chat.

Book a free session here โ†’ bookings.opensourceitsolutions.co.uk

I also maintain a growing list of open source software I recommend and implement.

Feel free to reach out if youโ€™re curious how this can fit your business.

This hands-on deployment also tied in perfectly with my AZ-400 certification journey, which I recently started. Working on a real-world implementation like this โ€” rather than following only the static examples from Microsoft Learn โ€” helped me absorb the concepts more deeply. It's been a winโ€“win: practical experience that aligns directly with my certification goals.