diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4e6bd496abd1f2e80547e01c7798d91b4fa9eb02
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,49 @@
+language: go
+go_import_path: github.com/go-lpc/mim
+
+go:
+ - 1.13.x
+
+os:
+ - linux
+
+arch:
+ - amd64
+ - arm64
+
+cache:
+ directories:
+   - $HOME/.cache/go-build
+   - $HOME/gopath/pkg/mod
+
+git:
+ depth: 10
+
+matrix:
+ fast_finish: true
+ include:
+   - go: 1.13.x
+     env:
+       - TAGS=""
+       - COVERAGE="-coverpkg=github.com/go-lpc/mim/..."
+   - go: master
+     env:
+       - TAGS=""
+       - COVERAGE="-race"
+
+sudo: required
+
+notifications:
+  email:
+    recipients:
+      - binet@cern.ch
+    on_success: always
+    on_failure: always
+
+script:
+ - go get -d -t -v ./...
+ - go install -v $TAGS ./...
+ - go run ./ci/run-tests.go $TAGS $COVERAGE
+
+after_success:
+ - bash <(curl -s https://codecov.io/bash)
diff --git a/ci/run-tests.go b/ci/run-tests.go
new file mode 100644
index 0000000000000000000000000000000000000000..051e6062b5db2609e610d95b0d0296a6923d74e5
--- /dev/null
+++ b/ci/run-tests.go
@@ -0,0 +1,123 @@
+// Copyright 2020 The go-lpc Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build ignore
+
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"flag"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"strings"
+	"time"
+
+	"golang.org/x/xerrors"
+)
+
+func main() {
+	log.SetPrefix("ci: ")
+	log.SetFlags(0)
+
+	start := time.Now()
+	defer func() {
+		log.Printf("elapsed time: %v\n", time.Since(start))
+	}()
+
+	var (
+		race    = flag.Bool("race", false, "enable race detector")
+		cover   = flag.String("coverpkg", "", "apply coverage analysis in each test to packages matching the patterns.")
+		tags    = flag.String("tags", "", "build tags")
+		verbose = flag.Bool("v", false, "enable verbose output")
+	)
+
+	flag.Parse()
+
+	pkgs, err := pkgList()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	f, err := os.Create("coverage.txt")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer f.Close()
+
+	args := []string{"test"}
+
+	if *verbose || *cover != "" || *race {
+		args = append(args, "-v")
+	}
+	if *cover != "" {
+		args = append(args, "-coverprofile=profile.out", "-covermode=atomic", "-coverpkg="+*cover)
+	}
+	if *tags != "" {
+		args = append(args, "-tags="+*tags)
+	}
+	switch {
+	case *race:
+		args = append(args, "-race", "-timeout=20m")
+	default:
+		args = append(args, "-timeout=10m")
+	}
+	args = append(args, "")
+
+	for _, pkg := range pkgs {
+		args[len(args)-1] = pkg
+		cmd := exec.Command("go", args...)
+		cmd.Stdin = os.Stdin
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		err := cmd.Run()
+		if err != nil {
+			log.Fatal(err)
+		}
+		if *cover != "" {
+			profile, err := ioutil.ReadFile("profile.out")
+			if err != nil {
+				log.Fatal(err)
+			}
+			_, err = f.Write(profile)
+			if err != nil {
+				log.Fatal(err)
+			}
+			os.Remove("profile.out")
+		}
+	}
+
+	err = f.Close()
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func pkgList() ([]string, error) {
+	out := new(bytes.Buffer)
+	cmd := exec.Command("go", "list", "./...")
+	cmd.Stdout = out
+	cmd.Stderr = os.Stderr
+	cmd.Stdin = os.Stdin
+
+	err := cmd.Run()
+	if err != nil {
+		return nil, xerrors.Errorf("could not get package list: %w", err)
+	}
+
+	var pkgs []string
+	scan := bufio.NewScanner(out)
+	for scan.Scan() {
+		pkg := scan.Text()
+		if strings.Contains(pkg, "vendor") {
+			continue
+		}
+		pkgs = append(pkgs, pkg)
+	}
+
+	return pkgs, nil
+}