DEV Community

Raman Butta
Raman Butta

Posted on

FAIR Research Software with GitHub, GitHub Actions, Docker & Zenodo

Here I will explain how to take a small research/software project and make it:

  • Findable → Zenodo + DOI + metadata
  • Accessible → GitHub source + GitHub Releases
  • Interoperable → standard Git/Docker artifacts
  • Reusable → documented code + reproducible container

This is a practical introduction to the FAIR principles through an actual Next.js application.

Our example:

Schedule App
Enter fullscreen mode Exit fullscreen mode

Architecture:

                 ┌─────────────────┐
                 │   Source Code   │
                 │     GitHub      │
                 └────────┬────────┘
                          │
                    Git versioning
                          │
             ┌────────────┴────────────┐
             │                         │
             ▼                         ▼
       GitHub Release              Zenodo
          v3                       DOI
             │
             │
             ▼
       GitHub Actions
             │
          Docker
             │
             ▼
       GitHub GHCR
             │
             ▼
        docker pull
             │
             ▼
       Reproducible app
Enter fullscreen mode Exit fullscreen mode

Part I — Understand the objects first

Before touching the terminal, understand the vocabulary.

1. Git

Git tracks changes to source code.

commit A
   ↓
commit B
   ↓
commit C
Enter fullscreen mode Exit fullscreen mode

A commit answers:

What exactly was the state of the code at this point?


2. GitHub

GitHub hosts the Git repository remotely.

Local Git repository
        │
        │ push
        ▼
GitHub repository
Enter fullscreen mode Exit fullscreen mode

GitHub gives you:

  • source-code hosting
  • collaboration
  • Issues
  • Actions
  • Releases
  • Packages

3. Git tag

A tag is a named pointer to a specific commit.

For example:

commit 540886d
      ↑
     v3
Enter fullscreen mode Exit fullscreen mode

It says:

"Call this particular state of the source code v3."


4. GitHub Release

A GitHub Release is a human-facing release record associated with a Git tag.

Think:

Git tag
  ↓
GitHub Release
  ↓
release notes + downloadable artifacts
Enter fullscreen mode Exit fullscreen mode

A tag can exist without a Release.


5. Zenodo

Zenodo is an archival/research repository.

It gives your software a persistent scholarly identity:

Schedule App
     │
     ▼
Zenodo
     │
     ▼
DOI
10.5281/zenodo/...
Enter fullscreen mode Exit fullscreen mode

The DOI makes the software citable and persistently identifiable.


6. Docker

Docker packages the runtime environment with your application.

Instead of:

"Install Node 22,
install these packages,
configure this,
hope it works..."
Enter fullscreen mode Exit fullscreen mode

you provide:

Docker image
     ↓
docker run
     ↓
application
Enter fullscreen mode Exit fullscreen mode

7. GitHub Actions

GitHub Actions is the automation layer.

Instead of manually doing:

build Docker image
        ↓
test
        ↓
push image
Enter fullscreen mode Exit fullscreen mode

you tell GitHub:

"Whenever I create a release/tag, do these steps automatically."


Part II — Start with an existing GitHub repository

Suppose your project already exists on GitHub.

Clone it:

git clone git@github.com:YOUR_USERNAME/YOUR_REPOSITORY.git
Enter fullscreen mode Exit fullscreen mode

Enter it:

cd YOUR_REPOSITORY
Enter fullscreen mode Exit fullscreen mode

Check:

git status
Enter fullscreen mode Exit fullscreen mode

You should see:

On branch main
Your branch is up to date with 'origin/main'.
Enter fullscreen mode Exit fullscreen mode

Key insight

origin is simply the conventional name for your remote GitHub repository.

Check it:

git remote -v
Enter fullscreen mode Exit fullscreen mode

You might see:

origin  git@github.com:username/project.git (fetch)
origin  git@github.com:username/project.git (push)
Enter fullscreen mode Exit fullscreen mode

So:

local repository
       │
       │ origin
       ▼
GitHub repository
Enter fullscreen mode Exit fullscreen mode

Part III — Establish a release

Suppose your current project state should become version 3.

