Skip to content

alpine-cgit

gh_commit status gh_stars gh_forks gh_watches gh_issues gh_pr
dh_pulls dh_stars dh_size:aarch64 dh_size:armhf dh_size:armv7l dh_size:x86_64

MultiArch Alpine Linux + S6 + cGit + SSHd for network-local repositories.


This image containerizes cGit running under a LigHttpd server to serve locally hosted git repositories. Can also be used to clone and push/pull the repos using git via SSH/PubKey authentication. Scripts included to ease the tasks e.g creating or mirroring bare repositories, or sync them periodically.

Based on Alpine Linux from the s6 image with the cgit package installed in it.


Get the Image

Pull the image for your architecture if it's already available from Docker Hub.

docker pull woahbase/alpine-cgit
Image Tags

The image is tagged respectively for the following architectures,

latest tag is annotated as multiarch so pulling without any tags should fetch the correct image for your architecture. Same goes for any of the version tags.

non-x86_64 builds have embedded binfmt_misc support and contain the qemu-user-static binary that allows for running it also inside an x86_64 environment that has support for it.


Run

Running the container starts the service.

docker run --rm \
  --name docker_cgit \
  -p 64801:80 -p 64822:22 \
  -v $PWD/data/git`#(1)`:/home/git \
  -v $PWD/data/repositories`#(2)`:/home/git/repositories \
  -v $PWD/data/ssh`#(3)`:/etc/ssh \
  -v $PWD/data/web`#(4)`:/var/www \
woahbase/alpine-cgit
  1. Path to git user homedir, ssh-configurations generated/persisted here.
  2. (Required) Path to repositories storage root directory.
  3. (Required) If you want the host keys to persist.
  4. (Optional) Path to web-ui customization files directory.
Multi-Arch Support

If you want to run images for other architectures on a x86_64 machine, you will need to have binfmt support configured for your machine before running the image. multiarch, has made it easy for us containing that into a docker container, just run

docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

Now images built for other architectures will also be executable. This is optional though, without the above, you can still run the image that is made for your architecture.


Configuration

We can customize the runtime behaviour of the container with the following environment variables.

ENV Vars Default Description
CGIT_HOSTNAME localhost Hostname to set in the url for cloning via web or ssh.
CGIT_SUBPATH /git Alternate virtual-root for CGit repositories, default is /cgit.
CGIT_REPODIR /home/git/repositories Default path to repositories. (since 1.2.3_20240907)
CGIT_ARCHIVEDIR $CGIT_REPODIR/.archived Default path to archived repos (used in backup/restore scripts). (since 1.2.3_20240907)
CGIT_HOOKSDIR /defaults/hooks Custom hooks to add into repositories created via the bareinit or mirror script. (since 1.2.3_20240907)
CGIT_SYNC_RUNFILE /tmp/sync_is_running File indicator that a sync job is already running. (used in sync script) (since 1.2.3_20240907)
CGIT_SYNC_LIST /tmp/sync_list_of_repos List of repositories to sync, generated at start of sync job. (used in sync script) (since 1.2.3_20240907)
CGIT_SYNC_IGNORELIST $CGIT_REPODIR/ignored.txt List of repositories to ignore while sync. (format: (category)/(repo-name).git) (used in sync script) (since 1.2.3_20240907)
CGIT_SYNC_ERRORLIST $CGIT_REPODIR/errors.txt Catches errors encountered while sync. Flushed on each run. (used in sync script) (since 1.2.3_20240907)
CGIT_SYNC_JOBS 1 Number of threads used by git to sync a repository. (since 1.2.3_20240907)
CGIT_PERMFIX_REPOS unset If set to true, will fix repositories permissions to $S6_USER (default git:git). (since 1.2.3_20240907)
LIGHTTPD_CONFDIR /etc/lighttpd Path to lighttpd configuration directory. (since 1.2.3_20240907)
LIGHTTPD_LOGFILE /var/log/lighttpd/lighttpd.log Logfile for lighttpd, by default logs both access and error logs. (since 1.2.3_20240907)
LIGHTTPD_USER lighttpd Non-root user that lighttpd drops privileges to. (since 1.2.3_20240907)
LIGHTTPD_ARGS -D Customizable arguments passed to lighttpd service.
LIGHTTPD_SKIP_LOGFIFO unset By default lighttpd logs to stdout via a fifo, set this to true to log to a regular file instead. (since 1.2.3_20240907)
SSHD_CONFDIR /etc/ssh Path to ssh server configuration directory. (since 1.2.3_20240907)
SSHD_ARGS -De Customizable arguments passed to sshd service.
SSHD__(parameter) unset If set, will update the parameter (if exists) with the value in sshd_config. E.g. SSHD__Port=2222. (Note the double underscores.) (since 1.2.3_20240907)
S6_NEEDED_PACKAGES empty string Space-separated list of extra APK packages to install on start. E.g. "curl git tzdata"
PUID 1000 Id of S6_USER.
PGID 100 Group id of S6_USER.
S6_USER git (Preset) Default non-root user for services to drop privileges to.
S6_USERHOME /home/git (Preset) HOME directory for S6_USER.
Did you know?

