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
Architecture:
┌─────────────────┐
│ Source Code │
│ GitHub │
└────────┬────────┘
│
Git versioning
│
┌────────────┴────────────┐
│ │
▼ ▼
GitHub Release Zenodo
v3 DOI
│
│
▼
GitHub Actions
│
Docker
│
▼
GitHub GHCR
│
▼
docker pull
│
▼
Reproducible app
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
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
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
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
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/...
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..."
you provide:
Docker image
↓
docker run
↓
application
7. GitHub Actions
GitHub Actions is the automation layer.
Instead of manually doing:
build Docker image
↓
test
↓
push image
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 it:
cd YOUR_REPOSITORY
Check:
git status
You should see:
On branch main
Your branch is up to date with 'origin/main'.
Key insight
origin is simply the conventional name for your remote GitHub repository.
Check it:
git remote -v
You might see:
origin git@github.com:username/project.git (fetch)
origin git@github.com:username/project.git (push)
So:
local repository
│
│ origin
▼
GitHub repository
Part III — Establish a release
Suppose your current project state should become version 3.
First inspect existing tags:
git tag
For example:
v1
v2
Make sure your working tree is clean:
git status
Commit your current changes:
git add .
git commit -m "Prepare v3 release"
Push:
git push origin main
Now create the tag:
git tag v3
Push the tag:
git push origin v3
Verify remote tags:
git ls-remote --tags origin
You should see:
refs/tags/v1
refs/tags/v2
refs/tags/v3
⭐ Important lesson: tag ≠ GitHub Release
At this point:
v3
is a Git tag.
It isn't necessarily a GitHub Release yet.
Go to:
GitHub → Releases → Draft a new release
Select:
v3
Give it a title:
Schedule App v3
Add release notes and publish.
Now you have:
Git commit
↓
Git tag v3
↓
GitHub Release v3
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
Your resulting DOI might look like:
10.5281/zenodo.xxxxxxx
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:
[](https://doi.org/10.5281/zenodo.22981870)
For example:
# Schedule App
[](https://doi.org/10.5281/zenodo.22981870)
An interactive scheduling tool that models people's preferred meeting
times using Gaussian distributions.
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
Then:
docker info
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;
Why?
Normally Next.js produces a .next build directory containing various things.
With:
output: "standalone"
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
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"]
🧠 Understand the Dockerfile
This is a multi-stage build.
Stage 1 — dependencies
package.json
package-lock.json
↓
npm ci
↓
node_modules
Stage 2 — builder
source code
+
node_modules
↓
npm run build
↓
.next/standalone
Stage 3 — runner
Only production artifacts are copied:
standalone
static
public
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
Put:
node_modules
.next
.git
.github
.vscode
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
Dockerfile
.dockerignore
Why?
Docker sends a build context to the builder.
You don't want:
.git
node_modules
.next
unnecessarily included.
Part VII — Build locally
From the repository root:
docker build -t schedule-app:local .
The final:
.
means:
Use the current directory as the Docker build context.
Check the image:
docker images schedule-app
You should see:
schedule-app:local
Part VIII — Run the container
docker run --rm -p 3000:3000 schedule-app:local
Then visit:
http://localhost:3000
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
You ran:
docker run ...
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
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
Understand what this workflow does
The important trigger is:
on:
push:
tags:
- "v*"
Meaning:
When a version tag is pushed, run this workflow.
Then:
Git tag
↓
GitHub Actions
↓
Checkout source
↓
Build Docker image
↓
Push image
↓
GHCR
Part X — GitHub Container Registry
GHCR is:
GitHub Container Registry
Your image gets a name like:
ghcr.io/galaxyeagle/schedule_app
Think of it as:
GitHub
│
├── Repository
│ └── source code
│
└── Container Registry
└── Docker images
They're related but different artifacts.
Part XI — Verify the published image
After GitHub Actions succeeds:
docker pull ghcr.io/galaxyeagle/schedule_app:latest
Then:
docker run --rm -p 3000:3000 \
ghcr.io/galaxyeagle/schedule_app:latest
Visit:
http://localhost:3000
If it works, you've demonstrated something very important:
Source code
↓
GitHub
↓
Automated build
↓
Container registry
↓
Fresh machine
↓
docker pull
↓
Application runs
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
And potentially:
v3
│
▼
Docker image
│
:v3 / :latest
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
Then somebody can reproduce exactly what you used.
Part XIII — Why latest isn't enough
Suppose you only publish:
schedule_app:latest
Today:
latest → v3
Six months later:
latest → v4
Someone trying to reproduce your v3 research gets the wrong software.
That's why versioned Docker tags matter:
schedule_app:v3
is much more reproducible than:
schedule_app:latest
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
A researcher can discover the software and cite it.
A — Accessible
GitHub source
+
Zenodo archived release
+
Docker image
Multiple access paths exist.
I — Interoperable
You aren't inventing proprietary packaging.
You're using:
Git
Markdown
JSON/YAML
Docker
OCI container ecosystem
DOI
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?
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
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
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
Top comments (0)