First inspect existing tags:

git tag
Enter fullscreen mode Exit fullscreen mode

For example:

v1
v2
Enter fullscreen mode Exit fullscreen mode

Make sure your working tree is clean:

git status
Enter fullscreen mode Exit fullscreen mode

Commit your current changes:

git add .
git commit -m "Prepare v3 release"
Enter fullscreen mode Exit fullscreen mode

Push:

git push origin main
Enter fullscreen mode Exit fullscreen mode

Now create the tag:

git tag v3
Enter fullscreen mode Exit fullscreen mode

Push the tag:

git push origin v3
Enter fullscreen mode Exit fullscreen mode

Verify remote tags:

git ls-remote --tags origin
Enter fullscreen mode Exit fullscreen mode

You should see:

refs/tags/v1
refs/tags/v2
refs/tags/v3
Enter fullscreen mode Exit fullscreen mode

⭐ Important lesson: tag ≠ GitHub Release

At this point:

v3
Enter fullscreen mode Exit fullscreen mode

is a Git tag.

It isn't necessarily a GitHub Release yet.

Go to:

GitHub → Releases → Draft a new release

Select:

v3
Enter fullscreen mode Exit fullscreen mode

Give it a title:

Schedule App v3
Enter fullscreen mode Exit fullscreen mode

Add release notes and publish.

Now you have:

Git commit
    ↓
Git tag v3
    ↓
GitHub Release v3
Enter fullscreen mode Exit fullscreen mode

This distinction is extremely useful.


Part IV — Zenodo

Now connect the repository to Zenodo.

Zenodo can archive a GitHub release and create a DOI.

Conceptually:

GitHub Release v3
       │
       ▼
     Zenodo
       │
       ▼
 DOI
Enter fullscreen mode Exit fullscreen mode

Your resulting DOI might look like:

10.5281/zenodo.xxxxxxx
Enter fullscreen mode Exit fullscreen mode

FAIR principle

This contributes strongly to:

F — Findable

because your software gets:

  • persistent metadata
  • DOI
  • searchable scholarly record
  • citation information

It also contributes to:

A — Accessible

because people have a persistent location from which to access the archived release.


Part V — Add the DOI to README

A good README should expose the DOI prominently.

Use the Zenodo badge:

[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22981870.svg)](https://doi.org/10.5281/zenodo.22981870)
Enter fullscreen mode Exit fullscreen mode

For example:

# Schedule App

[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22981870.svg)](https://doi.org/10.5281/zenodo.22981870)

An interactive scheduling tool that models people's preferred meeting
times using Gaussian distributions.
Enter fullscreen mode Exit fullscreen mode

Now your GitHub repository immediately communicates:

This isn't just some random code repository. This software has a citable archival record.


Part VI — Containerize the application

Now we introduce Docker.

Step 1 — Check Docker

docker --version
Enter fullscreen mode Exit fullscreen mode

Then:

docker info
Enter fullscreen mode Exit fullscreen mode

The second command verifies that the Docker daemon is actually accessible.


Step 2 — Configure Next.js

For Next.js, use standalone output.

next.config.ts:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
};

export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

Why?

Normally Next.js produces a .next build directory containing various things.

With:

output: "standalone"
Enter fullscreen mode Exit fullscreen mode

Next.js creates a self-contained production server suitable for copying into a minimal Docker image.


Step 3 — Create Dockerfile

At the repository root:

Dockerfile
Enter fullscreen mode Exit fullscreen mode

Use a multi-stage build:

# syntax=docker/dockerfile:1

FROM node:22-alpine AS base

# Install dependencies
FROM base AS deps

WORKDIR /app

COPY package.json package-lock.json ./

RUN npm ci

# Build application
FROM base AS builder

WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .

RUN npm run build

# Production image
FROM base AS runner

WORKDIR /app

ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0

RUN addgroup --system --gid 1001 nodejs \
    && adduser --system --uid 1001 nextjs

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public

USER nextjs

EXPOSE 3000

CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

🧠 Understand the Dockerfile

This is a multi-stage build.

Stage 1 — dependencies

