Skip to main content

Git Submodules: A Practical Guide for Managing Multiple GitHub Repositories from a Master Repository

· 12 min read
Sivabharathy

When working on a product with multiple independent repositories, it is common to have separate repositories for the backend, frontend, mobile application, smart contracts, admin panel, or shared services.

For example:

Product Master Repository

├── backend
├── frontend
├── mobile
├── contracts
└── admin

One approach is to combine everything into a monorepo. Another approach is to keep each project as an independent repository and use Git submodules to bring them together under a master repository.

Git submodules are useful when you want to maintain repository independence while still having a single place that represents the complete product.

This article explains how Git submodules work, how to manage branches, how to update repositories, and the commands I use most often.

What is a Git Submodule?

A Git submodule is a Git repository embedded inside another Git repository.

Imagine you have these independent repositories:

github.com/example/backend
github.com/example/frontend
github.com/example/mobile
github.com/example/contracts

You can create another repository:

product-master/
├── backend/
├── frontend/
├── mobile/
└── contracts/

The directories are not ordinary folders. Each one is connected to its own Git repository.

The master repository simply keeps track of which commit of each submodule should be used.

For example:

product-master

├── backend → commit 82a31f
├── frontend → commit 71bc92
├── mobile → commit 19cd83
└── contracts → commit 52de11

This is an important concept.

The master repository does not store the complete Git history of those repositories. It stores references to specific commits.

Why Use Git Submodules?

Git submodules are particularly useful when repositories need to remain independent.

For example, a company might have:

Platform
├── API
├── Web Application
├── Mobile Application
├── Admin Dashboard
├── Smart Contracts
└── Notification Service

Each project may have:

  • Its own development team
  • Its own release cycle
  • Its own CI/CD pipeline
  • Its own branch strategy
  • Its own GitHub permissions
  • Its own deployment process

At the same time, having a master repository can make it easier to understand the complete product structure.

This gives you a hybrid architecture:

             Master Repository

┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
Backend Frontend Contracts
│ │ │
Git Repo Git Repo Git Repo

Creating a Master Repository

First create your master repository normally:

mkdir product-master
cd product-master

git init
git remote add origin https://github.com/example/product-master.git

Create your initial commit:

git add .
git commit -m "Initial commit"
git push -u origin main

Now you can start adding independent repositories.

Adding a Repository as a Submodule

The basic syntax is:

git submodule add <repository-url> <directory>

For example:

git submodule add https://github.com/example/backend.git backend

Another repository:

git submodule add https://github.com/example/frontend.git frontend

And another:

git submodule add https://github.com/example/mobile.git mobile

Your structure becomes:

product-master/
├── backend/
├── frontend/
└── mobile/

Git also creates a file called:

.gitmodules

Understanding .gitmodules

The .gitmodules file contains the configuration of your submodules.

It may look like:

[submodule "backend"]
path = backend
url = https://github.com/example/backend.git

[submodule "frontend"]
path = frontend
url = https://github.com/example/frontend.git

[submodule "mobile"]
path = mobile
url = https://github.com/example/mobile.git

The important properties are:

  • path — where the repository exists inside the master repository
  • url — the actual Git repository

Commit this file:

git add .gitmodules
git commit -m "Add project repositories"
git push

Cloning a Repository with Submodules

This is one of the most important commands to remember.

Instead of:

git clone https://github.com/example/product-master.git

use:

git clone --recurse-submodules https://github.com/example/product-master.git

Git will clone the master repository and initialize all its submodules.

If you already cloned the master repository without submodules, run:

git submodule update --init --recursive

This will initialize and download the submodules.

Checking Submodule Status

To see all submodules:

git submodule

You can also use:

git submodule status

You might see:

82a31f backend
71bc92 frontend
19cd83 mobile

These values are the commit SHAs currently referenced by the master repository.

The Most Important Concept: Submodules Track Commits

This is probably the most confusing part when first working with submodules.

Suppose the backend repository has:

main

├── A
├── B
├── C
└── D

The master repository might currently point to:

backend → commit C

Even if commit D exists in the backend repository, the master repository still points to C.

This is intentional.

It means the master repository can represent an exact version of the entire product.

For example:

Product Release 2.0

Backend → 82a31f
Frontend → 71bc92
Mobile → 19cd83
Contracts → 52de11

Someone can clone that master repository six months later and get exactly those versions.

This makes submodules particularly useful for release management.

Switching Branches Inside a Submodule

A submodule is a real Git repository.

You can enter it:

cd backend

Check branches:

git branch -a

Switch to another branch:

git switch develop

Or:

git switch staging

You can work with it exactly like any other Git repository.

For example:

git switch develop
git pull origin develop

Make changes:

git add .
git commit -m "Implement new feature"
git push origin develop

Then return to the master repository:

cd ..

An Important Detail When Switching Branches

Suppose the master repository currently points to:

backend → commit A

You enter the backend repository and switch to a branch containing:

commit B

The master repository will now see the submodule as changed.

Run:

git status

You may see:

modified: backend (new commits)

This happens because the master repository was pointing to commit A, but the submodule is now at commit B.

If you want the master repository to use commit B:

git add backend
git commit -m "Update backend version"
git push

The master repository now points to B.

Configuring a Branch for a Submodule

You can tell Git which branch a submodule should follow.

For example:

[submodule "backend"]
path = backend
url = https://github.com/example/backend.git
branch = develop

Now:

git submodule update --remote backend

will use the configured branch when updating the submodule.

You can configure different branches for different repositories:

[submodule "backend"]
path = backend
url = https://github.com/example/backend.git
branch = develop

[submodule "frontend"]
path = frontend
url = https://github.com/example/frontend.git
branch = staging