You can check your own UID/GID by running the command id in a terminal.

Also,

  • cGit is deployed at the path /cgit/, alternately proxied to /git/ for convenience, controlled via the CGIT_SUBPATH environment variable.

  • Default configuration listens to ports 80 and 22(ssh), these may be published at 64801 and 64822 by default, so they don't clash with other services.

  • CGit config file loaded from /etc/cgitrc edit or remount this with your own. A sample is provided which is auto loaded if there aren't any config file to start with.

  • Lighttpd config file loaded from /etc/lighttpd/lighttpd.conf edit or remount this with your own. A sample is provided which is auto loaded if there aren't any config file to start with. (since 1.2.3_20240907)

  • SSH config file loaded from /etc/ssh/sshd_config edit or remount this with your own. A sample is provided which is auto loaded if there aren't any config file to start with.

  • To persist the same SSH host keys, preserve their contents at /etc/ssh. These are re-generated if not found.

  • Only allows pubkey authentication by default, either use the one for the user git, or add your own in /home/git/.ssh/authorized_keys to get clone and push/pull access. Default adds only the pubkey of the git user, if that does not exist, a new set of private/public keys are generated.

  • Repositories stored at /home/git/repositories, can be changed via setting the CGIT_REPODIR environment variable.

  • Web specific stuff, e.g about.html or syntax filters should be inside /var/www, cgit provides some default filters (not used) located at /usr/lib/cgit/. Mount or configure your own if that's what you need.


Scripts

A few scripts for common tasks are baked into the image, E.g. we can create a bare repository with,

docker exec -u git -it docker_cgit /scripts/bareinit

Mirror a copy of an existing online repository locally with,

docker exec -u git -it docker_cgit /scripts/mirror

Sync the repositories already tracking with their remote,

docker exec -u git -it docker_cgit /scripts/sync

Backup a bare or mirror repository to $CGIT_ARCHIVEDIR/(optional category-dir)/repo-name, (since 1.2.3_20240907)

docker exec -u git -it docker_cgit /scripts/backup <filters: category-dirs, or reponames>

Restore a backed-up repository from $CGIT_ARCHIVEDIR/(optional category-dir)/repo-name, (since 1.2.3_20240907)

docker exec -u git -it docker_cgit /scripts/restore <filters: category-dirs, or reponames>

Stop the container with a timeout, (defaults to 2 seconds)

docker stop -t 2 docker_cgit

Restart the container with

docker restart docker_cgit

Removes the container, (always better to stop it first and -f only when needed most)

docker rm -f docker_cgit

Shell access

Get a shell inside a already running container,

docker exec -it docker_cgit /bin/bash

Optionally, login as a non-root user, (default is git)

docker exec -u git -it docker_cgit /bin/bash

Or set user/group id e.g 1000/100,