package.json
package-lock.json
       ↓
     npm ci
       ↓
node_modules
Enter fullscreen mode Exit fullscreen mode

Stage 2 — builder

source code
    +
node_modules
    ↓
npm run build
    ↓
.next/standalone
Enter fullscreen mode Exit fullscreen mode

Stage 3 — runner

Only production artifacts are copied:

standalone
static
public
Enter fullscreen mode Exit fullscreen mode

The final image doesn't need your entire development environment.

This is why multi-stage builds are common in production.


Step 4 — .dockerignore

Create:

.dockerignore
Enter fullscreen mode Exit fullscreen mode

Put:

node_modules
.next
.git
.github
.vscode

npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

Dockerfile
.dockerignore
Enter fullscreen mode Exit fullscreen mode

Why?

Docker sends a build context to the builder.

You don't want:

.git
node_modules
.next
Enter fullscreen mode Exit fullscreen mode

unnecessarily included.


Part VII — Build locally

From the repository root:

docker build -t schedule-app:local .
Enter fullscreen mode Exit fullscreen mode

The final:

.
Enter fullscreen mode Exit fullscreen mode

means:

Use the current directory as the Docker build context.

Check the image:

docker images schedule-app
Enter fullscreen mode Exit fullscreen mode

You should see:

schedule-app:local
Enter fullscreen mode Exit fullscreen mode

Part VIII — Run the container

docker run --rm -p 3000:3000 schedule-app:local
Enter fullscreen mode Exit fullscreen mode

Then visit:

http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

If your application appears:

🎉 You have proven that the application can run independently of your local Node development environment.


⭐ The important reproducibility insight

Notice what happened.

You didn't run:

npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

You ran:

docker run ...
Enter fullscreen mode Exit fullscreen mode

The container contains the runtime environment required to run the application.

That's the beginning of reproducibility.


Part IX — Put the Docker process into GitHub Actions

Now automate it.

Create:

.github/
└── workflows/
    └── docker.yml
Enter fullscreen mode Exit fullscreen mode

A simple release-triggered workflow:

name: Docker Image

on:
  push:
    tags:
      - "v*"

  workflow_dispatch:

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  docker:
    name: Build and publish Docker image
    runs-on: ubuntu-latest

    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=raw,value=latest

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
Enter fullscreen mode Exit fullscreen mode

Understand what this workflow does

The important trigger is:

on:
  push:
    tags:
      - "v*"
Enter fullscreen mode Exit fullscreen mode

Meaning:

When a version tag is pushed, run this workflow.

Then:

Git tag
   ↓
GitHub Actions
   ↓
Checkout source
   ↓
Build Docker image
   ↓
Push image
   ↓
GHCR
Enter fullscreen mode Exit fullscreen mode

Part X — GitHub Container Registry

GHCR is:

GitHub Container Registry

Your image gets a name like:

ghcr.io/galaxyeagle/schedule_app
Enter fullscreen mode Exit fullscreen mode

Think of it as:

GitHub
│
├── Repository
│      └── source code
│
└── Container Registry
       └── Docker images
Enter fullscreen mode Exit fullscreen mode

They're related but different artifacts.


Part XI — Verify the published image

After GitHub Actions succeeds:

docker pull ghcr.io/galaxyeagle/schedule_app:latest
Enter fullscreen mode Exit fullscreen mode

Then:

docker run --rm -p 3000:3000 \
  ghcr.io/galaxyeagle/schedule_app:latest
Enter fullscreen mode Exit fullscreen mode

Visit:

http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

If it works, you've demonstrated something very important:

Source code
     ↓
GitHub
     ↓
Automated build
     ↓
Container registry
     ↓
Fresh machine
     ↓
docker pull
     ↓
Application runs
Enter fullscreen mode Exit fullscreen mode

That is much stronger evidence of reproducibility than simply saying:

"It works on my computer."


Part XII — The versioning lesson

There are multiple versioned objects:

                 v3
                  │
       ┌──────────┼──────────┐
       │          │          │
       ▼          ▼          ▼
     Git tag   GitHub      Zenodo
                Release      DOI