[submodule "contracts"]
path = contracts
url = https://github.com/example/contracts.git
branch = main

This allows:

Backend    → develop
Frontend → staging
Contracts → main

Updating a Single Submodule

If you only want to update the backend:

git submodule update --remote backend

You don't have to update all submodules.

For example:

backend     → update
frontend → unchanged
mobile → unchanged
contracts → unchanged

After updating:

git status

Then commit the updated submodule reference:

git add backend
git commit -m "Update backend"
git push

Updating All Submodules

To update all configured submodules:

git submodule update --remote

For nested submodules:

git submodule update --remote --recursive

Be careful with this approach if your master repository is being used as a production release manifest. Updating everything at once may move several components to newer commits.

Pulling Changes from the Master Repository

A common workflow is:

git pull
git submodule update --init --recursive

You can also use:

git pull --recurse-submodules

For teams working heavily with submodules, this is worth understanding because pulling the master repository and updating the submodules are related but separate operations.

Running Commands Across All Submodules

One of my favorite Git submodule commands is:

git submodule foreach git status

Git will execute git status inside every submodule.

You can also run:

git submodule foreach git fetch

Or:

git submodule foreach 'git branch --show-current'

You can even run project commands:

git submodule foreach 'npm install'

This can be very useful when managing a large number of repositories.

Checking Submodule Differences

If a submodule has changed:

git status

may show:

modified: backend (new commits)

To understand what changed:

git diff --submodule

You can also configure Git to display submodule commit history in diffs:

git config --global diff.submodule log

Changing a Submodule Repository URL

If the repository URL changes, use:

git submodule set-url backend https://github.com/example/new-backend.git

Then synchronize:

git submodule sync

For nested repositories:

git submodule sync --recursive

Removing a Submodule

If you no longer need a repository:

git submodule deinit -f backend

Then:

git rm -f backend

Finally:

git commit -m "Remove backend submodule"
git push

Working with Private Repositories

Submodules work with private GitHub repositories as well.

For teams, SSH is often convenient:

git submodule add git@github.com:example/backend.git backend

You can verify your GitHub SSH authentication with:

ssh -T git@github.com

Every developer who clones the master repository needs appropriate access to the private submodule repositories.

This is an important consideration when designing the permissions for your GitHub organization.

Detached HEAD in Submodules

Sometimes you enter a submodule and see:

HEAD detached at 82a31f

This is not necessarily an error.

It happens because the master repository tells the submodule: check out this exact commit.

That is how Git maintains reproducible versions.

If you want to actively develop on a branch:

git switch develop

Then continue working normally.

A Practical Development Workflow

A typical workflow might look like this.

Clone the master repository:

git clone --recurse-submodules https://github.com/example/product-master.git
cd product-master

Check everything:

git submodule status

Go to a project:

cd backend

Switch to your development branch:

git switch develop

Pull the latest changes:

git pull origin develop

Develop and commit:

git add .
git commit -m "Implement payment integration"
git push origin develop

Return to the master repository:

cd ..

Check the master repository:

git status

You will see that the submodule reference changed.

Commit that reference:

git add backend
git commit -m "Update backend submodule"
git push

This gives you two separate Git histories:

Backend Repository

└── Payment integration commit


Backend commit X

Master Repository

└── Points backend to commit X

Production and Release Management

This is where Git submodules become particularly useful.

Imagine a production release:

Product v2.5

Backend → 8a31f2
Frontend → 72bc91
Mobile → 19cd83
Contracts → 91de32

The master repository records these exact versions.

If the backend team releases another version tomorrow:

Backend → abc123

the production master repository doesn't automatically change.

You explicitly update it:

git add backend
git commit -m "Upgrade backend to abc123"
git push

This gives the master repository a role similar to a release manifest.

That can be a very clean architecture for systems where each component is developed and deployed independently.

Git Submodule Cheatsheet

Here is the short version I keep handy.

Add

git submodule add <url> <directory>

Clone with submodules

git clone --recurse-submodules <url>

Initialize existing submodules

git submodule update --init --recursive

List submodules

git submodule

Check status

git submodule status

Enter submodule

cd <directory>

Switch branch

git switch <branch>

Pull branch

git pull origin <branch>

Update one submodule

git submodule update --remote <submodule>

Update all

git submodule update --remote

Update recursively

git submodule update --remote --recursive

Run command in every submodule

git submodule foreach '<command>'

Fetch every submodule

git submodule foreach git fetch

Check differences

git diff --submodule

Synchronize URLs

git submodule sync --recursive

Change URL

git submodule set-url <submodule> <new-url>

Remove

git submodule deinit -f <submodule>
git rm -f <submodule>

Configure branch

[submodule "backend"]
path = backend
url = https://github.com/example/backend.git
branch = develop

Show branches

git -C backend branch -a

Show remote

git -C backend remote -v

Show current branch

git -C backend branch --show-current

Final Thoughts

Git submodules are sometimes considered complicated, but the underlying concept is actually quite simple:

Each project remains an independent repository
+
Master repository tracks exact versions
=
Controlled multi-repository architecture

The biggest thing to remember is that the master repository tracks a commit, not simply a branch.

Branches are useful when you are developing inside a submodule. Commits are what the master repository ultimately records.

For a product consisting of several independently managed repositories, this gives you a useful balance between a monorepo and completely separate repositories:

                    Master

┌──────────────┼──────────────┐
│ │ │
Backend Frontend Mobile
│ │ │
Independent Independent Independent
Repo Repo Repo
│ │ │
└──────────────┼──────────────┘

Exact commits
tracked by master

If you establish a clear branch strategy and make the master repository responsible for the versions of each component, Git submodules can work very well for this kind of architecture.