docker exec -u 1000:100 -it docker_cgit /bin/bash

Logs

To check logs of a running container in real time

docker logs -f docker_cgit

As-A-Service

Run the container as a service with the following as reference (and modify it as needed).

With docker-compose (alpine-cgit.yml)

---
services:
  cgit:
    container_name: cgit
    deploy:
      resources:
        limits:
          cpus: '2.00'
          memory: 2048M  # may need more memory for sync job
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 5
        window: 120s
    environment:
      # CGIT_REPODIR: /home/git/repositories
      # CGIT_SYNC_IGNORELIST: /home/git/repositories/ignored.txt
      # CGIT_SYNC_ERRORLIST: /home/git/repositories/errors.txt
      # CGIT_SYNC_JOBS: 1
      PUID: ${PUID:-1000}
      PGID: ${PGID:-100}
      # TZ: ${TZ}
    # healthcheck:
    #   interval: 2m
    #   retries: 5
    #   start_period: 5m
    #   test:
    #     - CMD-SHELL
    #     - >
    #       curl -f http://localhost:80/git/ || exit 1
    #   timeout: 20s
    hostname: cgit
    image: woahbase/alpine-cgit:${CGIT_TAG:-latest}
    network_mode: bridge
    ports:
      - protocol: tcp
        host_ip: 0.0.0.0
        published: ${CGIT_PORT_SSH:-22}
        target: 22
      - protocol: tcp
        host_ip: 0.0.0.0
        published: ${CGIT_PORT_HTTP:-80}
        target: 80
    volumes:
      - type: bind
        source: ${CGIT_DIR:?err}/git
        target: /home/git
        bind:
          create_host_path: true
      - type: bind
        source: ${CGIT_DIR:?err}/repositories
        target: /home/git/repositories
        bind:
          create_host_path: true
      - type: bind
        source: ${CGIT_DIR:?err}/ssh
        target: /etc/ssh
        bind:
          create_host_path: true
      # - type: bind
      #   source: ${CGIT_DIR:?err}/configs/cgitrc
      #   target: /etc/cgitrc
      #   bind:
      #     create_host_path: false
      # - type: bind
      #   source: ${CGIT_DIR:?err}/configs/scripts
      #   target: /config/scripts
      #   bind:
      #     create_host_path: false
      # - type: bind
      #   source: ${CGIT_DIR:?err}/configs/web
      #   target: /var/www
      #   bind:
      #     create_host_path: false
      - type: bind
        source: /etc/localtime
        target: /etc/localtime
        read_only: true
        bind:
          create_host_path: false

With HashiCorp Nomad (alpine-cgit.hcl)

variables {
  dc   = "dc1" # to load the dc-local config file
  pgid = 100   # gid for docker
  puid = 1000  # uid for docker
  version = "1.2.3"
}
# locals { var = yamldecode(file("${var.dc}.vars.yml")) } # load dc-local config file