Enter fullscreen mode Exit fullscreen mode

And potentially:

                  v3
                   │
                   ▼
              Docker image
                   │
              :v3 / :latest
Enter fullscreen mode Exit fullscreen mode

These should be intentionally aligned, but they aren't automatically the same thing.

For a polished research software project, you ideally want:

Software v3
│
├── Git commit
├── Git tag: v3
├── GitHub Release: v3
├── Zenodo archive: v3
├── DOI
└── Docker image: v3
Enter fullscreen mode Exit fullscreen mode

Then somebody can reproduce exactly what you used.


Part XIII — Why latest isn't enough

Suppose you only publish:

schedule_app:latest
Enter fullscreen mode Exit fullscreen mode

Today:

latest → v3
Enter fullscreen mode Exit fullscreen mode

Six months later:

latest → v4
Enter fullscreen mode Exit fullscreen mode

Someone trying to reproduce your v3 research gets the wrong software.

That's why versioned Docker tags matter:

schedule_app:v3
Enter fullscreen mode Exit fullscreen mode

is much more reproducible than:

schedule_app:latest
Enter fullscreen mode Exit fullscreen mode

latest is convenient.

A version tag is archival.


Part XIV — FAIR mapping

Now we can connect the technical exercise to FAIR.

FAIR principle What your project does
Findable GitHub repository + Zenodo metadata + DOI
Accessible Public GitHub repository + Zenodo archive
Interoperable Git, Markdown, Docker, standard package formats
Reusable README + version history + DOI + Dockerized runtime

More specifically

F — Findable

GitHub
+
Zenodo
+
DOI
+
metadata
Enter fullscreen mode Exit fullscreen mode

A researcher can discover the software and cite it.


A — Accessible

GitHub source
+
Zenodo archived release
+
Docker image
Enter fullscreen mode Exit fullscreen mode

Multiple access paths exist.


I — Interoperable

You aren't inventing proprietary packaging.

You're using:

Git
Markdown
JSON/YAML
Docker
OCI container ecosystem
DOI
Enter fullscreen mode Exit fullscreen mode

These are widely understood standards.


R — Reusable

This is where your documentation becomes important.

Someone needs to understand:

What does this software do?
How do I install it?
Which version am I using?
How do I run it?
Where is the archived version?
How do I cite it?
Enter fullscreen mode Exit fullscreen mode

Your README + DOI + release + Docker image answer those questions.


Part XV — The complete mental model

                       RESEARCH SOFTWARE
                              │
                              ▼
                         Git repository
                              │
                    ┌─────────┴─────────┐
                    │                   │
                 commits              tags
                    │                   │
                    │                  v3
                    │                   │
                    │          ┌────────┴────────┐
                    │          │                 │
                    │          ▼                 ▼
                    │     GitHub Release       Zenodo
                    │          v3                │
                    │                            ▼
                    │                           DOI
                    │
                    ▼
              GitHub Actions
                    │
                    ▼
               Docker Build
                    │
                    ▼
                  GHCR
                    │
                    ▼
             schedule_app:v3
                    │
                    ▼
              docker pull
                    │
                    ▼
              Reproducible
               execution
Enter fullscreen mode Exit fullscreen mode

That is essentially the FAIR research-software lifecycle we just practiced.


Conclusion

This wasn't just a Docker tutorial.

We discussed several layers of modern research software engineering:

Level 1
Git
│
└── version control

Level 2
GitHub
│
└── remote collaboration/distribution

Level 3
GitHub Releases
│
└── software versioning

Level 4
Zenodo
│
└── scholarly archival + DOI

Level 5
Docker
│
└── environment reproducibility

Level 6
GHCR
│
└── distribution of executable artifacts

Level 7
GitHub Actions
│
└── automation / CI/CD
Enter fullscreen mode Exit fullscreen mode

And the really important conceptual leap is:

A research software artifact isn't just source code.

It can have:

Source
+
Version
+
Metadata
+
Citation
+
Archive
+
Runtime environment
+
Automated build
Enter fullscreen mode Exit fullscreen mode

Top comments (0)