Guide for Version Control with Ignition

If you can guarantee you'll only ever have one project you care about, sure, that's totally reasonable. My second thought is that I don't know if the project system ignores .git directories within the project or not.

My first thought was that if you ever have to change to VCSing multiple projects, it's going to be difficult/annoying to do that in a single transaction and preserve history on all your files. Maybe not a deal breaker - you could always have an archival repo and a brand new repo if you do end up needing to track multiple projects.

If you can guarantee you'll only ever have one project you care about, sure, that's totally reasonable.

Sometimes I will, sometimes I won’t. If it’s a job with multiple projects I’d just make the repo the projects directory and use the .gitignore file like normal. It’s not that much of a pain anyway, so maybe it’s written that way because it’s just best practice to do things the same way every time.

My second thought is that I don't know if the project system ignores .git directories within the project or not.

That’s what gave me pause. I wasn’t sure how that would affect things. It doesn’t seem like it because the .git folder is hidden, but you never know. I guess the only thing to do is try and test, if it’s even worth it, and I’m doubting if it is.

With the release of 8.3, we now have a Version Control Guide on our docs page that may be useful. As part of the team who wrote the guide, it contains a lot of the same information included in the original post here, but updated for 8.3.

6 Likes

Thank you, Eric. This is a very comprehensive guide. I still struggle to see how the “additive approach” can be applied in a simple, practical way. I believe the real key would be integrating Git directly into the Designer. Without that, it seems difficult to fully take advantage of Git’s benefits. In this regard, you might draw some inspiration from SAP’s “transport requests.”

@Eric_Knorr Nice work, I have one recommendation for the project template, add “:rw” to the end of your project volume in your compose file. This allows you to manage your git commits much easier and also allows you to directly edit you gateway scripts in your IDE, so you can use tools like cursor ect.
I haven’t figured the correct way to reload the scripts back into designer, I often just restart the container since it only takes a few seconds in my dev environment.

- ./projects/frontend:/usr/local/bin/ignition/data/projects:rw

@Eric_Knorr , I appreciate the guide. I’m running into an issue that seems tied to the bind mounts I defined for Git. Each time I run docker compose up to recreate a container, the startup takes a long time, and a new temporary user profile is generated—eventually leaving me with a long list of these profiles.

This was my original volumes definition based off of the new guide

volumes:
      # gateway configuration
      - ./services/ignition/config:/usr/local/bin/ignition/data/config
      # gateway projects
      - ./services/ignition/projects:/usr/local/bin/ignition/data/projects

After experimenting, I found that adding a named volume alongside the Git bind mounts fixes the problem. Startup time improves, and no extra temporary user profiles are created.

This works much better…

    volumes:
      - type: volume
        source: gateway_data
        target: /usr/local/bin/ignition/data
      # gateway configuration
      - ./services/ignition/config:/usr/local/bin/ignition/data/config
      # gateway projects
      - ./services/ignition/projects:/usr/local/bin/ignition/data/projects

 volumes:
  gateway_data:

Should this be the way forward?

1 Like

@Craig_webber, when I discard changes in Git for a project and want to restore the original code in the Designer, I first trigger a project scan to update the Gateway.

There are two ways to do this in v8.3:

  1. Use the API:
<ip>:<port>/data/api/v1/scan/projects
  1. Call the system function to rescan the project folder:
system.project.requestScan([timeout])

Once the rescan completes, click the Merge Changes from Gateway button in the Designer (see screenshot below).

This pulls the updated code from the Gateway into the Designer—no container restart required. I hope this helps :slight_smile: !

2 Likes

Hi Andrew, your method indeed does work, unless you destroy that volume. We have an internal ticket (IGN-14241) to streamline this process, but here's a little bit of insight on what is happening:

In order for Ignition to commission properly, the following need to exist:

  • commissioning.json (can be empty {})

  • config/resources/core/ignition/identity-provider/

  • config/resources/core/ignition/user-source/

  • config/resources/core/ignition/security-properties/

    Note: The resources in config can be overridden by a deployment mode, but the resource definition just has to exist somewhere)

Your approach works because the named volume persists commissioning.json and the core resource directories between container recreations. When you only bind-mount config/, these files don't exist yet on first startup, causing Ignition to create temporary profiles while attempting to commission.

A more robust solution is to use a bootstrap service that seeds the required files before Ignition starts. This way, you don't need to rely on a persistent volume that could be accidentally destroyed:

yaml

services:
  bootstrap:
    image: inductiveautomation/ignition:8.3.0
    entrypoint: ["/bin/bash", "-c"]
    volumes:
      - gateway-data:/gateway-data
    command:
      - |
        if [ ! -f /gateway-data/.ignition-seed-complete ]; then
          echo "Seeding gateway data..." ;
          touch /gateway-data/.ignition-seed-complete ;
          cp -dpR /usr/local/bin/ignition/data/* /gateway-data/ ;
          echo "{}" > /gateway-data/commissioning.json ;
        fi ;
        echo "Bootstrap completed successfully."

  ignition:
    image: inductiveautomation/ignition:8.3.0
    depends_on:
      bootstrap:
        condition: service_completed_successfully
    volumes:
      - gateway-data:/usr/local/bin/ignition/data
      - ./services/ignition/config:/usr/local/bin/ignition/data/config
      - ./services/ignition/projects:/usr/local/bin/ignition/data/projects

volumes:
  gateway-data:
4 Likes

Eric, thanks for following up. I tried your approach, and it does work. On the first run, the Ignition Gateway fails to start up in the container, but after a restart everything comes up fine.

After restart I see this:

My only dislike with this solution is ending up with a stopped container for the bootstrap:
image

When I re-read your post, I realized the only thing missing for Ignition to properly commission was the commissioning.json file. To address this, I created a commissioning.json containing just {} and referenced it in my compose file:

x-default-logging: &default-logging
  logging:
    options:
      max-size: '100m'
      max-file: '5'
    driver: json-file

x-ignition-opts: &ignition-opts
  <<: *default-logging
  image: inductiveautomation/ignition:8.3.0

services:
  frontend-gateway:
    <<: *ignition-opts
    container_name: ignition-83test
    hostname: test_gateway
    ports:
      - "8588:8088"
      - "8543:8043"
      - "8560:8060"
    volumes:
      # commissioning.json
      - ./services/ignition/data/commissioning.json:/usr/local/bin/ignition/data/commissioning.json
      # gateway configuration
      - ./services/ignition/config:/usr/local/bin/ignition/data/config
      # gateway projects
      - ./services/ignition/projects:/usr/local/bin/ignition/data/projects

With this setup, the first run still fails during commissioning but starts cleanly after a restart. From then on, there are no issues with the user profile when re-creating the gateway container, and I can safely commit the commissioning.json file into Git for team reuse. This also avoids the problem of losing a persisted volume, since a persisted volume is not in use in this approach.

Overall, this feels like a solid alternative approach :man_shrugging:, and I’m curious to see how things change once internal ticket IGN-14241 is resolved.

2 Likes

Yup, this is also a totally valid path forward!

1 Like

Craig,

I realized that my docker compose setup did not allow for updating files in the container. I needed to change file permissions to enable Git operations. In my docker compose file, I now reference an entrypoint.sh script that updates the file permissions in the container before Ignition starts.

Docker Compose changes:

x-default-logging:
  &default-logging
  logging:
    options:
      max-size: '100m'
      max-file: '5'
    driver: json-file

x-ignition-opts:
  &ignition-opts
  <<: *default-logging
  image: inductiveautomation/ignition:8.3.0

services:
  frontend-gateway:
    <<: *ignition-opts
    container_name: ignition-83test
    hostname: test_gateway
    ports:
      - "8588:8088" 
      - "8543:8043"  
      - "8560:8060"
  
    entrypoint: ["/bin/sh", "/custom-entrypoint.sh"]
    command: >
      -n test_gateway
      -m 4096
      wrapper.java.initmemory=4096
      wrapper.java.maxmemory=4096
      -XX:MaxGCPauseMillis=100
  
    volumes:
      - ./entrypoint.sh:/custom-entrypoint.sh:ro
    # commissioning.json
      - ./services/ignition/data/commissioning.json:/usr/local/bin/ignition/data/commissioning.json
    # gateway configuration
      - ./services/ignition/config:/usr/local/bin/ignition/data/config
    # gateway projects
      - ./services/ignition/projects:/usr/local/bin/ignition/data/projects
          

Entrypoint script (entrypoint.sh):

#!/bin/sh

echo "Setting permissions on specific folders..."

# Projects folder - needs write access for git operations
chmod -R 777 /usr/local/bin/ignition/data/projects || true

# Config folder - needs write access for git operations
chmod -R 777 /usr/local/bin/ignition/data/config || true

echo "Starting Ignition..."
exec /usr/local/bin/docker-entrypoint.sh "$@"

With these changes, I can now update container files through Git operations. For example, when I discard changes in Git, it properly reverts the changes in the container. I can then trigger a project scan to update the project in the gateway, and finally use Merge Changes from Gateway to sync those changes into the Designer.

Hi @andrew.brauer , could you please share your full architecture and guide on the implementation you did?

I’m also interested in this docker+git combo.

Thanks

Hi @John_S1,

I have been meaning to write up a simple guide, hopefully this answers your question. This was written in the context of how I develop with docker and Git. This is also written for developing against Ignition v8.3.x. Here's my first pass...

Environment setup for docker

This architecture provides a foundational setup for developers to work locally on Ignition projects while leveraging Git for version control.

The host machine OS is Windows 11 24H2 with the following software tools:

  • WSL2 Ubuntu 24.04.1 LTS
  • Rancher Desktop v1.20.0 - Container Engine-> dockerd(moby) - this allows for VS Code or Cursor to interact with the docker environment
  • Cursor v1.7.40 or Visual Studio Code v1.105.0 as the IDE for docker compose and Git commands
  • Container Tools (IDE extension for VS Code or Cursor) - this extension will hook into the docker engine for interaction in the IDE
  • Git for Windows

In this example I have a Git repo for the docker compose file and a separate Git repo for the Ignition gateway configuration and project data. Below is a base file structure for docker.

├── .git
├── db-backups
├── external modules
└── envFile
   └── MSSQL.env
├── secrets
├── docker-compose.yml
├── entrypoint.sh
├── .gitignore
└── services
   └── ignition
      ├── .git
      ├── config
      ├── data
      ├── projects
      ├── .gitattributes
      ├── .gitignore
      └──  README.md
  • db-backups - this folder is for database backups to restore into container
  • external-modules - this folder is for .jar files or ignition modules to include in the container
  • envFile - this folder contains the environment variable files for configuring container environment settings
  • secrets - this folder contains passwords for the containers, but this is not saved into the Git repo
  • services/ignition/data - contains a commissioning.json file to satisfy the Ignition service on startup
  • service/ignition/config - contains the gateway configuration data
  • service/ignition/projects - contains the gateway projects
  • .gitignore - is for the docker compose files in the docker compose Git repo
  • service/ignition/.gitignore - is for the ignition gateway files in the ignition Git repo

Git Configuration Files

  • .gitignore - Configured for both Docker compose files and Ignition gateway files
  • .gitattributes - Handles line ending normalization for cross-platform compatibility
  • Separate Git repositories for Docker compose and Ignition configuration

Create a Git repository in Azure DevOps, GitHub, or GitLab this depends on the development needs.

Create a local Git repository in the path and link it to a remote Git repo similar to example below

git init
git add .
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/your_git_repo.git
git push -u origin main

Example docker compose file

x-default-logging:
  &default-logging
  logging:
    options:
      max-size: '100m'
      max-file: '5'
    driver: json-file

x-ignition-opts:
  &ignition-opts
  <<: *default-logging
  image: inductiveautomation/ignition:8.3.0

services:
  frontend-gateway:
    <<: *ignition-opts
    container_name: ignition-83test
    hostname: test_gateway
    ports:
      - "8588:8088" 
      - "8543:8043"  
      - "8560:8060"
  
    entrypoint: ["/bin/sh", "/custom-entrypoint.sh"]
    command: >
      -n test_gateway
      -m 4096
      wrapper.java.initmemory=4096
      wrapper.java.maxmemory=4096
      -XX:+UseG1GC
      -XX:MaxGCPauseMillis=100
      
    environment:
      - GATEWAY_ADMIN_PASSWORD_FILE=/run/secrets/gateway-admin-password
      - ACCEPT_IGNITION_EULA=Y
      - IGNITION_EDITION=standard
    depends_on:
      - rabbitmq

    volumes:
      - ./entrypoint.sh:/custom-entrypoint.sh:ro
    # commissioning.json
      - ./services/ignition/data/commissioning.json:/usr/local/bin/ignition/data/commissioning.json
    # gateway configuration
      - ./services/ignition/config:/usr/local/bin/ignition/data/config
    # gateway projects
      - ./services/ignition/projects:/usr/local/bin/ignition/data/projects
    # rabbitMQ library for listening to RabbitMQ queue for event updates 
      - ./external-modules/amqp-client-5.26.0.jar:/usr/local/bin/ignition/lib/core/common/amqp-client-5.26.0.jar
      
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

  db:
    image:  mcr.microsoft.com/mssql/server:2022-latest
    <<: *default-logging
    ports:
      # Note that the 1433 port doesn't need to be published here for the gateway container to connect, 
      # only for external connectivity to the database.
      - 1433:1433
    container_name: sql-server-db
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - type: volume
        source: db_data
        target: /var/opt/mssql
      - type: bind
        source: ./db-backups
        target: /backups

    environment:
      - MSSQL_SA_PASSWORD_FILE=/run/secrets/mssql-sa-password
    secrets:
      - mssql-sa-password
    env_file:
      - ./envFile/MSSQL/Settings.env
    
  rabbitmq:
    image: rabbitmq:3-management
    <<: *default-logging
    container_name: rabbitmq
    hostname: my-rabbit
    environment:
      - RABBITMQ_DEFAULT_USER=admin
    extra_hosts:
      - "host.docker.internal:host-gateway"  # this is needed to access the RabbitMQ AMQP 
    ports:
      - "5672:5672"     # AMQP (default RabbitMQ)
      - "15672:15672"   # RabbitMQ Management UI
    volumes:
      - ./data-rabbitmq:/var/lib/rabbitmq
      - ./entrypoint-rabbitmq.sh:/usr/local/bin/entrypoint-rabbitmq.sh:ro
    secrets:
      - rabbitmq-admin-password
    entrypoint: ["/usr/local/bin/entrypoint-rabbitmq.sh"]

secrets:
  gateway-admin-password:
    file: ./secrets/GATEWAY_ADMIN_PASSWORD
  mssql-sa-password:
    file: ./secrets/MSSQL_SA_PASSWORD
  rabbitmq-admin-password:
    file: ./secrets/RABBITMQ_PASSWORD
volumes:
  db_data:

5 Likes

Thank you, this is nice, I will try to make use of the same and let you know, i was also planning to provide a small guidebook kind of thing after i did it completely.
my plan is to have a

OS: RHEL

Install Docker

Use docker desktop for easy access or even Portainer

install ignition, postgres, gitlab

Do we need to install the gitlab runner for this?

I’m planning to do it as a Dev and staging for ignition, and wanted to have the git to be involved for that process. For 8.3, since we have everything in the folder format, I guess this will be helpful.

A small question, @andrew.brauer do you have any comments on this plan? Something that i should have to keep in mind for this implementation or any challenge?

I’m buidling a ‘KISS' Portainer ‘GitOps’ edge ‘stack' + DevOps for project-level repo CD into named volumes. Would be nice if i could deploy an “empty” ignition container with separate config/project named/anonymous volumes and clone into them afterwards with a git-sync sidecar, but that’s not possible ATM. Criticism welcome of course

thoughts:

  • mounting empty volume to /projects fails with unable to create .resources dir
    • guessing due to similar issue above, data/* dir init doesn’t fire if it exists, assuming pre-seeding would work
  • named volumes/PVCs are best practice in orchestrated environments - might as well avoid bind-mounts in dev as well - the fact that “Advanced” section of the guide provides bind-mount examples alongside mentions of GitOps tools like Argo/Flux is surprising to me (though I’m admittedly a k8s noob, maybe I’m missing something)
    • historically the more OT-accessible Portainer didn’t support relative-path bind volume mounts for gitOps-based ‘stack’ deployment, without git, must seed the docker host with bind mount source data manually/via CD tools

well here’s some potential AI hallucination on the subject indicating it might not be as easy as i had hoped:

That .resources file failure is the tell-tale symptom: Ignition expects the projects/ folder to live inside the same volume as the rest of /usr/local/bin/ignition/data because it writes cross-directory metadata (.resources, project.json, etc.) using relative paths and Java NIO file handles. When you split projects/ into a second mounted volume, Ignition’s gateway service sees a mount boundary and can’t atomically create or lock the .resources file.

so it’s looking like no matter what I must clone/stage the project-level repos somewhere else like /tmp and then rsync them over (if using git_sync sidecar)

alternative suggestions welcome