job "cgit" {
  datacenters = [var.dc]
  # namespace   = local.var.namespace
  priority    = 80
  # region      = local.var.region
  type        = "service"

  # vault { policies = ["nomad-kv-readonly"] }

  group "docker" {
    count = 1

    restart {
      attempts = 2
      interval = "2m"
      delay    = "15s"
      mode     = "fail"
    }
    update {
      max_parallel      = 1
      min_healthy_time  = "10s"
      healthy_deadline  = "10m"
      progress_deadline = "15m"
      auto_revert       = false
    }

    service {
      name        = NOMAD_JOB_NAME
      port        = "http"
      tags        = ["ins${NOMAD_ALLOC_INDEX}", attr.unique.hostname, "aid=${NOMAD_ALLOC_ID}", "urlprefix-/git"]
      canary_tags = ["canary${NOMAD_ALLOC_INDEX}", "urlprefix-/c/git"]
      check {
        name      = "${NOMAD_JOB_NAME}@${attr.unique.hostname}:${NOMAD_HOST_PORT_http}=>${NOMAD_PORT_http}"
        type      = "http"
        path      = "/git/?p=about"
        interval  = "60s"
        timeout   = "10s"
      }
      check_restart {
        limit     = 3
        grace     = "10m"
      }
    }

    service {
      name        = "${NOMAD_JOB_NAME}-ssh"
      port        = "ssh"
      tags        = ["ins${NOMAD_ALLOC_INDEX}", attr.unique.hostname, "aid=${NOMAD_ALLOC_ID}", "host=git"]
      canary_tags = ["canary${NOMAD_ALLOC_INDEX}"]
      check {
        name      = "${NOMAD_JOB_NAME}@${attr.unique.hostname}:${NOMAD_HOST_PORT_http}=>${NOMAD_PORT_ssh}"
        type      = "tcp"
        interval  = "60s"
        timeout   = "10s"
      }
      check_restart {
        limit     = 3
        grace     = "10m"
      }
    }

    ephemeral_disk { size = 128 } # MB
    network {
      # dns { servers = local.var.dns_servers }
      port "http" {
        static = 64801
        to     = 80
      }
      port "ssh"  {
        static = 64822
        to     = 22
      }
    }
    volume "nomad-cgit-repos" {
      type      = "host"
      read_only = false
      source    = "nomad-cgit-repos"
    }
    volume "nomad-cgit-hostkeys" {
      type      = "host"
      read_only = false
      source    = "nomad-cgit-hostkeys"
    }
    volume "nomad-cgit-userkeys" {
      type      = "host"
      read_only = false
      source    = "nomad-cgit-userkeys"
    }
    # volume "nomad-cgit-scripts" {
    #   type      = "host"
    #   read_only = false
    #   source    = "nomad-cgit-scripts"
    # }
    volume "nomad-cgit-web" {
      type      = "host"
      read_only = false
      source    = "nomad-cgit-web"
    }

    task "cgit" {
      driver = "docker"

      config {
        healthchecks { disable = true }
        hostname     = NOMAD_JOB_NAME
        image        = "${local.var.docker_registry.url}/woahbase/alpine-cgit:${var.version}"
        network_mode = "bridge"
        ports        = ["http", "ssh"]

        # volumes      = [
        #   "/path/to/your/cgit/git/.ssh:/home/git/.ssh",
        #   "/path/to/your/cgit/scripts:/scripts",
        #   "/path/to/your/cgit/hooks:/defaults/hooks",
        #   "/path/to/your/cgit/ssh:/etc/ssh",
        #   "/path/to/your/cgit/web:/var/www"
        # ]

        logging {
          type = "journald"
          config {
            mode = "non-blocking"
            tag = NOMAD_JOB_NAME
          }
        }

        mount {
          source   = "local/cgitrc"
          target   = "/etc/cgitrc"
          type     = "bind"
          readonly = true
        }
        mount {
          type     = "bind"
          target   = "/etc/localtime"
          source   = "/etc/localtime"
          readonly = true
        }
      }

      volume_mount {
        # ensure policies allow vault-generated-token to read-write to the volume
        volume      = "nomad-cgit-repos"
        destination = "/home/git/repositories"
        read_only   = false
      }

      volume_mount {
        volume      = "nomad-cgit-hostkeys"
        destination = "/etc/ssh"
        read_only   = false
      }

      # volume_mount {
      #   volume      = "nomad-cgit-scripts"
      #   destination = "/scripts"
      #   read_only   = true
      # }

      volume_mount {
        volume      = "nomad-cgit-userkeys"
        destination = "/home/git/.ssh"
        read_only   = false
      }

      volume_mount {
        volume      = "nomad-cgit-web"
        destination = "/var/www"
        read_only   = true
      }

      env {
        PGID = var.pgid
        PUID = var.puid
        # TZ   = local.var.tz
      }

      resources {
        cpu    = 1536 # MHz
        memory = 2048 # MB
      }

      template {
        change_mode = "restart"
        destination = "local/cgitrc"
        data        = <<-EOC
          {{ key "nomad/${var.dc}/cgit/cgitrc" }}
        EOC
        error_on_missing_key = true
      }
    }
  }
}

