Using Private Go Modules

We’ve started to write some microservices in Go. In our PHP applications we use Satis hosted on S3 to share common libraries. As long as you can access the S3 bucket, you can install the packages. In Go this was a little more complex.

We use a self-hosted GitLab instance, so I’ve tailored these steps for that. But the same thing should work for private GitHub repositories as well.


The first thing you’ll need to do is to create an access token. This token should have both the read_repository scope. If you are using this on a local machine, you may need to also add the write_repository scope if you’re using HTTP to access your repository.

GitLab Token

There’s a couple options for fetching Go modules from private repositories. The most common way I’ve seen recommended is using git config url."".insteadOf "". This works, but more elegant solution is using a ~/.netrc file. This file defines automatic logins for ftp, but other programs have also adopted it, including Git.

machine login USERNAME password TOKEN

Replace, USERNAME, and TOKEN with your values. Now when you push or pull via HTTPS in Git, it will use that information to login you in. This is why you need the write_repository scope for local purposes.

This will also work with GitHub, Bitbucket, or

machine login USERNAME password TOKEN

One final thing that you’ll have to do is set a GOPRIVATE environment variable. This contains a comma-separated list of module prefixes. Save this value to your ~/.bashrc or ~/.zshrc.


Now you can run go get and start pulling private modules.


To allow for installing private modules in a Docker image, we use build arguments and create the ~/.netrc file mentioned above.

Here is what our Dockerfile looks like.

FROM golang:1.13-alpine AS builder



# Allow pulling private modules
RUN apk add --no-cache git
RUN echo "machine login ${GITLAB_LOGIN} password ${GITLAB_TOKEN}" > ~/.netrc

# prevent the reinstallation of vendors at every change in the source code
COPY go.mod go.sum ./
RUN go mod download && go mod verify

# Copy and build the app
COPY . .
RUN go build -o bin/app .

FROM alpine:latest

COPY --from=builder /app/bin/app /app
RUN chown nobody: /app
USER nobody

CMD [ "/app" ]

Then when we build the Docker image we pass the arguments. Make sure you’re using a token with only the read_repository scope otherwise changes could be pushed from the Docker container.

docker build --build-arg GITLAB_LOGIN=${GITLAB_LOGIN} --build-arg GITLAB_TOKEN=${GITLAB_TOKEN} company/app .

If you’re using Docker Compose, you can pass the args in the build option.

version: '3.7'
      context: .
      target: builder
    image: company/app
      - 8080:8080
      - ./:/app:cached
    command: go run ./main.go

Continuous Integration

The same methodology applies to Continuous Integration. Again, make sure the token you’re using has only the read_repository scope. You don’t want to accidentally push or give someone else access to your repositories.

We use GitLab CI, but the same should apply to other CI platforms like Circle or Jenkins. In our before_script, we’re crafting our ~/.netrc file, pulling values from CI/CD Variable configuration.

  image: golang:1.13
    - echo "machine login ${GITLAB_LOGIN} password ${GITLAB_TOKEN}" > ~/.netrc

  extends: .golang
  script: make test

Comments? I'm @mloberg on Twitter.


Matt Loberg

Matt Loberg

Software Engineer passionate about DevOps and Open Source.