The Development Flow for a php-local.ini Change
Here is the step-by-step process for a developer:
1. The Developer Edits the Config (Locally)
The developer needs to modify the php-local.ini file. They do this on their local machine within their cloned Git repository:
- They locate the file:
my-hugo-site/infrastructure/nginx/php/php-local.ini - They make their desired change (e.g., changing
upload_max_filesize = 2Mto8M).
2. Local Verification (Testing the Change)
The developer needs to verify that the Nginx container picks up the new configuration locally. Since they are using bind mounts for local development (Option 1 for dev), this happens automatically:
- They run
docker compose upfor their local dev environment. - Because the
php-local.iniis bound from the host into the container at runtime, the running Nginx container instantly uses the new setting (or needs a quick restart to pick it up, depending on how Nginx/PHP-FPM reloads configs).
3. Commit and Push the Change
Once they verify the change locally, they commit it to the Git repository:
git add infrastructure/nginx/php/php-local.inigit commit -m "feat: increase max file upload size in php-local.ini"git push origin feature/php-size-increase
4. The CI/CD Pipeline & QA (Automated)
The push triggers the pipeline you defined earlier:
- The pipeline checks out the entire repo, including the updated
.inifile. - It runs the
hugo buildcommand. - It executes the
docker build -f Dockerfile.Deploy ...command. TheCOPYcommand in the Dockerfile bakes the updated.inifile into the new Nginx Docker image. - It pushes the new image (
myregistry/site-a:qa-latest) to the registry. - It automatically deploys this new image to the Unraid QA/Staging environment.
5. Verification on Staging/QA
The QA team verifies the change on the centralized Unraid QA server. The new container running there is guaranteed to have the correct .ini file because it was baked into the image artifact that was just tested and pushed.
Summary
The development flow is:
Edit locally -> Verify locally (via bind mount) -> Commit to Git -> Pipeline builds new image with baked-in changes -> Deploy new image everywhere.
This workflow provides an auditable, versioned history for every single file that makes up your application, including server configuration files.
Possible Hugo folder structure to include nginx config
my-hugo-site/
├── content/ # Hugo content
├── layouts/ # Hugo layouts
├── themes/ # Hugo themes (submodules)
├── hugo.toml # Hugo config
├── Dockerfile.Deploy # Dockerfile for Nginx production image
├── public/ # (gitignore this) built output
└── infrastructure/ # <-- NEW FOLDER FOR SERVER CONFIG
└── nginx/
├── nginx.conf
├── site-confs/
└── php/
└── php-local.ini
Add Dockerfile.Deploy to root Hugo repo
To make this work, the configuration files must also be managed in your Git repository.
Update Dockerfile.Deploy: Add a copy command for the config files:
Store Config in Git: Create a config/nginx directory in your Hugo Git repository and commit all necessary .conf and .ini files there.
<em># Dockerfile.Deploy in the root of your Git repo</em>
FROM lscr.io/linuxserver/nginx
ARG PUID=99
ARG PGID=100
<em># Copy the custom config files from the Git repo into the image</em>
COPY ./config/nginx/php-local.ini /config/php/php-local.ini
COPY ./config/nginx/site-confs/mysite.conf /config/nginx/site-confs/mysite.conf
<em># Copy the built Hugo static content (as before)</em>
COPY ./public /config/www
<em># Set permissions</em>
RUN chown -R ${PUID}:${PGID} /config
<em># No CMD needed, inherits from base image</em>
The Flow of Content to the New Server
The website content is encapsulated within the Docker image itself.
| Stage | Location of Content | Action |
|---|---|---|
| Development | Developer’s Laptop (Local Git Repo) | Developer writes code/content. |
| Build Machine | CI/CD Runner’s Temporary Workspace | Hugo build occurs; content is compiled. |
| Packaging | Docker Image | Content is baked into the new Docker image via COPY command. |
| Distribution | Container Registry (e.g., Docker Hub) | Image is PUSHed to the registry. |
| New Server (ESXi/Unraid) | New Server’s Docker Engine | Image is PULLed from the registry. |
| Running Server | Inside the new Nginx Container | The site is served from the image’s internal filesystem. |
How to Get the Image onto the New Server
On your new ESXi-hosted server (or Unraid), the process is a single command:
docker pull myregistry/site-a:qa-latest
This command downloads the entire image from the registry to the new server’s local Docker image store.
Once the image is pulled, you run it using the appropriate docker run command (using bind mounts or named volumes for the configuration as discussed previously):
docker run -d --name='nginx_site_a' ... 'myregistry/site-a:qa-latest'
Summary
The Docker image registry acts as your content distribution system.
- The content is no longer separate data that needs
git pullorrsync. - It is an integral part of your deployed application artifact.
This is why this approach is considered more reliable and efficient: you simply tell the new server which packaged version of your entire website to run.
Here is where to store the Dockerfile and how to structure the build process correctly:
1. Where to Store the Dockerfile.Deploy
Store the Dockerfile.Deploy within your Git repository (e.g., in the root directory of your Hugo project). This allows your CI/CD pipeline to access it as part of the source code checkout process.
You should name it something specific like Dockerfile.Deploy so it’s not confused with the standard Dockerfile that might be used for other purposes.
2. Correcting the Paths in Dockerfile.Deploy
The paths inside a Dockerfile during the COPY command relate to two things:
- The local file system of the machine running the
docker buildcommand (the CI/CD runner). - The target file system inside the new Docker image.
Your proposed paths use Unraid host paths (/mnt/user/appdata/...), which the CI/CD runner does not have access to.
You must revert to internal, relative paths that work within the isolated context of the build environment:
<em># Dockerfile.Deploy</em>
<em># Stored in the root of your Hugo Git repo</em>
FROM lscr.io/linuxserver/nginx
<em># Define the user/group for file ownership within the image</em>
ARG PUID=99
ARG PGID=100
<em># The LSIO image expects files in /config/www internally, not /mnt/user/appdata/nginx/www</em>
RUN rm -rf /config/www/*
<em># This COPY command takes the './public' directory from the CI runner's local workspace</em>
<em># and puts it into the '/config/www' directory *inside* the new image.</em>
COPY ./public /config/www
<em># Ensure correct permissions within the image</em>
RUN chown -R ${PUID}:${PGID} /config/www
3. How to Trigger and Execute This Build (CI/CD Pipeline Script)
The build process is triggered by the git push event in your CI/CD pipeline (e.g., GitHub Actions YAML file). The script needs to manage the workspace and execute the commands in the correct order:
<em># Example CI/CD Pipeline Steps (Simplified conceptual view)</em>
steps:
- name: Checkout Repository
uses: actions/checkout@v3
with:
submodules: true <em># Ensures themes are available</em>
- name: Build Hugo Static Site
<em># This runs a temp Hugo container to generate the './public' folder in the workspace</em>
run: |
docker run --rm -v $(pwd):/src hugomods/hugo:exts hugo build -D
- name: Build Custom Nginx Docker Image
<em># This uses the Dockerfile.Deploy we defined above</em>
run: |
docker build -f Dockerfile.Deploy -t myregistry/site-a:qa-latest .
- name: Push Image to Registry
<em># Requires login to registry first</em>
run: |
docker push myregistry/site-a:qa-latest
- name: Deploy to Unraid
<em># The final step: SSH into Unraid to pull and run the NEW image</em>
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.UNRAID_HOST }}
username: ${{ secrets.UNRAID_USER }}
key: ${{ secrets.UNRAID_SSH_KEY }}
script: |
docker pull myregistry/site-a:qa-latest
docker stop nginx_site_a || true
docker rm nginx_site_a || true
# Run the new container with your specific Unraid port/volume config
docker run -d --name='nginx_site_a' -p '8013:80/tcp' -v '/mnt/user/appdata/nginx_site_a_config':'/config':'rw' myregistry/site-a:qa-latest