Reverse Proxy

To proxy it through a web server, see below

The following snippet can be used to reverse-proxy the service using NGINX.

upstream proxy_cgit {
    server your.host.local:<cgit-port> fail_timeout=5;
}

## the following goes inside a server block

location /cgit { return 301 /cgit/; }
location /cgit/ {
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;

    proxy_connect_timeout 300;
    proxy_read_timeout 300;
    proxy_redirect redirect replacement;
    proxy_pass http://proxy_cgit;
}

location /git { return 301 /git/; }
location /git/ {
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;

    proxy_connect_timeout 300;
    proxy_read_timeout 300;
    proxy_redirect redirect replacement;
    proxy_pass http://proxy_cgit;
}

Build Your Own

Feel free to clone (or fork) the repository and customize it for your own usage, build the image for yourself on your own systems, and optionally, push it to your own public (or private) repository.

Here's how...


Setting up


Before we clone the /repository, we must have Git, GNU make, and Docker (optionally, with buildx plugin for multi-platform images) setup on the machine. Also, for multi-platform annotations, we might require enabling experimental features of Docker.

Clone the repo with,

git clone https://github.com/woahbase/alpine-cgit
cd alpine-cgit

To get a list of all available targets, run

make help
Always Check Before You Make!

Did you know, we could check what any make target is going to execute before we actually run them, with

make -n <targetname> <optional args>

Build and Test


To create the image for your architecture, run the build and test target with

make build test 

For building an image that targets another architecture, it is required to specify the ARCH parameter when building. e.g.

make build test ARCH=aarch64 
make build test ARCH=armhf 
make build test ARCH=armv7l 
make build test ARCH=x86_64 
Build Parameters

All images have a few common build parameters that can be customized at build time, like

  • ARCH
The target architecture to build for. Defaults to host architecture, auto-detected at build-time if not specified. Also determines if binfmt support is required before build or run and runs the regbinfmt target automatically. Possible values are aarch64, armhf, armv7l, and x86_64.
  • BUILDDATE
The date of the build. Can be used to create separate tags for images. (format: yyyymmdd)
  • DOCKERFILE
The dockerfile to use for build. Defaults to the file Dockerfile, but if per-arch dockerfiles exist, (e.g. for x86_64 the filename would be Dockerfile_x86_64) that is used instead.
  • TESTCMD
The command to run for testing the image after build. Runs in a bash shell.
  • VERSION
The version of the app/tool, may need to be preset before starting the build (e.g. for binaries from github releases), or extracted from the image after build (e.g. for APK or pip packages).
  • REGISTRY
The registry to push to, defaults to the Docker Hub Registry (docker.io) or any custom registry that is set via docker configurations. Does not need to be changed for local or test builds, but to override, either pass it by setting an environment variable, or with every make command.
  • ORGNAME
The organization (or user) name under which the image repositories exist, defaults to woahbase. Does not need to be changed for local or test builds, but to override, either pass it by setting an environment variable, or with every make command.

The image may also require custom parameters (like binary architecture). Before you build, check the makefile for a complete list of parameters to see what may (or may not) need to be set.

BuildX and Self-signed certificates

If you're using a private registry (a-la docker distribution server) with self-signed certificates, that fail to validate when pulling/pushing images. You will need to configure buildx to allow insecure access to the registry. This is configured via the config.toml file. A sample is provided in the repository, make sure to replace YOUR.PRIVATE.REGISTRY with your own (include port if needed).


Make to Run


Running the image creates a container and either starts a service (for service images) or provides a shell (can be either a root-shell or usershell) to execute commands in, depending on the image. We can run the image with

make run 

But if we just need a root-shell in the container without any fance pre-tasks (e.g. for debug or to test something bespoke), we can run bash in the container with --entrypoint /bin/bash. This is wrapped in the makefile as

make shell 
Nothing vs All vs Run vs Shell

By default, if make is run without any arguments, it calls the target all. In our case this is usually mapped to the target run (which in turn may be mapped to shell).

There may be more such targets defined as per the usage of the image. Check the makefile for more information.


Push the Image


If the build and test steps finish without any error, and we want to use the image on other machines, it is the next step push the image we built to a container image repository (like /hub), for that, run the push target with

make push 

If the built image targets another architecture then it is required to specify the ARCH parameter when pushing. e.g.

make push ARCH=aarch64 
make push ARCH=armhf 
make push ARCH=armv7l 
make push ARCH=x86_64 
Pushing Multiple Tags

With a single make push, we are actually pushing 3 tags of the same image, e.g. for x86_64 architecture, they're namely

  • alpine-cgit:x86_64
The actual image that is built.
  • alpine-cgit:x86_64_(version)
It is expected that the application is versioned when built or packaged, it can be specified in the tag, this makes pulling an image by tag possible. Usually this is obtained from the parameter VERSION, which by default, is set by calling a function to extract the version string from the package installed in the container, or from github releases. Can be skipped with the parameter SKIP_VERSIONTAG to a non-empty string value like 1.
  • alpine-cgit:x86_64_(version)_(builddate)
When building multiple versions of the same image (e.g. for providing fixes or revisions), this ensures that a more recent push does not fully replace a previously pushed image. This way, although the architecture and version tags are replaced, it is possible to roll back to the previously built image by build date (format yyyymmdd). This value is obtained from the BUILDDATE parameter, and if not essential, can be skipped by setting the parameter SKIP_BUILDDATETAG to a non-empty string value like 1.
Pushing To A Private Registry

If you want to push the image to a custom registry that is not pre-configured on your system, you can set the REGISTRY variable either on the build environment, or as a makefile parameter, and that will be used instead of the default Docker Hub repository. Make sure to have push access set up before you actually push, and include port if needed. E.g.

export REGISTRY=your.private.registry:5000
make build test push

or

make build test push REGISTRY=your.private.registry:5000

Annotate Manifest(s)


For single architecture images, the above should suffice, the built image can be used in the host machine, and on other machines that have the same architecture too, i.e. after a push.

But for use-cases that need to support multiple architectures, there's a couple more things that need to be done. We need to create (or amend if already created beforehand) a manifest for the image(s) that we built, then annotate it to map the images to their respective architectures. And for our three tags created above we need to do it thrice.

Did you know?

We can inspect the manifest of any image by running

docker manifest inspect <imagename>:<optional tag, default is latest>


Tag Latest

Assuming we built the images for all four architectures, we can create/amend the manifest and annotate it to map the images tagged x86_64, aarch64, armv7l and armhf to the tag latest by running

make annotate_latest 
How it works

First we create or amend the manifest with the tag latest

docker manifest create \
woahbase/alpine-cgit:latest \
woahbase/alpine-cgit:aarch64 \
woahbase/alpine-cgit:armhf \
woahbase/alpine-cgit:armv7l \
woahbase/alpine-cgit:x86_64 \
;
docker manifest create --amend \
woahbase/alpine-cgit:latest \
woahbase/alpine-cgit:aarch64 \
woahbase/alpine-cgit:armhf \
woahbase/alpine-cgit:armv7l \
woahbase/alpine-cgit:x86_64 \
;

Then annotate the image for each architecture in the manifest with

docker manifest annotate --os linux --arch arm64 \
    woahbase/alpine-cgit:latest \
    woahbase/alpine-cgit:aarch64;
docker manifest annotate --os linux --arch arm --variant v6 \
    woahbase/alpine-cgit:latest \
    woahbase/alpine-cgit:armhf;
docker manifest annotate --os linux --arch arm --variant v7 \
    woahbase/alpine-cgit:latest \
    woahbase/alpine-cgit:armv7l;
docker manifest annotate --os linux --arch amd64 \
    woahbase/alpine-cgit:latest \
    woahbase/alpine-cgit:x86_64;

And finally, push it to the repository using

docker manifest push -p woahbase/alpine-cgit:latest

Tag Version

Next, to facilitate pulling images by version, we create/amend the manifest and annotate it to map the images tagged x86_64_(version), aarch64_(version), armv7l_(version) and armhf_(version) to the tag (version) by running

make annotate_version 
How it works

First we create or amend the manifest with the tag (version)

docker manifest create \
woahbase/alpine-cgit:(version) \
woahbase/alpine-cgit:aarch64_(version) \
woahbase/alpine-cgit:armhf_(version) \
woahbase/alpine-cgit:armv7l_(version) \
woahbase/alpine-cgit:x86_64_(version) \
;
docker manifest create --amend \
woahbase/alpine-cgit:(version) \
woahbase/alpine-cgit:aarch64_(version) \
woahbase/alpine-cgit:armhf_(version) \
woahbase/alpine-cgit:armv7l_(version) \
woahbase/alpine-cgit:x86_64_(version) \
;

Then annotate the image for each architecture in the manifest with

docker manifest annotate --os linux --arch arm64 \
    woahbase/alpine-cgit:(version) \
    woahbase/alpine-cgit:aarch64_(version);
docker manifest annotate --os linux --arch arm --variant v6 \
    woahbase/alpine-cgit:(version) \
    woahbase/alpine-cgit:armhf_(version);
docker manifest annotate --os linux --arch arm --variant v7 \
    woahbase/alpine-cgit:(version) \
    woahbase/alpine-cgit:armv7l_(version);
docker manifest annotate --os linux --arch amd64 \
    woahbase/alpine-cgit:(version) \
    woahbase/alpine-cgit:x86_64_(version);

And finally, push it to the repository using

docker manifest push -p woahbase/alpine-cgit:(version)

Tag Build-Date

Then, (optionally) we create/amend the manifest and annotate it to map the images tagged x86_64_(version)_(builddate), aarch64_(version)_(builddate), armv7l_(version)_(builddate) and armhf_(version)_(builddate) to the tag (version)_(builddate) by running

make annotate_date 
How it works

First we create or amend the manifest with the tag (version)_(builddate)

docker manifest create \
woahbase/alpine-cgit:(version)_(builddate) \
woahbase/alpine-cgit:aarch64_(version)_(builddate) \
woahbase/alpine-cgit:armhf_(version)_(builddate) \
woahbase/alpine-cgit:armv7l_(version)_(builddate) \
woahbase/alpine-cgit:x86_64_(version)_(builddate) \
;
docker manifest create --amend \
woahbase/alpine-cgit:(version)_(builddate) \
woahbase/alpine-cgit:aarch64_(version)_(builddate) \
woahbase/alpine-cgit:armhf_(version)_(builddate) \
woahbase/alpine-cgit:armv7l_(version)_(builddate) \
woahbase/alpine-cgit:x86_64_(version)_(builddate) \
;

Then annotate the image for each architecture in the manifest with

docker manifest annotate --os linux --arch arm64 \
    woahbase/alpine-cgit:(version)_(builddate) \
    woahbase/alpine-cgit:aarch64_(version)_(builddate);
docker manifest annotate --os linux --arch arm --variant v6 \
    woahbase/alpine-cgit:(version)_(builddate) \
    woahbase/alpine-cgit:armhf_(version)_(builddate);
docker manifest annotate --os linux --arch arm --variant v7 \
    woahbase/alpine-cgit:(version)_(builddate) \
    woahbase/alpine-cgit:armv7l_(version)_(builddate);
docker manifest annotate --os linux --arch amd64 \
    woahbase/alpine-cgit:(version)_(builddate) \
    woahbase/alpine-cgit:x86_64_(version)_(builddate);

And finally, push it to the repository using

docker manifest push -p woahbase/alpine-cgit:(version)_(builddate)

That's all folks! Happy containerizing!


Maintenance

Sources at Github. Built and tested at home using Buildbot. Images at Docker Hub.

Maintained (or sometimes a lack thereof?) by WOAHBase.